之前的博客已经给出了如何自己定义一个string类,以及其内部应该有的操作,今天就让我们根据STL库中给出的string来看看,它重要的写实拷贝实现和一点点读时拷贝(也是写时拷贝)
1、写时拷贝(copy-on-write)
class String { public: String(const String &str) :_pData(NULL) { String temp(str._pData); swap(_pData,temp._pData); } private: char *_pData; } void test() { String s1("hello world");//构造 String s2(s1);//拷贝构造 }
这里面实现的是用空间换时间的一种方法,定义极其简单,然而大神们写出来的STL库中的string是更为精巧的。就是应用了写实拷贝的技术,防止浅拷贝发生,并且还省了空间,
那么问题来了????
Q:什么是写时拷贝呢?
A:写时拷贝就是一种拖延战术,当你真正用到的时候才去给它开辟空间,不然它只是看起来存在,实际上只是逻辑上的存在,这种方法在STL的string中体现的很明显。
由于string类是用char* 实现的,其内存都是在堆上开辟和释放的。堆上的空间利用要很小心,所以当你定义一个sring类的对象,并且想对这个对象求地址,其返回值是const char*类型,onlyread属性哦,如果还想对该地址的内容做什么改变,只能通过string给的方法去修改。
举个栗子:
#include#include using namespace std; int main() { string s1("再来一遍:hello world"); string s2(s1); //c++方式打印一个字符串的地址!!!!! //static_cast---c++中的强制类型转换,不检查 //string的c_str()方法返回值是const char * cout< (s1.c_str())< (s2.c_str())< _<)~~~~我也这么觉得 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; 那么每个对象的实例化都拥有一个自己的引用计数,无法实现共享
class String { public: String(pData=NULL) :_pData(new char[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; }; string s1="hello world"; string s2(s1); //s1构造,s2拷贝构造:s1和s2指向同一空间,s1和s2的_count都变成2 //当s2先析构,s2的_count--变成1,不释放 //当s1析构时,s1的_count--变成1,不释放 //造成内存泄露
static int _pCount;那么每个对象的实例化都拥有这唯一的一个引用计数,共享范围过大
class String { public: String(pData=NULL) :_pData(new char[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; static int _count; //静态的成员变量要在类外进行初始化 }; int String::_count=0; string s1="hello world"; string s2(s1); string s3("error"); //s1构造,s2拷贝构造:s1和s2指向同一空间,_count都变成2 //s3构造,_count变成1 //当s3先析构,_count--变成0,释放 //s1,s2造成内存泄露
int *_pCount;可以实现引用计数。
class String { public: String(pData=NULL) :_pData(new char[strlen(pData)+1]) ,_pCount(new int(1)) { strcpy(_pData,pData); } ~String() { if(--(*_pCount)==0) { delete []_pData; delete _pCount; } } String& operator=(const String *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++版实现代码
class String { private: char *_pData; //引用计数存在于_pData[-1] public: //构造函数 String(pData=NULL) :_pData(new char[strlen(pData)+1+sizeof(int)]) { //强转在头上4个字节存放引用计数的值 (*(int *)_pData)=1; //恢复其字符串的长度 _pData+=4; strcpy(_pData,pData); } //拷贝构造 String(const String&str) :_pData(str. _pData) { //(*(--(int *)str._pData)) ++; //这个版本是错的,大家看看错在哪里?可以留言告诉我哦 (*(--(int*)_pData-1)++; } //赋值运算符重载 String& operator=(const String &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被这么利用时:
string s1="hello world"; long begin = getcurrenttick(); for(size_t i=0;i