寫時拷貝(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;
} 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM