我们直接通过现象看问题!(下面的拷贝构造函数和赋值运算符重载是有问题的!!!)
#include <string>
class String
{
public:
String(const char* str = "")
{
if (nullptr == str)
str = "";
_str = new char[strlen(str) + 1];
strcpy(_str,str);
}
String(const String& s)
:_str(s._str)
{}
//问题:1.内存泄漏 2.与拷贝构造函数类似
String& operator=(const String& s)
{
_str = s._str;
return *this;
}
~String()
{
delete[] _str;
_str = nullptr;
}
private:
char* _str;
};
int main()
{
String s1("hello");
String s2(s1);
String s3("world");
String s4 = s3;
return 0;
}
我们可以从监视窗口清楚的看到,s1和s2共用一块内存空间。s3和s4共用一块内存空间。
上面的代码是浅拷贝的写法
怎么样来理解这个问题(我们只看拷贝构造函数的例子,赋值运算符重载的原理基本一样):首先给s1分配内存空间,地址为0x008f5308,然后放入该地址管理的资源"hello",当用s1拷贝构造s2时,调用拷贝构造函数,*str是一个指针,直接用其进行赋值,所以等于对象s1、s2共用同一块空间,那么当对象生命周期结束时,需要调用析构函数对其资源进行释放,相对于s1,优先释放s2,那么0x008f5308这段空间已被释放,当再释放s1时,那么系统就会崩溃。
正确的写法是下面的这种即深拷贝
template<class T>
void Swap(T& a, T& b)
{
T c(a);
a = b;
b = c;
}
class String
{
public:
String(const char* str = "")
{
if (nullptr == str)
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(const String& s)
{
String strTemp(s._str);
Swap(_str, strTemp._str);
}
//传统写法
String& operator=(const String& s)
{
if (this != &s)
{
//写法一:
delete[] _str;//释放旧空间
_str = new char[strlen(s._str) + 1];//申请新空间
strcpy(_str, s._str);//完成拷贝
//写法二:优点:如果开辟空间失败,原来的内容还存在
//char* pStr = new char[strlen(s._str) + 1];
//strcpy(pStr, _str);
//delete[] _str;
//_str = pStr;
}
return *this;
}
//现代写法1
/*String& operator=(const String& s)
{
if (this != &s)
{
String strTemp(s);
Swap(_str, strTemp._str);
}
return *this
}*/
//现代写法2
String& operator=(String s)
{
Swap(_str, s._str);
return *this;
}
~String()
{
delete[] _str;
_str = nullptr;
}
private:
char* _str;
};
int main()
{
String s1;
String s2(s1);
String s3;
String s4 = s3;
return 0;
}
我们可以清楚的看到深拷贝的对象的内存空间各不相同,当对象生命周期结束时,先调用析构函数对s4的资源进行清理,再调用析构函数清理s3,因为它们的内存空间各不相同,所以就不会发生冲突。
浅拷贝+引用计数来看看下面的代码。
下面的代码仅仅在单线程下时安全的
class String
{
public:
String(const char* str = "")
:_pCount(new int(1))
{
if (nullptr == str)
str = "";
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
String(String& s)
:_str(s._str)
, _pCount(s._pCount)
{
++(*_pCount);
}
String& operator=(const String& s)
{
if (this != &s)
{
//先处理自己的资源
if (0 == --(*_pCount) && _str)
{
delete[] _str;
_str = nullptr;
delete _pCount;
_pCount = nullptr;
}
//共享资源
_str = s._str;
_pCount = s._pCount;
//计数+1
++(*_pCount);
}
}
char& operator[](size_t index)
{
if (*_pCount > 1)
{
String str(_str);
this->Swap(str);
}
return _str[index];
}
~String()
{
if (0 == --(*_pCount) && _str)
{
delete[] _str;
_str = nullptr;
delete[] _pCount;
_pCount = nullptr;
}
}
void Swap(String& s)
{
swap(_str, s._str);
swap(_pCount, s._pCount);
}
private:
char* _str;
int* _pCount;
};
void test()
{
String s1("hello");
String s2(s1);
s1[0] = 'H';//写时拷贝
String s3;
}
从上面我们可以看出,这样也可以解决问题,就是加一个pCount计数器,当有对象使用这块空间时,计数+1,当该对象生命周期结束时,计数-1,当该计数为0时,就释放这块空间,这里这个计数不能通过静态成员变量来实现,因为静态成员变量时全局作用域的,当不发生拷贝构造时,指向不同空间的不同对象也使用同一个计数count,就会出现问题。