拿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。