一:浅拷贝
一个类,如果不写拷贝构造函数,那么它的默认拷贝构造函数为浅拷贝,浅拷贝有什么问题呢?
拿一个简单的String类举例:
class String{
public:
String(char* str = "\0")
:_str(new char[strlen(str)+1])
{
strcpy(_str, str);
}
~String()
{
if (_str){
delete[] _str;
}
}
private:
char* _str;
void Test()
{
String s1("hello String!");
String s2(s1);
}
在Test函数里,我创建了s1对象,然后s2对象通过s1来拷贝构造,由于没有写拷贝构造函数,所以为浅拷贝,运行程序直接崩溃:
原因是什么呢?
因为是浅拷贝,所以两个对象的_str指针同时指向了一块空间,然后这两个对象生命周期结束时,都会调用析构函数,那么这一块空间就被析构了两次,所以会崩溃;
还有一个问题,如果改变s1对象的指针指向的空间,由于两对象的指针指向同一块空间,所以s2对象的指针所指向的空间也一块变了,这不是我们想看到的,两对象应该各是各的。
大白话总结一下浅拷贝的问题:
1.析构多次;
2.一个改变影响另一个。
二:深拷贝可以解决浅拷贝的问题
什么是深拷贝,就是我拷贝你的时候,重开一块空间,然后把你的数据复制到我的空间里,咱俩各是各的,以后就不影响了。
代码实现:
String(const String& s)
:_str(new char[strlen(s._str)+1])
{
strcpy(_str, s._str);
}
三:深拷贝要拷贝数据,代价过大
我们可以既做到浅拷贝,又能解决析构时一块空间被释放多次,那就是引用计数。
private:
char* _str;
int* _refCount;
_refCount指向的空间就专门用来存_str指向的空间同时被多少对象指向。
①构造对象时,new出来的空间当然只被_str指向,所以引用计数初始化为1。
String(const char* str = "")
:_str(new char[strlen(str) + 1])
, _refCount(new int(1))
{
strcpy(_str, str);
}
②拷贝构造时,多出来一个对象的指针指向空间,所以引用计数要加一。
String(String& s)
{
this->_str = s._str;
_refCount = s._refCount;
++(*this->_refCount);
}
③ operator= 同样是需要拷贝,所以又多出来了一个对象指向空间,引用计数再加一
和拷贝构造不一样,operator=是把一个已经构造好的对象重新赋值,而拷贝构造是构造一个新对象,所以在operator之前,我们需要先考虑之前构造好的对象它的引用计数是多少?
.如果之前的对象引用计数为1,我们给又给它重新赋值,所以之前的对象的指针指向的空间就应该被释放了:
.如果之前的对象引用计数为大于1,就算重新赋值了,也还有其他对象的指针指向这块空间,所以不需要释放。
String& operator=(const String& s)
{
if (this != &s)
{
if ((*_refCount) == 1){
delete[] _str;
delete _refCount;
}
_str = s._str;
_refCount = s._refCount;
(*_refCount)++;
}
return *this;
}
使用引用计数,重要的是析构函数怎么写,每一次析构都要给引用计数减1,只有当引用计数为1时,才释放空间。
~String()
{
if (--(*_refCount) == 0)
{
delete[] this->_str;
delete this->_refCount;
}
}
到目前为止,已经通过引用计数解决了浅拷贝的析构多次会崩溃的问题,但是还是没有解决一个改变影响另一个的问题。
四:通过写时拷贝来解决浅拷贝的一个改变影响另一个的问题
什么是写时拷贝,就是写的时候才拷贝,也就是说,你如果要改变,就需要拷贝一份,改变的是拷贝的这一份。
什么时候可能会改变String类的数据呢?
char& operator[](size_t pos)
{
return _str[pos];
}
operator[]时,我返回引用,这样你可以通过我的返回值来改变我的_str指针指向的数据了。
写时拷贝实现:
void CopyOnWrite()
{
if (*_refCount > 1){//如果引用计数为1,直接写不影响
char* tmp = new char[strlen(_str) + 1];
strcpy(tmp, _str);
(*_refCount)--;
str = tmp;
}
}
接下来我把所有用户可能通过用来改变数据的接口先写时拷贝一份就好了,比如:
char& operator[](size_t pos)
{
CopyOnWrite();
return _str[pos];
}