浅拷贝&深拷贝
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;
};
这里不再对方法二进行测试
使用写时拷贝技术可以减少开辟空间的消耗,提高程序的性能。但同时存在线程安全的问题。