首先,我们介绍浅拷贝,又浅拷贝推出深拷贝,引用计数拷贝以及写时拷贝
浅拷贝:
我们由下面的代码来找出浅拷贝存在的问题
源代码:
class String
{
public:
String(const char *pstr = "")
{
if(NULL == pstr)
{
_pstr = new char[1];
*_pstr = '\0';
}
else
{
_pstr = new char[strlen(pstr)+1];
strcpy(_pstr,pstr);
}
}
String(const String &s)
:_pstr(new char[strlen(s._pstr)+1])
{
if(this != &s)
{
_pstr = s._pstr;
}
}
String& operator=(const String &s)
{
if(this != &s)
{
_pstr = s._pstr;
}
return *this;
}
~String()
{
if(NULL != _pstr)
{
delete[] _pstr;
_pstr = '\0';
}
}
private:
char *_pstr;
};
int main()
{
String s1("hello");
String s2("world");
String s3(s1);
return 0;
}
存在问题:
若String类的成员函数存在指针时,在拷贝构造以及赋值运算符重载时 创建的对象和参数对象将会指向同一块目标空间,赋值符号左边的对象与也会指向右边对象的空间。即拷贝构造函数创建的新对象没有分配自己的空间,而赋值运算符重载会造成当前对象的原空间不能被找到,内存空间发生泄露。在程序结束时,两个String类 对象分别调用两次析构函数来释放同一个空间,会导致程序崩溃。
为了解决浅拷贝存在的问题,我们相对应有很多解决办法
一、深拷贝-----普通版
源代码:
class String
{
public:
String(const char *pstr = "")
{
if(NULL == pstr)
{
_pstr = new char[1];
*_pstr = '\0';
}
else
{
_pstr = new char[strlen(pstr)+1];
strcpy(_pstr,pstr);
}
}
String(const String & s)
:_pstr(new char[strlen(s._pstr)+1]) //为String类对象重新生成一段新的空间
{
strcpy(_pstr,s._pstr); //将s1对象的指针所指向的字符串拷贝到新创建的空间中去
}
String& operator=(const String& s)
{
if(this != &s)
{
char *_temp = new char[strlen(s._pstr)+1]; //首先创建一个临时的指针变量,
strcpy(_temp,s._pstr); //将s对象的内容拷贝一份给临时变量
delete[] _pstr; //释放当前对象指针指向的空间
_pstr = _temp; //让当前对象指向临时变量指向的值
_temp = NULL; //将临时指针变量指针置空
}
return *this;
}
~String()
{
if(_pstr != NULL)
{
delete[] _pstr;
_pstr = NULL;
}
}
private:
char *_pstr;
};
int main()
{
String s1("hello");
String s2("world");
String s3(s1);
String s4;
s4 = s2;
return 0;
}
二、深拷贝----简洁版
源代码:
#include<iostream>
using namespace std;
class String
{
public:
String(const char *pstr = "")
{
if(NULL == pstr)
{
_pstr = new char[1];
*_pstr = '\0';
}
else
{
_pstr = new char[strlen(pstr)+1];
strcpy(_pstr,pstr);
}
}
String(const String &s)
:_pstr(NULL) //添加该行语句才可运行成功,因为新创建的对象*this类是乱码,
{ //如果进行交换,将会导致temp._pstr内是乱码,在释放对象temp
String temp(s._pstr); //即调用析构函数时,将会使temp
swap(_pstr,temp._pstr); //中的乱码作为地址释放其内容,导致程序崩溃
}
String& operator=(const String &s)
{
if(this != &s)
{
String str(s);
swap(_pstr,str._pstr);
}
return *this;
}
String& operator=(String s) //函数调用时传值而不是传引用,将会生成一个临时变量,与上方的operator=函数本质相同
{
if(this != &s)
{
swap(_pstr,s._pstr);
}
return *this;
}
~String()
{
if(_pstr != NULL)
{
delete[] _pstr;
_pstr = NULL;
}
}
private:
char *_pstr;
};
int main()
{
String s1("hello");
String s2("world");
String s3(s1);
String s4;
s4 = s2;
return 0;
}
三、“引用计数”解决浅拷贝问题
1.设置Sring类成员int count,------失败
原因:count是一个成员变量,即是一个临时变量,在不同的对象中不能累加
2.设置String类成员为静态变量,static int count;----失败
可以实现累加,但是对于每个对象来说,都有一个静态变量count,不能保证
不同对象共用同一个变量count
3.将引用计数设计成int*,完成String类-------成功
源代码:
class String
{
public:
String(const char* pstr="")
:_pcount(new int(1))
{
if(NULL == pstr)
{
_pstr = new char[1];
*_pstr = '\0';
}
else
{
_pstr = new char[strlen(pstr)+1];
strcpy(_pstr,pstr);
}
}
String(const String& s)
:_pstr(s._pstr)
,_pcount(s._pcount)
{
++(*_pcount);
}
String& operator=(const String& s)
{
if(_pstr != s._pstr)
{
if(--(*_pcount)==0&&_pstr)
{
delete[] _pstr;
delete _pcount;
}
_pstr = s._pstr;
_pcount = s._pcount;
++(*_pcount);
}
return *this;
}
//[]的重载
char& operator[](size_t index)const //const String s1("1111");char c =s1[0];const char c
{
return _pstr[index];
}
const char& operator[](size_t index)const
{
return _pstr[index];
}
~String()
{
if(--(*_pcount) == 0&&_pstr)
{
delete[] _pstr;
delete _pcount;
_pstr = NULL;
_pcount = NULL;
}
}
private:
char *_pstr;
int *_pcount;
};
四、写时拷贝(与深拷贝不同的是,我们采用new()函数的灵感,将计数空间放在我们开辟的字符串空间的上方偏移4字节)
我们应该知道在string类中,要实现写时才拷贝,需要解决两个问题,一个是内存共享, 一个是Copy-On-Wirte
(转http://blog.csdn.net/shinehoo/article/details/5728995)
Copy-On-Write的原理是什么?
有一定经验的程序员一 定知道,Copy-On-Write一定使用了“引用计数”,是的,必然有一个变量类似于RefCnt。当第一个类构造时,string的构造函数会根据 传入的参数从堆上分配内存,当有其它类需要这块内存时,这个计数为自动累加,当有类析构时,这个计数会减一,直到最后一个类析构时,此时的RefCnt为 1或是0,此时,程序才会真正的Free这块从堆上分配的内存。
是的,引用计数就是string类中写时才拷贝的原理!
2.3.1、string类在什么情况下才共享内存的?
使 用别的类的数据时,无非有两种情况,1)以别的类构造自己,2)以别的类赋值。第一种情况时会触发拷贝构造函数,第二种情况会触发赋值操作符。这两种情况 我们都可以在类中实现其对应的方法。对于第一种情况,只需要在string类的拷贝构造函数中做点处理,让其引用计数累加;同样,对于第二种情况,只需要 重载string类的赋值操作符,同样在其中加上一点处理。
2.3.2、string类在什么情况下触发写时才拷贝(Copy-On-Write)
很显然,当然是在共享同一块内存的类发生内容改变时,才会发生Copy-On-Write。比如string类的 []、=、+=、+、操作符赋值,还有一些string类中诸如insert、replace、append等成员函数,包括类的析构时。
修改数据才会触发Copy-On-Write,不修改当然就不会改啦。这就是托延战术的真谛,非到要做的时候才去做。源代码:
class String
{
public:
String(const char *pstr = "")
{
if(NULL == pstr)
{
_pstr = new char[1+4];
*(_pstr+4)= '\0';
}
else
{
_pstr = new char[strlen(pstr)+1];
_pstr+=4;
strcpy(_pstr,pstr);
}
getRef() = 1;
}
String(const String &s)
:_pstr(s._pstr)
{
++getRef();
}
String& operator=(const String &s)
{
if(_pstr != s._pstr)
{
if(0 == --getRef())
{
delete[] (_pstr-4);
}
_pstr = s._pstr;
++getRef();
}
return *this;
}
char& operator[](size_t index) //返回值必须是引用,因为若是返回值,则会产生一个值的临时对象,具有常性
{
return *(_pstr+index); //这两个[]重载函数存在的问题是,若两个String类对象在同 //一空间,则一个改变,另一个也会改变
}
const char& operator[](size_t index)const //第二个const时还是可以对const String s1("1111")进行修改
{ //返回引用则不能对s1的值改动
return *(_pstr+index);
}
//写时拷贝--SWO--若s1、s2处于同一块空间,则改变s1的值必定会改变s2,写时拷贝则解决了这个问题,
//在改变s1的情况下s2的值不变
char& operator[](size_t index)
{
if((getRef()>1))
{
--getRef();
char* pstr = new char[strlen(_pstr)+1+4];
strcpy(pstr,_pstr);
_pstr = pstr;
++getRef();
}
return *(_pstr+index);
}
~String()
{
if(0 == --getRef())
{
delete[] (_pstr-4);
_pstr = _pstr-4;
_pstr = NULL;
}
}
private:
char *_pstr;
int& getRef()
{
return *((int*)_pstr-1);
}
};
int main()
{
String s1("hello");
String s2(s1);
String s3(s1);
s1[0] = '1';
return 0;
}