【c++学习笔记】深入解析浅拷贝与深拷贝

测试环境:vs2013

什么是浅拷贝

  • 也称位拷贝,编译器只是将对象中的值拷贝过来,如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进行操作时,就会发生发生了访问违规。

先看下面的代码有问题吗

class String
{
public:
    String(const char* ptr = "")//构造函数,缺省放'\0'
        :_ptr(new char[strlen(ptr) + 1])//strlen计算出的长度不加\0
    {
        strcpy(_ptr, ptr);
    }

    ~String()
    {
        if (_ptr)
        {
            cout << "~String()" << this << endl;
            delete[] _ptr;
        }
    }

private:
    char* _ptr;
};

int main()
{
    String s1("hello");
    String s2(s1);

    return 0;
}

执行结果如下:
这里写图片描述

  • 可以发现这里程序会崩,为什么会造成这样的原因呢,那就必须搞清楚上面的代码都做了什么
    这里写图片描述
    这就是浅拷贝,多个对象共享同一份资源,造成的问题也显而易见,一份资源被释放了多次。那么,怎么解决呢?

引用计数

  • 当多个对象共享一块资源时,要保证该资源只释放一次,只需记录有多少个对象在使用该资源即可,每减少(增加)一个对象使用,给该计数减一(加一),当最后一个对象不使用时,该对象负责将资源释放掉即可;观察下面代码:
class String
{
public:
    String()
    {}

    String(const char* str )
    {
        if (str == NULL)
        {
            _str = new char[4+1];//多申请一个int用来存储计数器
            _str += 4;
            _str = '\0';
        }
        else
        {
            _str = new char[strlen(str) + 1 + 4];
            _str += 4;
            strcpy(_str, str);
        }

        GetCount(_str) = 1;//将计数器初始值设置为1
    }

    String(const String& s)//拷贝构造
        :_str(s._str)
    {
        GetCount(_str)++;
    }

    //s1=s2
    String& operator=(const String& s)//赋值操作符重载
    {
        if (_str != s._str)
        {
            //1.当s1的引用计数为1时
            //①释放空间
            //②改变s1指针的指向
            //③s2的引用计数加1
            Release();
            //当s1的引用计数大于1时
            //①s1的引用计数减1
            //同上②③

            _str = s._str;
            ++GetCount(_str);//引用计数+1
        }

        return *this;
    }

    ~String()//析构函数
    {
        Release();
    }

private:
    int& GetCount(char* str)
    {
        return *(int*)(str - 4);//因为这块内存类型为char
    }

    void Release()
    {
        if (_str != NULL && (--GetCount(_str)) == 0)
        {
            delete [](_str - 4);//一定要释放存储计数器的空间
        }
    }

private:
    char* _str;
};


int main()
{
    String s1("hello");
    String s2(s1);

    String s3("world");
    String s4(s3);
    String s5(s3);

    s1 = s3;

}

上面的代码是在构造的时候多申请4字节的空间用来存储计数器,从而实现引用计数。那么具体是怎么做的呢?
这里写图片描述
但是这样还是有问题,看下面的代码

class String
{
public:
    String()
    {}

    String(const char* str)
    {
        if (str == NULL)
        {
            _str = new char[4 + 1];//多申请一个int用来存储计数器
            _str += 4;
            _str = '\0';
        }
        else
        {
            _str = new char[strlen(str) + 1 + 4];
            _str += 4;
            strcpy(_str, str);
        }

        GetCount(_str) = 1;//将计数器初始值设置为1
    }

    String(const String& s)//拷贝构造
        :_str(s._str)
    {
        GetCount(_str)++;
    }

    //s1=s2
    String& operator=(const String& s)//赋值操作符重载
    {
        if (_str != s._str)
        {
            Release();
            _str = s._str;
            ++GetCount(_str);//引用计数+1
        }

        return *this;
    }

    char& operator[](size_t index)//可以采用下标的方式访问String类
    {
        return _str[index];
    }


    ~String()//析构函数
    {
        Release();
    }

private:
    int& GetCount(char* str)
    {
        return *(int*)(str - 4);//因为这块内存类型为char
    }

    void Release()
    {
        if (_str != NULL && (--GetCount(_str)) == 0)
        {
            delete[](_str - 4);//一定要释放存储计数器的空间
        }
    }

private:
    char* _str;
};


int main()
{
    String s1("hello");
    String s2(s1);
    String s3(s1);
    s3[1] = 'a';

    return 0;
}

这里写图片描述

  • 当几个共用同一块空间的对象中的任一对象修改字符串中的值,则会导致所有共用这块空间的对象中的内容被破坏掉。像上面的代码中,我们只想改变s3中的值,但是和他公用空间的另外两个对象s2,s1中的值也改变了,这就引出了写时拷贝

写时拷贝

  • 有多个对象共享同一个空间时,当对其中一个对象只读时,不会有什么影响,但是如果想要改变(写入)某一个对象中的值时,这时就要为这个对象重新分配空间。代码实现如下:
class String
{
public:
    String()
    {}

    String(const char* str)
    {
        if (str == NULL)
        {
            _str = new char[4 + 1];//多申请一个int用来存储计数器
            _str += 4;
            _str = '\0';
        }
        else
        {
            _str = new char[strlen(str) + 1 + 4];
            _str += 4;
            strcpy(_str, str);
        }

        GetCount() = 1;//将计数器初始值设置为1
    }

    String(const String& s)//拷贝构造
        :_str(s._str)
    {
        GetCount()++;
    }

    //s1=s2
    String& operator=(const String& s)//赋值操作符重载
    {
        if (_str != s._str)
        {
            Release();

            _str = s._str;
            ++GetCount();//引用计数+1
        }

        return *this;
    }

    char& operator[](size_t index)//可以采用下标的方式访问String类
    {
        if (GetCount() > 1)
        {
            --GetCount();
            char* pTmp = new char[strlen(_str) + 1 + 4];
            pTmp += 4;
            strcpy(pTmp, _str);
            _str = pTmp;
            GetCount() = 1;//将新空间值赋1
        }

        return _str[index];
    }

    const char& operator[](size_t index)const//[]操作符必须成对重载
    {
        return _str[index];
    }

    ~String()//析构函数
    {
        Release();
    }

private:
    int& GetCount()
    {
        return *(int*)(_str - 4);//因为这块内存类型为char
    }

    void Release()
    {
        if (_str != NULL && (--GetCount()) == 0)
        {
            delete[](_str - 4);//一定要释放存储计数器的空间
        }
    }

private:
    char* _str;
};


int main()
{
    String s1("hello");
    String s2(s1);
    String s3(s1);
    s3[1] = 'a';

    return 0;
}

深拷贝

  • 给要拷贝构造的对象重新分配空间
class String
{
public:

    String(const char* str = "")
        :_str(new char[strlen(str)+1])
    {
        strcpy(_str, str);
    }

    String(const String& s)//深拷贝
        : _str(new char[strlen(s._str) + 1])
    {
        strcpy(_str, s._str);
    }
    //赋值操作符重载
    //方法一
    //String& operator=(const String& s)
    //{
    //  if (this != &s)
    //  {
    //      delete[] _str;
    //      _str = new char[strlen(s._str) + 1];
    //      strcpy(_str, s._str);
    //  }
    //  return *this;//为了支持链式访问
    //}

    //方法二(优)
    String& operator=(const String& s)
    {
        if (this != &s)//自己不能拷贝自己
        {
            char* tmp = new char[strlen(s._str) + 1];
            strcpy(tmp, s._str);
            delete[] _str;
            _str = tmp;
        }
        return *this;//为了支持链式访问
    }

    ~String()
    {
        if (_str != NULL)
        {
            delete[]_str;
        }
    }

private:
    char* _str;
};
int main()
{
    String s1("hello");
    String s2(s1);

    String s3("world");
    s1 = s3;
}
  • 一般情况下,上面对赋值操作符重载的两种写法都可以,但是相对而言,第二种更优一点。
    对于第一种,先释放了旧空间,但是如果下面用new开辟新空间时有可能失败——>抛异常,而这时你是将s2赋值给s3,不仅没有赋值成功(空间开辟失败),而且也破坏了原有的s3对象。对于第二种,先开辟新空间,将新空间的地址赋给一个临时变量,就算这时空间开辟失败,也不会影响原本s3对象。

猜你喜欢

转载自blog.csdn.net/virgofarm/article/details/80793193
今日推荐