string类的赋值浅拷贝会使多个对象指向同一块空间,当调用析构函数时会使一块空间释放多次,导致程序崩溃。再进一步我们会想到深拷贝,调用拷贝构造或赋值时会拷贝一块新的空间,并将值拷贝下来,这样各自指向自己的数据块,析构时释放各自的数据块。但由于不断的开辟空间、释放空间会花费时间,那怎样去避免这样的问题呢?
我们知道string类的浅拷贝使多个对象指向一块空间 ,为了让析构时这块空间不被释放多次,我们可以在这块空间上加上引用技术(表示此时有几个对象指向这块空间),当我需要写时才去开辟新的空间。这就是
引用技术。
写时拷贝 |
写时拷贝技术可以理解为“写的时候才去分配空间”,这实际上是一种拖延战术。
原理:
写时拷贝技术是通过"引用计数"实现的,在分配空间的时候多分配4个字节,用来记录有多少个指针指向块空间,当有新的指针指向这块空间时,引用计数加一,当要释放这块空间时,引用计数减一(假装释放),直到引用计数减为0时才真的释放掉这块空间。当有的指针要改变这块空间的值时,再为这个指针分配自己的空间(注意这时引用计数的变化,旧的空间的引用计数减一,新分配的空间引用计数加一)。
此时我们知道要使用写时拷贝技术实现String类需要加上引用技术,那我们又如何添加引用技术呢?
如图,三种方案参考,那种合理呢?
分析:
方案1:_refCount存放于String中,那么每个实例中都有一个不能实现共享。
方案2:_refCount为静态成员,所用的String共享,不由类的构造函数初始化,而对象的创建需要调用构造函数,所以它无法计数到正在使用同一块空间的对象的个数。
方案3:_refCount为指针类型,当然可以实现共享,进一步我们可以考虑将_refCount放在_str开始或结束的四个字节中。
String的实现
方法一:
class String { public: //构造函数 String(const char* str = "") //构造函数的初始化列表 :_str(new char[strlen(str)+1]) ,_refcount(new int(1)) { strcpy(_str,str); } //拷贝构造 //s2(s1) String(String& s) { _str = s._str; _refcount = s._refcount ; ++(*_refcount); } //s1 = s2 String& operator=(const String& s) { if(_str != s._str) { Release(); //将s1的指向释放, _str = s._str; _refcount = s._refcount ; ++(*_refcount); } return *this; } void CopyOnWrite() { if(*_refcount > 1) { char* tmp = new char[strlen(_str)+1]; strcpy(tmp,_str); --(*_refcount); _str = tmp; _refcount = new int(1); } } char& operator[](size_t index) { CopyOnWrite(); return _str[index]; } ~String() { Release(); } void Release() { if((--_refcount)==0) { delete [] _str; } } char* C_Str() { return _str; } private: char* _str; int* _refcount;//引用的次数(引用技术) 创一块空间配一个引用技术 };
由结果可以看出,s1,s2,s3指向同一块空间。
测试2:
当对s3进行写时,s3的地址改变(对其进行拷贝)
方法二:
class String { public: String(const char* str = "") :_str(new char[strlen(str)+5]) { *((int*)_str) = 1; _str+=4; strcpy(_str,str); } String(String& s) :_str(s._str ) { GetRefCount ()++; } ~String() { Release (); } String& operator= (const String& s) { if(_str != s._str ) { Release(); _str = s._str ; ++GetRefCount (); } return *this; } void CopyOnWrite() { if(GetRefCount()>1 ) { char* tmp = new char[strlen(_str)+1+5]; strcpy(tmp+4,_str); --GetRefCount (); _str = tmp; _str+=4; GetRefCount () = 1; } } char& operator[](size_t index) { CopyOnWrite(); return _str[index]; } int& GetRefCount() { return *((int*)(_str-4)); } void Release() { if(--GetRefCount () == 0) { delete[] (_str-4); } } char* C_Str() { return _str; } private: char* _str; };
这里不再对方法二进行测试
使用写时拷贝技术可以减少开辟空间的消耗,提高程序的性能。但同时存在线程安全的问题。