一、浅拷贝
一个类,如果不写拷贝构造函数,那么它的默认拷贝构造函数是浅拷贝,浅拷贝有什么问题呢?
用一个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;
};
int main()
{
String s1("hello String");
String s2(s1);
return 0;
}
在main函数中,创建了s1对象,然后通过s2对象是通过s1拷贝构造而来,由于没有写拷贝构造函数,所以编译器会自动生成成,但编译器自动生成的是浅拷贝,所以程序会崩溃。
因为是浅拷贝,所以两个对象的_str指针同时指向了一块空间,然后这两个对象的生命周期结束时,都会调用析构函数,那么这块空间就会double free,也就是被析构了两次;
还有一个问题,如果改变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指向的空间同时被多少对象所指向
1、构造对象时,new出来的空间只被_str指向,所以引用计数是1
String(char* str = "\0")
:_str(new char[strlen(str)+1])
,_refCount(new int(1))
{
strcpy(_str, str);
}
2、拷贝构造时,多出来一个对象的指针指向_str,所以引用计数要+1
String(String& s)
{
this->_str = s._str;
_refCount = s._refCount;
++(*this->_refCount);
}
3、operator同样是需要拷贝,所以多出来一个对象指向_str,引用计数+1
和拷贝构造不一样,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()
{
//如果引用计数为1,直接写不影响
if(*refCount > 1)
{
char* tmp = new char[strlen(_str)+1];
strcpy(tmp, _str);
(*refCount)--;
_str = tmp;
}
}
接下来,只需要把所有可能被用户用来改变数据的接口,先写时拷贝一份就好了。
例如:
char& operator[](size_t pos)
{
CopyOnWrite();
return _str[pos];
}