之前的博客已经给出了如何自己定义一个string类,以及其内部应该有的操作,今天就让我们根据STL库中给出的string来看看,它重要的写实拷贝实现和一点点读时拷贝(也是写时拷贝)


1、写时拷贝(copy-on-write)

classString{public:String(constString&str):_pData(NULL){Stringtemp(str._pData);swap(_pData,temp._pData);}private:char*_pData;}voidtest(){Strings1("helloworld");//构造Strings2(s1);//拷贝构造}

这里面实现的是用空间换时间的一种方法,定义极其简单,然而大神们写出来的STL库中的string是更为精巧的。就是应用了写实拷贝的技术,防止浅拷贝发生,并且还省了空间,

那么问题来了????

Q:什么是写时拷贝呢?

A:写时拷贝就是一种拖延战术,当你真正用到的时候才去给它开辟空间,不然它只是看起来存在,实际上只是逻辑上的存在,这种方法在STL的string中体现的很明显。

由于string类是用char* 实现的,其内存都是在堆上开辟和释放的。堆上的空间利用要很小心,所以当你定义一个sring类的对象,并且想对这个对象求地址,其返回值是const char*类型,onlyread属性哦,如果还想对该地址的内容做什么改变,只能通过string给的方法去修改。

举个栗子:

#include<iostream>#include<string>usingnamespacestd;intmain(){strings1("再来一遍:helloworld");strings2(s1);//c++方式打印一个字符串的地址!!!!!//static_cast---c++中的强制类型转换,不检查//string的c_str()方法返回值是constchar*cout<<static_cast<constvoid*>(s1.c_str())<<endl;cout<<static_cast<constvoid*>(s2.c_str())<<endl;//就让我们再来复习一下c语言是如何打印一个字符串的地址//是不是看起来超简单,~~~~(>_<)~~~~我也这么觉得printf("%x\n",s1.c_str());printf("%x\n",s2.c_str());}

(vs2010版本)

结果是不是和你想的不一样。。。。(明明应该不变的说~)

vc 6.0版本下:s1,s2的地址是一样的。这里就不进行截屏了,如果有兴趣的同学,下去可以试试哈~

那么当对s1,s2进行修改时是怎么样的呢

s1[0]='h';s2[0]='w';

(vs2010版本)

VC6.0版本下:s1,s2的地址不一样(同vs2010版本)

所以我们得出的结论是:

当对string对象只进行拷贝构造时,发生的是写时拷贝(假拷贝),只有对其对象进行修改时(有写的操作),才对其对象另外开辟空间,进行修改。

要想达到这样的效果,在一定程度上节省了空间。

必须做到两点:内存的共享,写时拷贝。


(1)copy-on-write的原理?

“引用计数”,程序猿就是这般机智~~~~

当对象s1实例化,调用构造,引用计数初始化=1;

当有对象对s1进行拷贝时,s1的引用计数+1;

当有对象是由s1拷贝来的或者是s1自身进行析构是,s1的引用计数进行-1;

当有对象是由s1拷贝来的或者是s1自身需要修改时,进行真拷贝,并且引用计数-1;

当引用计数==0的时候,进行真正的析构。


(2)引用计数应该如何设计在?

关于引用计数的实现,你是不是也有这样的疑惑呢?

当类的对象之间进行共享时,引用计数也是共享的

当类中的对象从公共中脱离出来,引用计数就是它自己的了。

那么如何做到从独立--->共享--->独立的呢???

如果你想将引用计数当做String类的成员变量,那么什么样的类型适合它呢?

int _count; 那么每个对象的实例化都拥有一个自己的引用计数,无法实现共享

classString{public:String(pData=NULL):_pData(newchar[strlen(pData)+1]),_count(1){strcpy(_pData,pData);}~String(){if(--_count==0){delete[]_pData;}}String(String&str):_pData(str._pData){str._count++;_count=str._count;}private:char*_pData;int_count;};strings1="helloworld";strings2(s1);//s1构造,s2拷贝构造:s1和s2指向同一空间,s1和s2的_count都变成2//当s2先析构,s2的_count--变成1,不释放//当s1析构时,s1的_count--变成1,不释放//造成内存泄露

static int _pCount;那么每个对象的实例化都拥有这唯一的一个引用计数,共享范围过大

classString{public:String(pData=NULL):_pData(newchar[strlen(pData)+1]){_count=1;strcpy(_pData,pData);}~String(){if(--_count==0){delete[]_pData;}}String(String&str)//不加const,不然底下的浅拷贝会出错:_pData(str._pData){str._count++;}private:char*_pData;staticint_count;//静态的成员变量要在类外进行初始化};intString::_count=0;strings1="helloworld";strings2(s1);strings3("error");//s1构造,s2拷贝构造:s1和s2指向同一空间,_count都变成2//s3构造,_count变成1//当s3先析构,_count--变成0,释放//s1,s2造成内存泄露

int *_pCount;可以实现引用计数。

classString{public:String(pData=NULL):_pData(newchar[strlen(pData)+1]),_pCount(newint(1)){strcpy(_pData,pData);}~String(){if(--(*_pCount)==0){delete[]_pData;delete_pCount;}}String&operator=(constString*str){if(_pData!=str._pData){if(--(*_pCount)==0){delete_pCount;delete[]_pData;}(*str._pCount)++;_pCount=str._pCount;_pData=str._pData;}return*this;}String(String&str)//不加const,不然底下的浅拷贝会出错:_pData(str._pData),_pCount(str._pCount){(*str._pCount)++;}private:char*_pData;int*_pCount;};

这些字符串都是在堆上开辟的,那么引用计数也可以在堆上开辟,要从逻辑上,看引用计数是个指针,存次数,从物理上看,引用计数应该和字符指针放在一起,便于管理。让数据相同的对象都可以共享同一片内存。

综上,引用计数的设计如图:

(3)引用计数什么时候需要共享呢?

情况1:string s2(s1); //s2拷贝自s1,即s2中的数据和s1的一样

情况2:string s2; s2=s1;//s2的数据由s1赋值而来,即s2中的数据和s1的一样

综上所述:

string类中的拷贝构造和赋值运算符重载需要引用计数


(4)什么情况下需要进行写时拷贝

对内容有修改时


(5)c++版实现代码

classString{private:char*_pData;//引用计数存在于_pData[-1]public://构造函数String(pData=NULL):_pData(newchar[strlen(pData)+1+sizeof(int)]){//强转在头上4个字节存放引用计数的值(*(int*)_pData)=1;//恢复其字符串的长度_pData+=4;strcpy(_pData,pData);}//拷贝构造String(constString&str):_pData(str._pData){//(*(--(int*)str._pData))++;//这个版本是错的,大家看看错在哪里?可以留言告诉我哦(*(--(int*)_pData-1)++;}//赋值运算符重载String&operator=(constString&str){if(_pData!=str._pData){if(--(*(--(int*)_pData-1)==0){_pData-=4;delete[]_pData;}else{_pData=str._pData;(*(--(int*)_pData-1)++;}}return*this;}//析构函数~String(){if(--(*(--(int*)_pData-1)==0){_pData-=4;delete[]_pData;}}};

2、读时拷贝(copy-on-read)

当C++的STL库中的string被这么利用时:

strings1="helloworld";longbegin=getcurrenttick();for(size_ti=0;i<s1.size();i++){cout<<s1[i]<<endl;}cout<<getcurrenttick()-begin<<endl;//你会发现这样的时间和修改内容进行的写时拷贝的时间一样长//这是因为string中对于operator[]无法自主进行判断//client是进行读还是写,所以一律按写考虑