string类的简单实现(写时拷贝Copy-on-write)
来源:互联网 发布:南昌广州seo外包 编辑:程序博客网 时间:2024/06/05 21:09
前言:上一篇文章实现了string的深拷贝写法;那我们能不能用浅拷贝写string类呢?当然可以;
一、
(1)
当我们需要对拷贝之后的对象进行修改时,采用深拷贝的方式; 如果不需要修改,只是输出字符串的内容时,或者是当偶尔修改的的时候,我们再采用深拷贝的方法;对于这两种情况,我们在实现拷贝构造函数时采用浅拷贝的方式。在采用浅拷贝的方式以后,那么两个对象的成员变量指针指向同一块字符串空间;根据上一篇的文章,我们知道这样的话,析构时必然会发生错误;那么怎么避免析构时出错;请看下文;
(2)
写之前,还是先说一下,为什么要实现string类的写时拷贝
原理:浅拷贝+引用计数;
因为在现实生活中,我们有很多情况下用一个对象来拷贝构造另一个对象的之后,我们只是把拷贝构造来的对象用一下而已,不会对他的字符串进行修改操作;那么这样的话,我们其实不用深拷贝那么麻烦。按照上一篇的深拷贝,如果我们要用一万个对象,那我们开辟一万个对象空间,但是得到之后只是用对象里面保存的字符串,并没有其他的操作;这样的话会很浪费空间;而且还要拷贝字符串,程序效率也会降低,所以我们想到了一种实现方式;就是说
,只有当我们要对拷贝来的字符串进行修改时,才采用深拷贝的方式,其余情况都采用浅拷贝;这种只有写的时候才进行深拷贝的方式叫做写时拷贝;
(3)实现写时拷贝的时候,因为采用了浅拷贝的方式,所以,为了避免析构的时候出现错误,上篇文章的析构函数就用不了;要在其基础上修改;我们可以在成员变量中添加一个变量count,用来记录拷贝的次数;每拷贝一次,我们就count++;然后在析构函数内部也加上if的判断语句;当我们每次析构一个对象的时候,就–count;当count等于0的时候,再进入if语句内部;释放空间;将变量置空;
再此,我给出了三种方案,其中有二个是错的;但都是经典的错误写法。所以把三种方法都说一下
二、两种经典的错误实现方法:
(1)方案一:错误写法
方案一:(错误)#include<iostream>using namespace std;class String{public: String(const char* str="")//构造函数 :_str(new char[strlen(str)+1]) ,_count(1) { strcpy(_str,str); } String( String& s)//拷贝构造---浅拷贝 :_str(s._str) { _count=++(s._count); } String& operator=(String s)//赋值操作符重载---现代写法 { std::swap(_str,s._str); return *this; } ~String()//析构函数 { if (--_count==0) { delete[] _str; _str=NULL; } }private: char* _str; int _count;};int main(){ String s1("abcdef"); String s2(s1); return 0;}
分析:
分析:首先这种方案是不对的,为什么呢?我们确实是添加了一个计数器_count;在构造函数里面;每次构造一个对象;_count赋值为1;在析构函数内部也加入了if(–_count=0)的判断机制,避免一块空间被多次释放;但是真的达到这个效果了吗?其实没有,因为在拷贝构造函数当中,我们每次++被拷贝的_count然后赋给要接受拷贝的对象的_count;当程序走完,此时两个_count都变为2;
当析构各自的对象时,这时,这两个对象的_conut各自都为2;当调用析构函数时,–_count以后,都不能够释放空间;这里的意思就是,当S2调用析构函数后,他没有释放空间,自己的_count变为了1;
但是接下来,当s1掉用析构函数时,它的_count不是1,仍然是2,–_count以后仍然不能够进入if语句释放空间;
所以,这种方法不对;那我们能不能定义一个_count使其能够被所有对象共用;当一个对象调用析构函数时,–_count;等到下一次另一个对象调用时,在上一次减了基础上再减呢;当然可以;我们把_count定义为局部静态变量;
(2)方案二:错误写法
在类中数据成员的声明前加上static,该成员是类的静态数据成员,必须在类外进行定义;
★★类中静态数据成员和非静态数据成员的区别:★★
①对于非静态数据成员,每个类对象都有自己的拷贝.
②而静态数据成员被当做是类的成员,无论这个类被定义了多少个,静态数据成员都只有一份拷贝, 为该类型的所有对象所共享(包括其派生类).所以,静态数据成员的值对每个对象都是一样的,它的值可以更新. 静态数据成员不属于某个类对象所私有,所以不能再在初始化列表中初始化
#include<iostream>using namespace std;#include<cassert>class String{public: String(const char* str="")//构造函数 :_str(new char[strlen(str)+1]) { _count=1; strcpy(_str,str); } String(const String& s)//拷贝构造---浅拷贝 :_str(s._str) { ++_count; } String& operator=(String s)//赋值操作符重载---现代写法 { std::swap(_str,s._str); return *this; } ~String()//析构函数 { if (--_count==0) { delete[] _str; _str=NULL; } }private: char* _str; static int _count;};int String:: _count=1;int main(){ String s1("abcdef"); String s2(s1); String s3("HELLO"); return 0;}
分析: 这种写法表面上看上去好像是对的;其实也存在问题;如过只看我上面的两行测试代码;好像没有错误;当s1创建时,静态成员变量_count变为了1;然后构造S2之后,_count++;变为2;这时这个_count是两个对象的共用成员变量,
然后s2先调用析构函数,–_count变为1,
然后s1再调用类的析构函数,此时的_count为1,–_count以后,_count 变为0;这时,才将s1和s2共同指向的空间释放掉,避免一块空间,被多次释放;
but,当我们在这后面,再创建一个对象s3时,这就不对了,创建s3的时候,调用构造函数,将静态变量_count变为1;
此时类的共用成员变量_count变为了1;当从后往前析构时;
S3调用析构函数_conut变为0;s3的空间成功释放;
但是当s2调用析构函数时,此时计数器_count=0;–count之后变为-1,不会进入if判断语句内部释放空间;
S1调用析构函数时,_count变为-2;更不会析构s1和S2共同指向的空间。所以错误
三、正确的写时拷贝写法
(1)方案三:正确写法
#include<iostream>using namespace std;#include<cstring>class String{public: String(const char* str="")//构造 :_str(new char[strlen(str)+1]) ,_PCount(new int(1)) { strcpy(_str,str); } String(const String& s)//拷贝构造----浅拷贝 :_str(s._str) ,_PCount(s._PCount) { ++_PCount[0]; } String& operator=(const String& s)//赋值运算符重载 { if (this!=&s) { delete[] _str; delete _PCount; _str=s._str; _PCount=s._PCount; ++_PCount[0]; } return *this; } ~String()//析构 { if (--_PCount[0]==0) { delete[] _str; delete _PCount; _str=NULL; _PCount=NULL; } }private: char* _str; int* _PCount;};void Test(){ String s1("abcdef"); String s2(s1); String s3("ABCEDF"); String s4; s4=s1;}int main(){ Test(); return 0;}
分析:
正确写法(每段字符串内存加上属于自己的计数器指针,这样的话,只有当拷贝构造的时候,两个对象的计数器指针才会指向同一个计数器内存,让其++,如果构建新的对象,那么他的计数器为构造函数1,析构时,是自己的1变为0,不会影响拷贝构造的两个对象的计数器变化,这样就能到达:没有拷贝构造的对象,一次把自己的空间释放掉,而拷贝构造的对象,所有对象的计数器都指向同一个计数器空间,释放该计数器的字符串内存;也不会出错)
图示说明:
①
String s1("abcdef"); String s2(s1); String s3("ABCEDF");
②赋值运算符重载详解
String& operator=(const String& s)//赋值运算符重载 { if (this!=&s) { delete[] _str; delete _PCount; _str=s._str; _PCount=s._PCount; ++_PCount[0]; } return *this; }
测试
String s1("abcdef"); String s2(s1); String s3; s3=s1;
(2)方案三写时拷贝的改进
前面的方案三,我们是用两个指针,分别管理内存和计数器,这样每次构造函数的时候,要分别开辟空间给各自的指针;这样做降低效率;我们可以只开辟一次空间;里面既存计数器又存字符串;在开辟的空间的前四个字节存储计数器,后面存储字符串;
#include<iostream>using namespace std;#include<cstring>#include<cassert>class String{public: String(const char* str="")//构造 :_str(new char[strlen(str)+1+4]) { cout<<"构造"<<endl; _str+=4;//走到数据区首地址 strcpy(_str,str); GetCount()=1;//计数器区域赋值为1 } String(const String& s)//拷贝构造 :_str(s._str) { cout<<"拷贝构造"<<endl; ++GetCount();//将所指向的共同内存的计数器加+ } String& operator=(String& s)//赋值运算符重载---赋值的对象计数器要++;被赋值的对象计数器要++ { if (this!=&s) { cout<<"构赋值运算符重载"<<endl; /*判断被赋值的对象的计数器--,是否为0;如果为0,就要释放他的(计数区+数据区) 如果没有到达0;则只是给计数器减1;*/ if (--GetCount()==0) { delete[] (_str-4);//回到内存的首位置 } ++(s.GetCount()); _str=s._str; } return *this; } ~String()//析构 { cout<<"析构"<<endl; if (--GetCount()==0) { cout<<"释放"<<endl; delete[] (_str-4); _str=NULL; } } char& operator[](size_t index)// 写时拷贝 { cout<<"重载[]"<<endl; assert(index>=0 && index<(int)strlen(_str)); if (GetCount()=1)//如果该对象的计数器为1 { return _str[index];//直接返回index位置的值,进行修改 } 如果不为1 --GetCount();//先给对象的计数器-- char* tmp=new char[strlen(_str)+1+4];//然后开辟一个同样大小的空间;用指针指着 strcpy(tmp+4,_str);//给这块空间字符串区域拷贝原空间的字符串 _str=tmp+4;//将指针指向新开辟字符串的首位置 GetCount()=1;//将指针的计算器置为1 return _str[index];//返回index位置的值 }private: int& GetCount()//取计数器的值 { return *((int*)_str-1); }private: char* _str;};void Test(){ String s1("abcdef"); String s2(s1); String s3(s2); s3[3]='q';}int main(){ Test(); return 0;}
部分函数过程图示:
①拷贝构造函数
②赋值运算符重载
四、写时拷贝比普通拷贝的效率高
#include<iostream>using namespace std;#include<cstring>#include<cassert>#include<Windows.h>namespace WriteCopy{class String{public: String(const char* str="")//构造 :_str(new char[strlen(str)+1+4]) { _str+=4; strcpy(_str,str); GetCount()=1; } String(const String& s) :_str(s._str) { ++GetCount(); } ~String()//析构 { if (--GetCount()==0) { delete[] (_str-4); _str=NULL; } }private: int& GetCount()//取计数器的值 { return *((int*)_str-1); }private: char* _str;};}namespace MyString{ class String { public: String(const char* str="") :_str(new char[strlen(str)+1]) { strcpy(_str,str); } String(const String& s) :_str(new char[strlen(s._str)+1]) { strcpy(_str,s._str); } ~String() { if (NULL!=_str) { delete[] _str; _str=NULL; } } private: char* _str; };}void Test(){ int start=GetTickCount(); WriteCopy::String s1("abcdef"); for (int i=0;i<10000000;i++) { WriteCopy::String s2(s1); } int end=GetTickCount(); cout<<"写时拷贝"<<end-start<<endl; start=GetTickCount(); MyString::String s3("abcdef"); for (int i=0;i<10000000;i++) { MyString::String s4(s3); } end=GetTickCount(); cout<<"普通拷贝"<<end-start<<endl;}int main(){ Test(); return 0;}
end!!!
接下来模拟实现c++库中的string类;
- string类的简单实现(写时拷贝Copy-on-write)
- C++ String写时拷贝(Copy On Write)
- STL的写时拷贝(Copy-On-Write)
- 标准C++类std::string的内存共享和Copy-On-Write(写时拷贝)
- 写时拷贝Copy-On-Write技术
- 写时拷贝技术(copy-on-write)
- 写时拷贝技术:Copy-On-Write
- 写时拷贝(copy on write)
- (转)Linux写时拷贝技术(copy-on-write)
- Linux写时拷贝技术(copy-on-write)
- Linux写时拷贝技术(copy-on-write)
- Linux写时拷贝技术(copy-on-write)
- Linux写时拷贝技术(copy-on-write)
- Linux写时拷贝技术(copy-on-write)--COW
- Linux写时拷贝技术(copy-on-write)
- Linux写时拷贝技术(copy-on-write)
- Linux写时拷贝技术(copy-on-write)
- Linux写时拷贝技术(copy-on-write)
- RapidMiner 数值调整
- Error:cannot open source file "itkImageToVTKImageFilter.h"
- java选择排序实现
- ubuntu16 备份 TimeShift
- 子树
- string类的简单实现(写时拷贝Copy-on-write)
- 存储过程
- Integer缓存原理与JVM调参应用
- lcm/gcd
- 记事本
- 1005. Spell It Right (20)
- 六、Git工作区和版本库的关系
- 对于FacebookF8开发者大会开源深度学习框架Caffe2以及百度开放自动驾驶平台API的看法
- 如何向mysql数据库中导入或导出txt文件