您的位置:首页 > 编程语言 > C语言/C++

【C++】c++写时拷贝Copy On Write

2015-10-03 23:09 453 查看

Copy On Write

Copy On Write(写时复制)使用了“引用计数”(reference counting),会有一个变量用于保存引用的数量。当第一个类构造时,string的构造函数会根据传入的参数从堆上分配内存,当有其它类需要这块内存时,这个计数为自动累加,当有类析构时,这个计数会减一,直到最后一个类析构时,此时的引用计数为1或是0。此时,程序才会真正的Free这块从堆上分配的内存。

写时复制(Copy-On-Write)技术,就是编程界“懒惰行为”——拖延战术的产物。举个例子,比如我们有个程序要写文件,不断地根据网络传来的数据写,如果每一次fwrite或是fprintf都要进行一个磁盘的I/O操 作的话,都简直就是性能上巨大的损失,因此通常的做法是,每次写文件操作都写在特定大小的一块内存中(磁盘缓存),只有当我们关闭文件时,才写到磁盘上 (这就是为什么如果文件不关闭,所写的东西会丢失的原因)。

class String
{
public:
String(char* ptr = "")           //构造函数
:_ptr(new char[strlen(ptr)+1])
{
strcpy(_ptr, ptr);
}
String(const String& s)
:_ptr(new char[strlen(s._ptr)+1])//另外开辟空间
{
strcpy(_ptr, s._ptr);
}
~String()
{
if (_ptr)
{
delete[] _ptr;
}
}
private:
char* _ptr;
};


void Test()
{
String s1 = "hello world";
int begin = GetTickCount();//记录此时毫秒数
for (int i = 0; i < 10000; ++i)
{
String s2 = s1;
}
int end = GetTickCount();//记录此时毫秒数
cout << "cost time:" << end - begin << endl;
}


GetTickCount : 在Release版本中,该函数从0开始计时,返回自设备启动后的毫秒数(不含系统暂停时间)。在头文件windows.h中。

在上面for循环中,语句“String s2 = s1;”不断调用拷贝构造函数为s2开辟空间,执行完语句“String s2 = s1;”后,不断调用析构函数对s2进行释放,导致低效率,Test执行结果如下图:



写时拷贝~~写时拷贝~自然是我们自己想写的时候再进行拷贝(复制),下面引入几种方案如下:(试着判断哪一种方案可行)



这里又引入另外一个概念“引用计数”:string的构造函数会根据传入的参数从堆上分配内存,当有其它类需要这块内存时(即其它对象也指向这块内存),这个计数为自动累加,上面方案中的_retCount就是用来计数的。

简单地介绍一下上面三个方案。方案一和方案二是不可行的,方案一中的_retCount是属于每个对象内部的成员,当有多个对象同时指向同一块空间时,_retCount无法记录多个对象方案二中的_retCount是静态成员变量,是所有对象所共有,似乎可以记录,举个例子:对象s1、s2指向A空间,_retCount为2,对象s3、s4指向B空间,此时_retCount变为4,但是当想释放B空间时,应当在析构函数中_retCount减到0时释放,但是当_retCount减到0时,却发现释放的是A空间,而B空间发生了内存泄露。也就是静态成员变量_retCount只能记录一块空间的对象个数。

- 下面通过代码介绍方案三:

class String
{
public:
String(char* ptr = "")        //构造函数
:_ptr(new char[strlen(ptr)+1])
, _retCount(new int(1))//每个对象对应一个整型空间存放
{                          //指向这块空间的对象个数
strcpy(_ptr, ptr);
}
String(const String& s)       //拷贝构造函数
:_ptr(s._ptr)
, _retCount(s._retCount)
{
_retCount[0]++;
}
String& operator= (const String& s)   //赋值运算符重载
{
if (this != &s)
{
if (--_retCount[0] == 0)
{//旧的引用计数减1,如果是最后一个引用对象,则释放对象
delete[] _ptr;
delete[] _retCount;
}
_ptr = s._ptr;//改变this的指向,并增加引用计数
_retCount = s._retCount;
++_retCount[0];
}
return *this;
}
~String()
{
if (--_retCount[0] == 0)
{
delete[] _ptr;
delete[] _retCount;
}
}
private:
char* _ptr;
int* _retCount;
};


同样执行Test函数,测试结果如下图:



下面进一步优化方案三来介绍写时拷贝(写时复制)

方案三:是每个对象对应一个整型空间(即_refCount)存放指向这块空间的对象个数

再优化:不引用_refCount,但每次给_ptr开辟空间的时候,多开辟四个字节,用来记录指向此空间的对象个数,规定用开头那四个字节来计数。

class String
{
public:
String(char* ptr = "")
:_ptr(new char[strlen(ptr)+5])
{
_ptr += 4;
strcpy(_ptr,ptr);
_GetRefCount(_ptr) = 1;//每构造一个对象,头四个字节存放计数
}
String(const String& s)
:_ptr(s._ptr)
{
_GetRefCount(_ptr)++;  //每增加一个对象,引用计数加1
}
String& operator= (const String& s)
{
if (this != &s)
{
Release(_ptr);
_ptr = s._ptr;
_GetRefCount(_ptr)++;
}
return *this;
}
char& operator [](size_t index)
{
if (_GetRefCount(_ptr) > 1)
{
--_GetRefCount(_ptr);//旧引用计数减1
char* str = new char[strlen(_ptr) + 1];//另外开辟一个空间
str += 4;
strcpy(str, _ptr);
_GetRefCount(str) = 1;
_ptr = str;
}
}
~String()
{
Release(_ptr);
}
inline void Release(char* ptr)
{
if (--_GetRefCount(ptr) == 0)
{
delete[](ptr - 4);
}
}
inline int& _GetRefCount(char* ptr)
{
return *(int*)(ptr - 4);//访问头四个字节
}
private:
char* _ptr;
};


程序执行过程,看下图说话





对下列函数进行解析:

char& operator [](size_t index)
{
if (_GetRefCount(_ptr) > 1)
{
--_GetRefCount(_ptr);//旧引用计数减1
char* str = new char[strlen(_ptr) + 1];//另外开辟一个空间
str += 4;
strcpy(str, _ptr);
_GetRefCount(str) = 1;
_ptr = str;
}
}


当在主函数中执行语句:s1[0] = ‘w’;时,想要改变s1对象中_ptr[0]的值;但是当我们改变s1中_ptr[0]的值时,不希望把s2、s3中_ptr[0]的值也改变了。由于s1、s2、s3目前指向同一块空间,改变其中一个,另外两个肯定也跟着改变了,所以提供了另外一种方法:把对象s1分离出来,旧引用计数减1,另外给s1开辟一段跟原来一样的空间,存放一样的内容,这时候即使改变了s1的内容,也不影响s2、s3的对容。

一样看下图说话:

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: