浅拷贝
对于String类的拷贝构造早函数及operator=函数来说,当用一个string对象拷贝构造或复制给另一个String对象时,就是讲这个对象里的指针的值赋值给另一个对象里的指针。讲一个帧赋值给另一个指针,就会使得两个指针指向同一块空间,就产生了浅拷贝。
class String
{
public:
String(const char* str)
:_str(new char[strlen(str) + 1])
{
strcpy(_str, str);
}
String(const String& s)
:_str(s._str)
{}
String& operator=(const String& s)
{
if (this != &s)
{
_str = s._str;
}
return *this;
}
~String()
{
if (_str)
{
delete[] _str;
}
_str = NULL;
}
private:
char* _str;
};
int main()
{
String s1("hello");
String s2(s1);
return 0;
}
浅拷贝存在的问题:
1.两个及以上指针指向同一块空间,这个内存就会被释放多次;
例子:定义了一个Sting对象s1,以浅拷贝的方式拷贝构造了一个String对象s2,则s1和s2中的指针_str就会指向同一块空间;出了作用域,s2先调用析构函数进行空间的释放,也就是所指向的空间会被s2释放,接下来s1也回去调用构函数去释放这块空间,但是这块空间已经被释放了,所以就会出错。
s2析构之后,s1再析构会出错。
2.因为两个指针指向同一块空间,所以一旦一个指针修改了这块空间,另一个指针指向的空间的值也会被修改。
浅拷贝的解决方法
1.深拷贝
2.引用数的写实拷贝
深拷贝
传统写法
若用一个s1对象拷贝构造或赋值给s2对象,当涉及到浅拷贝问题的时候:
对于拷贝构造函数来说,s2先开辟一块和s1一样大的空间;而对于赋值运算符重载函数来说s2已经存在,则必须先释放s2的空间然后让s2开辟与s1一样大的空间,否则就会导致s2里面的指针没有释放。
然后让s2指向这块新开的空间,最后将s1里面的数据拷贝至s2指向的空间(自己开空间自己拷数据)。
String(const String& s)
{
_str = new char[strlen(s._str) + 1]; //开空间
strcpy(_str, s._str); //拷数据
}
//赋值的传统写法
String& operator=(const String& s) //必须要返回引用,为了连续的赋值
{
if (this != &s)
{
delete[] _str;
_str = NULL;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
}
return *this;
}
现代写法:
特点:让别人去开空间,去拷贝数据,而我将你的空间与我交换就可以了。
实现:比如用s1拷贝构造一个s2对象,可以通过构造函数将s1里的指针_str构造一个临时对象tmp(构造函数不仅会开空间还会将数据拷贝至tmp),此时tmp就是我们想要的那个对象,然后将新tmp的指针_ptr与自己的指针进行交换。
对于构造函数来说,因为String有一个带参数的构造函数,则用现在写法写拷贝构造时可以调用构造函数,而对于无参的构造函数的类只能采用传统写法。
String(const String& s)
:_str(NULL)
{
String tmp(s._str); //调用构造函数,则tmp就是我们需要的
swap(_str, tmp._str); //将_str与tmp的_str指向的空间进行交换,tmp._str就会指向_str的空间,出了这个作用域,tmp就会调用析构函数,但是tmp里面的_str值可能不确定,所以在初始化列表中将_str置空,这样tmp._str=NULL
}
//赋值的现代写法
String& operator=(const String& s)
{
if (this != &s)
{
String tmp(s._str); //调用构造函数
swap(_str, tmp._str); //tmp是局部对象,出了这个作用域就会调用析构函数,就会将tmp里面的指针指向的空间释放掉,
}
return *this;
}
带引用计数的写实拷贝
为什么会出现带引用计数的写实拷贝?
1.有时会多次调用拷贝构造函数,但是拷贝构造的对象并不会修改这块空间的值。如果采用深拷贝,每次都会重复的考空间,然后拷数据,最后再释放这块空间,这样会花费很大的精力。
2.若是使用浅拷贝不用重复的开空间,但是析构会有问题,且改变一个会改变另一个。
为了解决上述问题,可以引进一个引用计数,当有新的指针指向这块空间的时候,增加引用计数,当这个指针需要销毁时,就将引用计数的值减1,当引用计数的值为1是才去释放这块空间;当有一个指针需要修改其指向空间的值的时候,才去开一块新的空间。
综上:引用计数解决了空间被多次释放的问题,写实拷贝解决了多个指针指向同一块空间会修改的问题。
引用计数的定义方式
1.将引用计数定义为int
缺点:每个对象的引用计数之间是独立的,如果增加指向这块空间的指针,也只会修改新增这个指针所在的对象的引用计数,就会使得每块空间对应引用计数不相同。
例如:用s1对象拷贝构造了s2对象,s1中的计数为1,而s2中计数为2
2.将引用计数定义为static int
缺点:因为静态成员为该类的所有对象所共享,就会使得String类创建的所有对象的指针哪怕指向不同的空间,但是这些对象的引用计数都相等。
3.将引用计数定义为int的指针
缺点:int的指针占4个字节,每次创建一个String对象,都会为其向操作系统申请呢4个字节的内存,这样就经常申请许多的小块内存,会造成内存碎片,对效率造成影响。
所以对这种方式进行改进,将_str与引用计数放在一起,就在_str的头上4个字节处存放引用计数,当我们取引用计数时,只用将*((int*)(_str-4))
class String
{
public:
String(char* str = "")
:_str(new char[strlen(str) + 5]) //因为多开了4个字节给引用计数,所以这里加5,上面引用计数和_str独立加的是1,只有这里的_str包含引用计数,后面的_str都不包含引用计数
{
_str += 4; //从_str+4才表示有效的字符,前面是引用计数
strcpy(_str, str);
GetRefCount() = 1; //将引用计数置为1
}
// s2(s1)
String(const String& s)
:_str(s._str)
{
++(GetRefCount());
}
//s2 = s1
String& operator=(const String& s)
{
if (_str != s._str)
{
if (--(GetRefCount()) == 0)
{
delete[](_str - 4);
}
_str = s._str;
++GetRefCount();
}
return *this;
}
~String()
{
if (--GetRefCount() == 0)
{
delete[](_str - 4);
}
}
int& GetRefCount()
{
return *((int*)(_str - 4));
}
const char* c_str()
{
return _str;
}
void CopyOnWrite()
{
if (GetRefCount() > 1)
{
char* newstr = new char[strlen(_str) + 5];
newstr += 4;
strcpy(newstr, _str);
--GetRefCount();
_str = newstr;
GetRefCount() = 1;
}
}
char& operator[](size_t pos)
{
CopyOnWrite();
return _str[pos];
}
private:
char* _str; // 引用计数放在_str的头上4个字节处
};
注意:引用计数的写实拷贝,读有时也会拷贝
在String类中,如果想要取出里面的某个字符或者修改某个对应位置上的字符需要重载operator[];
因为operator[]既可以读也可以修改,为了统一,无论读写,都要重新拷贝;