引用计数写时拷贝

来源:互联网 发布:安卓版ps软件中文版 编辑:程序博客网 时间:2024/05/19 23:00

浅拷贝是值拷贝,由之前深拷贝分析的文章知,浅拷贝后,两个指针指向同一空间。容易出现多次释放同一空间的问题,以及修改其中一个指针指向的内容影响其他指针指向的内容的问题。而深拷贝效率低,每次对对象进行值和空间同时拷贝,但这样会存开辟很多空间。为了避免产生更多的空间,引入写时拷贝,当对空间进行更改时,检查是否有除了自己以外的对象使用该块空间,若有,则自己重新开辟空间进行改正,不影响其他对象;若没有其他对象使用此空间,则说明只有自己使用此空间,直接进行更改。此时引用计数用来统计有多少对象使用此空间,克服了浅拷贝和深拷贝存在的问题。

void TestString(){    String s1;    for(size_t i=0;i<1000000;i++)    {       String s2(s1);//此处的拷贝构造为深拷贝时,程序会崩溃    }}

一、分析写时拷贝。
1、引用计数的工作方式
1)除了初始化对象外,每个构造函数(拷贝构造函数除外)要建立一个引用计数,用来记录多少对象与正在创建的对象为共享一块内存空间。当我们创建一个对象时,只有一个对象用该空间,故将引用计数初始化为1。
2)拷贝构造函数不分配新的计数器,而是拷贝数据成员,包括计数器。拷贝构造递增计数器,指出给定的对象的内存空间被一个新的用户共用。
3)析构函数递减计数器,指出共用空间的用户少了一个,如果计数器为0,则释放该空间。
4)赋值运算符重载,递增右侧运算对象的计数器,递减左侧运算对象的计数器。如果左侧运算对象的计数器为0,就要对该对象进行销毁。
2、写时拷贝
写时拷贝在写的时候(即改变对象内容时)才会真正的开辟空间拷贝(深拷贝),如果只是对数据进行读,只会对数据进行浅拷贝。
写时拷贝:引用计数器的浅拷贝,又称延时拷贝,写时拷贝技术是通过“引用计数”实现的,在分配空间的时候多分配4或8个字节,用来记录有多少指针指向该空间,当有新指针指向这块空间时,引用计数加1。当要释放该块空间时,引用计数减1,直到引用计数减为0才释放掉该块空间。当有的指针要改变这块空间的值时,再为这个指针分配自己的空间(此时,引用计数的变化,旧空间的引用计数减1,新分配的空间的引用计数加1)。

二、完成写时拷贝的各种方案。
1、分析以下String的copy on write的几种实现方案哪种可以?为什么?
这里写图片描述

//方案1class string{private:    char* _str;    int _refcount;}//方案1实现class String{  public:       String(char* str=" ")          :_refCount(0)       {          _str=new char[strlen(str)+1];          strcpy(_str,str);          _refCount++;       }       String(String &s)           :_refCount(s._refCount)       {           _str=s._str;           _refCount++;           s._refCount=_refCount;           //这里虽然可以让两个对象的_refCount相等,但           //如果超出两个对象的_str指针都指向同一块内存时,           //就无法让所有对象的_refCount都保持一致       }       ~String()       {         if(--_refCount==0)         {            delete[] _str;            _str=NULL;         }       }    private:        char* _str;        int _refCount;    };

解析:该方法中,将用作计数的整形变量_refcount定义为类的私有成员变量,任何一个对象都有它自己的成员变量 _refcount,它们互不影响,难以维护。只要拷贝出了对象, _refcount大于了0,每个对象在调用自己的析构函数时 _refcount不等于0,那么它们指向的那块内存都将得不到释放,无法达到想要的效果。
这里写图片描述

//方案2class String{public:       String(char* str=" ")     {          _str=new char[strlen(str)+1];          strcpy(_str,str);          Count++;      }       String(String &s)      {           _str=s._str;           Count++;       }       ~String()       {         if(--Count==0)         {            delete[] _str;            _str=NULL;         }       }private:    char* _str;    static int count;};int String::count = 0;      //初始化count

解析:该方案设置了一个静态整形变量来计算指向一块内存的指针的数量,每析构一次count减1,直到它等于0(也就是没有指针再指向它的时候)再去释放这块内存空间。但是该方案只适用于只调用一次构造函数,只有一块内存的情形,如果多次调用构造函数构造对象,新构造的对象照样会改变count的值,那么最后只释放了第一块内存,后来的内存无法释放会造成内存泄漏。
这里写图片描述

//方案3class String{  private:         char*  _str;         int*  _refCount;};

问题的关键是要为每一块内存设置一个引用计数,而不是为每一个对象建立一个引用计数,当指向这块内存的指针数为0时,再去释放它。
本方案设置了一个int型的指针变量用来引用计数,每份内存空间对应一个引用计数,而不是每个对象对应一个引用计数,而且不同内存之间的引用计数互不影响,不会出现方案一和方案二出现的问题。
这里写图片描述

2、 string中的两种写时拷贝
1)方法一:
这里写图片描述
当创建一个对象S1,引用计数为1,再拷贝构造一个对象S2时,引用计数自动加1,此时为2。如果S2要对自身进行修改,检查引用计数_refCount是否大于1,如果是大于1,则S2重新开偏僻一块空间,修改S1原来的引用计数 _refCount,使其减1。而重新开辟的空间中此时只有S2一个对象,所以引用计数 _refCount为1。
这里写图片描述
总结:此方案的写时拷贝计数是同时开辟两块空间,一个存放自身容量,一个存放引用计数 _refCount,同时管理两块空间,统计当前使用此空间的对象数,当要修改当前空间的时候,进行引用计数判断,决定是否要开辟空间。

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);    }        void Release()    {        if(--(*_refCount)==0)        {             delete[] _str;             delete _refCount;        }    }    //S4=S1    String& operator= (const String& s)   {       if(_str!=s._str)       {          Release();          _refCount=s._refCount;          _str=s._str;          (*_refCount)++;       }       return *this;   }   //copy on write   void CopyOnWrite()   {       if(*_refCount>1)       {          char* tmp=new char[strlen(_str)+1];          strcpy(tmp,_str);          --(*_refCount);          _str=tmp;          _refCount=new int(1);        }    }    //如果指向这块空间的指针只有这一个,直接进行修改;    //如果指向这块空间的指针数大于1,则调用写时拷贝进行开空间拷贝,    //1)减引用计数2)拷贝 3)创建新的引用计数    char* operator [] (size_t index)    {        if(*_refCount>1)       {           CopyOnWrite();       }        return _str[index];    }   ~String()   {       Release();   }   char* C_str()   {        return *_str;   }private:    char* _str;    int* _refCount;};       

2)方法二:
这里写图片描述

//方法二class String{   private:         char* _str;};

上一种方法是开辟了两块空间进行管理,本方法采用开辟一块空间进行写时拷贝。把用来计数的整形指针变量放在所开辟的内存空间的头4或8个字节。用 * ((int * ) _str)就能取得计数值。真正存放内容的空间从第5或9个字节开始。一次性多开辟4或8个字节进行写时拷贝。
这里写图片描述
当进行操作时,先检查引用计数个数,然后进行判断是否开辟空间,同时修改引用计数的值,放置空间不能释放。
当创建了3个对象,S1,S2,S3同时指向一个空间,此时引用计数为3,再创建一个对象S4,S4 的引用计数为1。
这里写图片描述
再进行S3=S4;此时对应的引用计数和对象的指向都需要改动,如下图:
这里写图片描述
对象S3指向了S4,同时S3原来的空间的引用计数进行减1,新指向空间的引用计数进行加1。
总结:方案2的写时拷贝计数使用一块空间进行内容和引用计数的管理和操作,不用开辟两块空间,方便管理。

class String{public:    String(const char* str=" ")          :_str(new char[strlen(str)+5])    {          *((int*)_str)=1;          _str+=4;          strcpy(_str,str);    }       int& GetRefCount()    {       return  *((int*)(_str-4));    }           //S2(S1)    String(String &s)          :_str(s._str)    {       GetRefCount()++;    }        void Release()    {        if(--GetRefCount()==0)        {             delete[] (_str-4);        }    }    //S4=S1 String& operator= (const String& s)   {       if(_str!=s._str)       {          Release();          _str=s._str;          GetRefCount()++;       }       return *this;   }   //copy on write   void CopyOnWrite()   {       if(GetRefCount()>1)       {          GetRefCount()--;          char* tmp=new char[strlen(_str)+4];          tmp+=4;          strcpy(tmp,_str);          _str=tmp;          GetRefCount()=1;       }   }    char* operator [] (size_t index)    {        if(GetRefCount()>1)        {            CopyOnWrite();        }                return _str[index];    }   ~String()   {       Release();   }   char* C_str()   {        return *_str;   }private:    char* _str;};       

http://www.cnblogs.com/Lynn-Zhang/archive/2016/04/17/5400714.html
http://blog.csdn.net/zone_programming/article/details/48372941

原创粉丝点击