浅拷贝:
对于String类的拷贝构造函数或operator=函数来说:
当用一个对象拷贝构造函数或赋值给另一个String对象时,其实就是将这个对象里的指针的值赋值给另一个对象里的指针,
而此时会使得两个指针指向同一块空间,这就产生了“浅拷贝”;
分析问题:
两个或两个以上指针指向同一块空间,这个内存就会被释放多次;
另一方面,当两个指针指向同一块空间时,一旦一个指针修改了这块空间值,另一个指针指向的空间的值也会被修改
例如:string s1(“hello”);string s2(s1);此时s1,s2里面的指针就会指向同一块空间,当出了作用域,s2先调用析构函数,即该空间被释放,
接下来调用s1析构函数,也会去释放这块空间,释放一块已经被释放的空间;
解决办法:1)深拷贝
对于拷贝构造函数来说,s2先开一块和s1一样大的空间;对于赋值运算符重载函数来说,s2已经存在,则必须先释放s2的空间然后让s2
开辟与s1一样大的空间;
然后让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;
}
//拷贝构造的现代写法
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;
}
2)引用计数的写时拷贝
1.常用场景:
①有时会多次调用拷贝构造函数,但是拷贝构造的对象并不会修改这块空间的值;
②如果采用深拷贝,每次都会重复的开空间,然后拷数据,最后再释放这块空间,这会花费很大的精力。
③我们想到浅拷贝不用重复的开空间,但是会有问题;为了解决释放多次的问题可以采用引用计数,当有新的指针指向这块空间的时候,我们可以增加引用计数,当这个指针需要销毁时,就将引用计数的值减1,当引用计数的值为1时才去释放这块空间;
④当有一个指针指需要修改其指向空间的值时,才去开一块新的空间(也就是写时拷贝);
⑤这相当于一个延缓政策,如果不需要修改,则不用开新的空间,毕竟开空间需要很大的消耗。
⑥引用计数解决了空间被释放多次的问题,写时拷贝解决了多个指针指向同一块空间会修改的问题。