浅拷贝
先来看一个例子:
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的地址上,析构的时候一定要全部析构,避免内存泄漏