• 写时拷贝(Copy On Write)方案详解


    本文旨在通过对 写时拷贝 的四个方案(Copy On Write)分析,让大家明白写时拷贝的实现及原理。

    关于浅拷贝与深拷贝,我在之前的博客中已经阐述过了 

    浅拷贝容易出现指针悬挂的问题,深拷贝效率低,但是我们可以应用引用计数来解决浅拷贝中多次析构的问题,写时拷贝也就应运而生了。

    首先要清楚写时拷贝是利用浅拷贝来解决问题!!

    方案一

    class String
    {
    private:
        char* _str;
        int _refCount;
    };

    方案一最不靠谱,它将用作计数的整形变量_refCount定义为类的私有成员变量,任何一个对象都有它自己的成员变量_refCount,它们互不影响,难以维护。只要拷贝出了对象,_refCount大于了0,每个对象在调用自己的析构函数时--_refCount不等于0,那么它们指向的那块内存都将得不到释放,无法达到我们要的效果。

    spacer.gif

    //以下是对方案一的简单实现,大家可以结合上图感受到方案一的缺陷
     
    class String
    {
    public:
        String(char* str = "")    //不能strlen(NULL)
           :_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;
               cout << "~String " << endl;
           }
        }
        friend ostream& operator<<( ostream& output, const String &s);
    private:
        char* _str;
        int _refCount;
    };
    ostream& operator<<( ostream& output, const String & s)
    {
        output << s._str;
        return output;
    }
    void Test()
    {
        String s1("aaa");
        String s2(s1);
        String s3(s2);
        cout << s1 << endl;
        cout << s2 << endl;
        cout << s3 << endl;
    } 

    方案二

    class String
    {
    private:
        char* _str;
        static int count;
    };

    设置了一个静态整形变量来计算指向一块内存的指针的数量,每析构一次减1,直到它等于0(也就是没有指针在指向它的时候)再去释放那块内存,看似可行,其实不然!

    这个方案只适用于只调用一次构造函数、只有一块内存的情形,如果多次调用构造函数构造对象,新构造的对象照样会改变count的值,那么以前的内存无法释放会造成内存泄漏。

     

    结合上图和下面的代码,我们可以清楚地看到该方案相比方案一的改善,以及缺陷

    class String
    {
    public:
        String(char* str = "")    //不能strlen(NULL)
        {
           _str = new char[strlen( str) + 1];
           strcpy(_str, str);
     
           count++;
        }
        String(const String &s)
        {
           _str = s._str;
           count++;
            
        }
        String& operator=( String& s)  
        {
           _str = s._str;
           count++;
           return *this;
        }
        ~String()
        {
           if (--count == 0)
           {
                delete[] _str;
               _str = NULL;
               cout << "~String " << endl;
           }
        }
        friend ostream& operator<<( ostream& output, const String &s);
        friend istream& operator>>( istream& input, const String &s);
    private:
        char* _str;
        static int count;
    };
    
    int String::count = 0;      //初始化count
    void Test()    //用例测试
    {
        String s1("abcdefg");
        String s2(s1);
        String s3;
        s3 = s2;
        cout << s1 << endl;
        cout << s2 << endl;
        cout << s3 << endl;
     
        String s4("opqrst");
        String s5(s4);
        String s6 (s5);
        s6 = s4;
        cout << s4 << endl;
        cout << s5 << endl;
        cout << s6 << endl; 
    }

    方案三

    问题的关键是,我们不是要为每一个对象建立一个引用计数,而是要每一块内存设置一个引用计数,只有这样才方便我们去维护。当指向这块内存的指针数为0时,再去释放它!

    class String
    {
        private:
                   char* _str;
                   int* _refCount;      
    };

    方案三设置了一个int型的指针变量用来引用计数,每份内存空间对应一个引用计数,而不是每个对象对应一个引用计数,而且内存之间的引用计数互不影响,不会出现方案一和方案二出现的问题。

    1.在实现赋值运算符重载时要谨慎,不要遇到下图的情形

     s1指向内存1,s2指向内存2,利用s2拷贝出的对象s3也指向内存块2,这时候内存块1的引用计数等于1 ,内存块2的引用计数等于2。一切似乎都很正常,但是调用赋值运算符重载执行语句:s2=s1后,错误慢慢显现出来了。将s2指向内存1 并把内存1 的引用计数加1,这理所当然,但是不能把s2原本指向的空间直接delete,s3还指向内存2着呢!这里千万在释放一块空间前,对指向这块内存的引用计数进行检查,当引用计数为0的时候再去释放,否则只做减引用计数就行。

     //错误代码
    String& operator=(String& s) { if (_str!= s._str) { delete[] _str; delete _refCount; _str = s._str; _refCount = s._refCount; (*_refCount)++; } return *this; } 

    2.改变字符串的某个字符时要谨慎,不要遇到类似下图所遇到的问题。

    如果多个对象都指向同一块内存,那么只要一个对象改变了这块内存的内容,那所有的对象都被改变了!!

    如下图:当s1和s2都指向内存块1,s3经过赋值运算符重载后也指向内存块1,现在s2如果对字符串进行修改后,所有指向内存块1 的指针指向的内容都会被改变!

    可以用下图的形式改善这种问题:新设置一块内存来存要改变的对象,这样就不会影响其他的对象了

    案例3我画的图较多,方便大家结合代码去理解 

    //案例三
    class String { public: String(char* str = "") //不能strlen(NULL) { _refCount = new int(1); //给_refCount开辟空间,并赋初值1 _size = strlen(str); _capacity = _size + 1; _str = new char[strlen(str) + 1]; strcpy(_str, str); } String(const String &s) { _refCount = s._refCount; _str = s._str; _size = strlen(s._str); _capacity = _size + 1; (*_refCount)++; //拷贝一次_refCount都要加1 } //要考虑是s1=s2时,s1原先不为空的情况,要先释放原内存 //如果要释放原内存时,要考虑它的_refCount减1后是否为0,为零再释放,否则其它对象指针无法再访问这片空间 String& operator=(String& s) { if (_str!= s._str) { _size = strlen(s._str); _capacity = _size + 1; if (--(*_refCount) == 0) { delete[] _str; delete _refCount; } _str = s._str; _refCount = s._refCount; (*_refCount)++; } return *this; }
    //如果修改了字符串的内容,那所有指向这块内存的对象指针的内容间接被改变 //如果还有其它指针指向这块内存,我们可以从堆上重新开辟一块内存空间, //把原字符串拷贝过来 //再去改变它的内容,就不会产生链式反应 // 1.减引用计数 2.拷贝 3.创建新的引用计数 char& String::operator[](const size_t index) //参考深拷贝 { if (*_refCount==1) { return *(_str + index); } else { --(*_refCount); char* tmp = new char[strlen(_str)+1]; strcpy(tmp, _str); _str = tmp; _refCount = new int(1); return *(_str+index); } } ~String() { if (--(*_refCount)== 0) //当_refCount=0的时候就释放内存 { delete[] _str; delete _refCount; _str = NULL; cout << "~String " << endl; } _size = 0; _capacity = 0; } friend ostream& operator<<(ostream& output, const String &s); friend istream& operator>>(istream& input, const String &s); private: char* _str; //指向字符串的指针 size_t _size; //字符串大小 size_t _capacity; //容量 int* _refCount; //计数指针 }; ostream& operator<<(ostream& output, const String &s) { output << s._str; return output; } istream& operator>>(istream& input, const String &s) { input >> s._str; return input; } void Test() //用例测试 { String s1("abcdefg"); String s2(s1); String s3; s3 = s2; cout << s1 << endl; cout << s2 << endl; cout << s3 << endl; s2[3] = '0'; cout << s1 << endl; cout << s2 << endl; cout << s3 << endl; }  

    方案四

    class String
    {
       private:
               char* _str;
    };
    

     wKiom1b03ZmQGBzTAAAhoeram48598.png

    方案四与方案三类似。方案四把用来计数的整型指针变量放在所开辟的内存空间的首部。

    用*((int*)_str)就能取得计数值

    class String
    {
    public:
               String(char * str = "" )    //不能strlen(NULL)
               {
                        _str = new char[strlen( str) + 5];
                        _str += 4;
                        strcpy(_str, str);
                        GetRefCount(_str) = 1;
               }
               String(const String &s)
               {
                        _str = s._str;
                        ++GetRefCount(_str);
               }
     
               //要考虑是s1=s2时,s1原先不为空的情况,要先释放原内存
               //如果要释放原内存时,要考虑它的_refCount减1后是否为0,
               //为零再释放,否则其它对象指针无法再访问这片空间
               String& operator=(String& s)
               {
                        if (this != &s )
                        {
                                  if (GetRefCount(_str ) == 1)
                                  {
                                           delete (_str-4);
                                           _str = s._str;
                                           ++GetRefCount(_str );
                                  }
                                  else
                                  {
                                           --GetRefCount(_str );
                                           _str = s._str;
                                           ++GetRefCount(_str );
                                  }
                        }
                        return *this ;
               }
               //如果修改了字符串的内容,那所有指向这块内存的对象指针的内容间接被改变
               //如果还有其它指针指向这块内存,我们可以从堆上重新开辟一块内存空间,
               //把原字符串拷贝过来.
               //再去改变它的内容,就不会产生链式反应
                
              
               char& String ::operator[](const size_t index ) //深拷贝     
               {
                        
                                  if (GetRefCount(_str) == 1)
                                  {
                                           return _str[index ];
                                  }
                                  else
                                  {
                                            //  1.减引用计数
                                           --GetRefCount(_str );
                                            //  2.拷贝     3.创建新的引用计数
                                           char* tmp = new char [strlen(_str) + 5];   
                                          *((int *)tmp) = 1;
                                           tmp += 4;
                                           strcpy(tmp, _str);
                                           _str = tmp;
                                           return _str[index ];
                                  }
               }
     
               int& GetRefCount(char* ptr)    //获取引用计数(隐式内联函数)
               {
                        return *((int *)(ptr -4));
               }
               ~String()
               {
                        if (--GetRefCount(_str) == 0)
                        {
                                  cout << "~String" << endl;
                                  delete[] (_str-4);             
                        }
               
               }
               friend ostream& operator<<( ostream& output, const String &s);
               friend istream& operator>>( istream& input, const String &s);
    private:
               char* _str;
     
    };
     
     
    ostream& operator<<(ostream& output, const String &s)
    {
               output << s._str;
               return output;
    }
    istream& operator>>(istream& input, const String &s)
    {
               input >> s._str;
               return input;
    }
     
    void Test()  //用例测试
    {
               String s1("abcdefg" );
               String s2(s1);
               String s3;
               s3 = s2;
               cout << s1 << endl;
               cout << s2 << endl;
               cout << s3 << endl;
               s2[3] = '0';
               cout << s1 << endl;
               cout << s2 << endl;
               cout << s3 << endl;
    } 
  • 相关阅读:
    NStimer 被堵塞
    零基础学python-7.6 字符串格式化表达式
    实验记录三 通用输入输出(GPIO)
    VTK的安装配置-使用VS2010
    Python 查找Twitter中特定话题中最流行的10个转发Tweet
    ios开发之-计算器的改进
    彻查网络局部网段内Ping时断时续的问题
    hdoj-1016-Prime Ring Problem【深搜】
    WebView的截屏实现
    CentOs虚拟机能够互相ping通,但无法訪问虚拟机服务
  • 原文地址:https://www.cnblogs.com/Lynn-Zhang/p/5400714.html
Copyright © 2020-2023  润新知