C++------浅拷贝、深拷贝和写实拷贝

浅拷贝

先来看一个例子:

class String 
{
public:    
    String(const char * pData = "") 
        :_pData(new char[strlen(pData) + 1])    
    {
            strcpy(_pData, pData);
    }    
    ~String()    
    {
        if (NULL != _pData)         
        {
            delete[] _pData;             
            m_pData = NULL;
        }
    } 
private:    
    char *_pData;
}; 
int main() 
{
    String s1;    
    String s2("hello");
    String s3(s2);    
    String s4;    
    s4 = s3;
}

运行程序,我们会发现程序崩溃了,为什么会出现这种情况呢?就是因为浅拷贝。

浅拷贝,当类里面有指针对象时,拷贝构造和赋值运算符重载都只进行值拷贝(浅拷贝),两个对象指向同一块内存,对象销毁时该空间被释放了两次,因此程序崩溃
来看张图理解一下:
这里写图片描述

在我们利用s2构造s3的时候,相当于调用了图中的拷贝构造函数。而这个拷贝构造的函数实际上只是将s3的地址指向了s2。

这样就有了很大的缺陷

  • 一旦对s3进行操作,s2的内容也会变化
  • 析构时先析构s3,再析构s2,但是由于s2,s3指向同一片空间,会发生一片空间的二次析构的情况导致出错

解决这种问题的方法就是深拷贝


深拷贝

深拷贝:重新开辟一块和源空间大小相同的空间,再将源空间的内容拷贝下来,保证了不同的对象指向不同的地址空间
如图:
这里写图片描述
来直接看一下深拷贝的实现:这里实现了传统写法和简洁写法

class String
{
public:

    String(const char* pStr = "")
        :_pStr(new char[strlen(pStr)+1])
    {
        strcpy(_pStr, pStr);
    }
    //深拷贝普通版
    String(const String& s)
        :_pStr(new char[strlen(s._pStr) + 1])
    {
        strcpy(_pStr, s._pStr);
    }
    String& operator=(const String& s)
    {
        if (this != &s)
        {
            char* temp = new char[strlen(s._pStr) + 1];
            strcpy(temp, s._pStr);
            delete[] _pStr;
            _pStr = temp;
        }
        return *this;
    }
    //深拷贝简洁版,利用构造函数
    //String(const String& s)
    //{
    //  String strTemp(s._pStr);
    //  swap(_pStr, strTemp._pStr);
    //}
    //赋值运算符重载
    //String& operator=(const String& s)
    //{
    //  if (this != &s)
    //  {
    //      String strTemp(s);
    //      swap(_pStr, strTemp._pStr);
    //  }
    //  return *this;
    //}
    ~String()
    {
        if (_pStr)
        {
            delete[] _pStr;
            _pStr = NULL;
        }
    }
    /******************Access******************/
    size_t Size()const
    {
        return sizeof(_pStr);
    }
    size_t Lengh()const
    {
        return strlen(_pStr);
    }
    char& operator[](size_t index)
    {
        return _pStr[index];
    }
    const char& operator[](size_t index) const
    {
        return _pStr[index];
    }
    bool operator>(const String& s)
    {
        if (strcmp(_pStr, s._pStr) > 0)
            return true;
        if (strcmp(_pStr, s._pStr) < 0)
            return false;
    }
    bool operator<(const String& s)
    {
        return !(_pStr>s._pStr);
    }
    bool operator==(const String& s)
    {
        if (strcmp(_pStr, s._pStr) == 0)
            return true;
        return false;
    }
    bool operator!=(const String& s)
    {
        return !(_pStr, s._pStr);
    }
    void Copy(const String& s)
    {
        strcpy(_pStr, s._pStr);
    }
    String& operator+=(const String& s)
    {
        char* dest;
        dest = new char[strlen(s._pStr) + Lengh() + 1];
        memcpy(dest, _pStr, Lengh());
        memcpy(dest + Lengh(), s._pStr, strlen(s._pStr) + 1);
        delete[] _pStr;
        _pStr = dest;
        return *this;
    }
private:
    char* _pStr;
};

写实拷贝

写实拷贝 其实还是一种浅拷贝,但是它改进了浅拷贝的缺陷。
写实拷贝引入一个计数器,每片不同内容的空间上都再由一个计数器组成,在构造第一个类时,计数器初始化为1,之后每次有新的类也指向同一片空间时,计数器自增;在析构时判断该片空间对应计数器是否为1,为1则执行清理工作,大于1则计数器自减。如果有需要进行增删等操作时,再拷贝空间完成,有利于提高效率

来看看写实拷贝的几种写法:

写法一:

class String
{
public:
    String(const char* pStr = "")
        :_pCount(new int(1))
    {
        if (pStr == NULL)
        {
            _pStr = new char[1];
            *_pStr = '\0';
        }
        else
        {
            _pStr = new char[strlen(pStr) + 1];
            strcpy(_pStr, pStr);
        }
    }
    String(const String& s)
        :_pStr(s._pStr)
        , _pCount(s._pCount)
    {
        ++(*_pCount);
    }
    String& operator=(const String& s)
    {
        if (this != &s)
        {
            if (--(*_pCount) == 0)
            {
                delete[] _pStr;
                delete _pCount;
            }
            _pStr = s._pStr;
            _pCount = s._pCount;
            ++(*_pCount);
        }
        return *this;
    }
    ~String()
    {
        if (_pStr&&--(*_pCount) == 0)//判断pCount状态,在进行析构
        {
            delete[] _pStr;
            _pStr = NULL;
            delete _pCount;
            _pCount = NULL;
        }
    }
private:
    char* _pStr;
    int* _pCount;//增加一个成员变量代表计数器
};

图解:
这里写图片描述
我们可以发现,这种方式确实解决了析构多次的问题,但同时也引入了新的问题,每当我们创建一个新的类对象,就会多开辟4个字节,导致空间出现许多内存碎片。

写法二:

class String
{
public:
    String(const char* pStr = "")
    {
        if (pStr == NULL)
        {
            _pStr = "";
        }
        _pStr = new char[strlen(pStr) + 1 + 4];
        _pStr += 4;
        strcpy(_pStr, pStr);
        GetReference() = 1;
    }
    String(const String& s)
        :_pStr(s._pStr)
    {
        ++GetReference();
    }
    String& operator=(const String& s)
    {
        if (this != &s)
        {
            Release();
            _pStr = s._pStr;
            ++GetReference();
        }
        return *this;
    }
    ~String()
    {
        Release();
    }
private:
    int& GetReference()
    {
        return *(int *)(_pStr - 4);
    }
    void Release()
    {
        if (_pStr&&--GetReference() == 0)
        {
            _pStr -= 4;
            delete[] _pStr;
            _pStr = NULL;
        }
    }
private:
    char* _pStr;
};

这种方式我们没有增加类成员变量,而是在_pStr的首部多开辟四个字节,保存计数信息,如图:
这里写图片描述
注:因为我们将计数器存放在_pStr-4的地址上,析构的时候一定要全部析构,避免内存泄漏

猜你喜欢

转载自blog.csdn.net/qq_34021920/article/details/80100713