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

来源:互联网 发布:javascript异步编程 编辑:程序博客网 时间:2024/06/09 19:14

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的对容。


一样看下图说话:


这里写图片描述

1 0
原创粉丝点击