拿String类举例:
class String
{
public:
String(const char* str = "")
:_str(new char[strlen(str) + 1])
{
strcpy(_str, str);
}
~String()
{
delete[] _str;
}
private:
char* _str;
};
void test1()
{
String s1("abcd");
for (size_t i = 0; i < 100; i++)
{
String s2(s1);
}
}
可以看出来,这个类是没有写拷贝构造函数的,那么在test1的循环,这100次拷贝构造都是浅拷贝,首先来说什么是浅拷贝呢?
那么就会有两个问题:
1.两个对象的_str指向同一片空间,那么析构的时候这片空间必然会析构两次。
2.一个的改变会影响另一个。
所以程序必然会崩溃
那么写出拷贝构造函数,变成深拷贝可以解决这个问题。
但是这种循环多次的程序,如果写成深拷贝,那么每次都要重新开辟空间,效率不高。
为了解决问题,使用引用计数:
class String { public: 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); } ~String() { if (--(*_refCount) == 0) { delete[] this->_str; delete this->_refCount; } } private: char* _str; int* _refCount; };
在类里多写一个int*型的成员变量,用来记录有多少对象指向同一片空间,并初始化为1,因为创建对象的时候就会有一个对象指向这片空间。
每次拷贝构造或者赋值运算符重载,就给(*refCount)加上1,表示多了一个对象指向这片空间。
那么拷贝构造如何写?赋值运算符重载怎么写?
String(String& s) { this->_str = s._str; _refCount = s._refCount; ++(*this->_refCount); } // s1 = s2 String& operator =(const String& s) { if (this->_str != s._str) { if (--(*this->_refCount) == 0) { delete[] _str; delete _refCount; } this->_str = s._str; this->_refCount = s._refCount; ++(*s._refCount); } return *this; }
现在已经解决了析构多次的情况,只有当*refCount等于1的时候才会进行delete。
但是第二个问题呢,现在一个改变还是会影响另一个,比如operator[]
char& operator[](size_t index)//可读,有可能可写 { return _str[index]; }在测试函数里:
String s1("abc");
s1.[0] = 'x';
就会改变s1的值,相应的s2的值也会改变,所以需要写一个函数来改变s1的指向。
void String::CopyOnWrite() { if (*_refCount > 1)//如果只有一个对象指向这片空间,那么可以直接修改,如果它的_refCount>1,有多个对象指向这片空间,\ 那一个的改变会影响其他的对象,要对它处理 { char* tmp = new char[strlen(this->_str)+1]; strcpy(tmp, _str); _str = tmp; --(*_refCount); _refCount = new int(1); } }
这样同样解决了一个对象的改变会影响另一个对象的问题。
接下来还有一种方法,我把它叫做多开4字节存数据的方法:
class String { public: String(const char* str) :_str(new char[strlen(str) + 1 + 4]) { *((int*)_str) = 1;//此时_str指向最前的地方,让他的前四个字节存有多少对象指向空间,初始化为1 _str += 4; strcpy(_str, str);//前四个字节存数据,之后才存字符(串) } ~String() { if (--(*((int*)(_str - 4))) == 0) { delete[] _str; } } private: char* _str; };
析构的时候是,如果只有一个对象指向这片空间,那么要delete[] _str-4,注意一点要给_str-4,不然前四个字节就释放不了,如果有多个对象指向这片空间,就给引用计数--。
那么拷贝构造与赋值运算符重载呢?
String(String& s) :_str(s._str) { *((int*)(_str - 4)) += 1; } // s1 = s2 String& operator=(const String& s) { if (_str != s._str) { if (--(*((int*)(_str - 4))) == 0) { delete[] (_str-4); } _str = s._str; *((int*)(s._str - 4)) += 1; } return *this; }
解决一个改变影响另一个的问题,也是同样的思路。
void String::CopyOnWrite() { if (--(*((int*)(_str - 4))) > 1) { char* tmp = new char[strlen(_str) + 1 + 4]; tmp += 4; strcpy(tmp, this->_str); *((int*)(_str - 4)) -= 1; _str = tmp; *((int*)(_str - 4)) += 1;//_str已经指向新开辟的空间了 } }
现在的话,一个的改变已经不会影响另一个:
char& String::operator[](size_t index) { CopyOnWrite(); return _str[index]; } void test1() { String s1("abcd"); String s2(s1); String s3("x"); s3 = s1; s1[0] = 'y'; }我写一个operator[],然后验证程序:
已经让s1,s2,s3的_str指向同一空间,然后执行s1[0] = 'y';
可以看到,s1的改变并没有影响s2和s3。