1. 程式人生 > >【C++】c++寫時拷貝Copy On Write

【C++】c++寫時拷貝Copy On Write

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

一樣看下圖說話:

這裡寫圖片描述