1. 程式人生 > >C++引用計數(reference counting)技術簡介(1)

C++引用計數(reference counting)技術簡介(1)

1.引用計數的作用

C++引用計數是C++為彌補沒有垃圾回收機制而提出的記憶體管理的一個方法和技巧,它允許多個擁有共同值的物件共享同一個物件實體。

C++的引用計數作為記憶體管理的方法和技術手段主要有一下兩個作用。
(1)簡化了堆物件(Heap Objects)的管理。 一個物件從堆中被分配出來之後,需要明確知道是誰擁有了這個物件,因為只有擁有這個物件的所有者才能夠銷燬它。但在實際使用過程中, 這個物件可能被傳遞給另一個物件(例如通過傳遞指標引數),一旦這個過程複雜,我們很難確定誰最後擁有了這個物件。 使用引用計數就可以拋開這個問題,我們不需要再去關心誰擁有了這個物件,因為我們把管理權交給了物件自己。當這個物件不再被引用時,它自己負責銷燬自己。

(2)解決了同一個物件存在多份拷貝的問題。引用計數可以讓等值物件共享一份資料實體。這樣不僅節省記憶體,也使程式速度加快,因為不在需要構造和析構同值物件的多餘副本。

2.等值物件具有多份拷貝的情況

一個未使用引用計數計數實現的String類虛擬碼示例如下:

class String
{
public:
    String(const char* value="");
    String& operator=(const String& rhs)
    {
        if(this==&rhs)    //防止自我賦值
            return
*this; delete[] data; //刪除舊資料 data=new char[strlen(rhs.data)=1] strcpy(data,rhs.data); return *this; } ... private: char* data; }; String a,b,c,d,e; a=b=c=d=e="Hello";

很顯然物件a~e都有相同的值”hello”,這就是等值物件存在多份拷貝。

3.以引用計數實現String

3.1含有引用計數的字串資料實體

引用計數實現String需要額外的變數來描述資料實體被引用的次數,即描述字串值被多少個String物件所共享。這裡重新設計一個結構體StringValue來描述字串和引用計數。StringValue設計如下:

Struct StringValue
{
    int refCount;
    char* data;
};

3.2含有引用計數的字串資料實體的String

新的String類的大致定義可描述如下:

class String
{
private:
    Struct StringValue
    {
        int refCount;
        char* data;
        StringValue(const char* initValue);
        ~StringValue();
    };
    StringValue* value;  

public:
    String(const char* initValue="");//constructor
    String(const String& rhs);//copy constructor
    String& operator=(const String& rhs); //assignment operator
    ~String(); //destructor
};

關於StringValue的建構函式和解構函式可定義如下:

String::StringValue::StringValue(const char* initValue):refCount(1)
{
     data=new char[strlen(initValue)+1];
     strcpy(data,initValue);
 }

String::StringValue::~StringValue()
{
    delete[] data;
}

String的成員函式可定義如下:
String的建構函式:

String::String(const char* initValue):value(new StringValue(initValue)){}

在這種建構函式的作用下String s1("lvlv");String s2=("lvlv"),分開構造相同初值的字串在記憶體中存在相同的拷貝,並沒有達到資料共享的效果。其資料結構為:
這裡寫圖片描述

事實上可以令String追蹤到現有的StringValue物件,並僅僅在字串獨一無二的情況下才產生新的StringValue物件,上圖所顯示的重複記憶體空間便可消除。這樣細緻的考慮和實現需要增加額外的程式碼,可有讀者自行實現和練習。

String拷貝建構函式:
當String物件被複制時,產生新的String物件共享同一個StringValue物件,其程式碼實現可為:

String::String(const String& rhs):value(rhs.value)
{
    ++valus->refCount;
}

如果以圖示表示,下面的程式碼:

String s1("lvlv");
String s2=s1;

會產生如下的資料結構:
這裡寫圖片描述

這樣就會比傳統的non-reference-counted String類效率高,因為它不需要分配記憶體給字串的第二個副本使用,也不要再使用後歸還記憶體,更不需要將字串值複製到記憶體中。這裡只需要將指標複製一份,並將引用計數加1。

String解構函式:
String的解構函式在絕大部分呼叫中只需要將引用次數減1,只有當引用次數為1時,才回去真正銷燬StringValue物件:

String::~String()
{
    if(--value->refCount==0) delete value;
}

String的賦值操作符(assignment):
當用戶寫下s2=s1;時,這是String物件的相互賦值,s1和s2指向同一個StringValue物件,該物件的引用次數應該在賦值過程中加1。此外,賦值動作之前s2所指向的StringValue物件的引用次數應該減1,因為s2不再擁有該值。如果s2是原本StringValue物件的最後一個引用者,StringValue物件將被s2銷燬。String的賦值操作符實現如下:

String& String::operator=(const String& rhs)
{  
    if (this->value == rhs.value) //自賦值  
        return *this;  

    //賦值時左運算元引用計數減1,當變為0時,沒有指標指向該記憶體,銷燬  
    if (--value->refCount == 0)  
        delete value;  

    //不必開闢新記憶體空間,只要讓指標指向同一塊記憶體,並把該記憶體塊的引用計數加1  
    value = rhs.value;  
    ++value->refCount;  
    return *this;  
}  

3.3String的寫時複製(Copy-on-Write)

字串應該支援以下標讀取或者修改某個字元,需要過載方括號操作符。String應該有

const char& operator[](size_t index) const;//過載[]運算子,針對const Strings  
char& operator[](size_t index);//過載[]運算子,針對non-const Strings  

對於const版本,因為是隻讀動作,字串內容不受影響:

const char& String::operator[](size_t index) const
{
    return value->data[index];
}

對於non-const版本,該函式可能用來讀取,也可能用來寫一個字元,C++編譯器無法告訴我們operator[]被呼叫時是用於寫還是取,所以我們必須假設所有的non-const operator[]的呼叫都用於寫。此時,我能就要確保沒有其他任何共享的同一個StringValue的String物件因寫動作而改變。也就是說,在任何時候,我們返回一個字元引用指向String的StringValue物件內的一個字元時,我們必須確保該StringValue物件的引用次數為1,沒有其他的String物件引用它。

//過載[]運算子,針對non-const Strings  
char& String::operator[](size_t index)
{  
    if (value->refCount>1)
    {  
        --value->refCount;  
        value = new StringValue(value->data);  
    }  
    if (index<strlen(value->data))  
        return value->data[index];  
}  

和其他物件共享一份資料實體,直到必須對自己擁有的那份實值進行寫操作,這種在電腦科學領域中存在了很長曆史。特別是在作業系統領域,各程序(processes)之間往往允許共享某些記憶體分頁(memory pages),直到它們打算修改屬於自己的那一分頁。這項技術非常普及,就是著名的寫時複製(copy-on-write)。

注意:實現了String的寫時複製,但存在一個問題,比如:

String s1="Hello";
char* p=&s1[1];
String s2=s1;

這樣就會出現如下資料結構:
這裡寫圖片描述

這表示下面的語句會導致其他的String物件也被修改。這個不問題不限於指標,如果有人以引用的方式將String的non-const operator[]返回值儲存起來,也會發生同樣的問題。

解決這種問題主要有三種方法。
(1)忽略之。
允許這種操作,即使出錯也不錯處理。這種方法很不幸被那些實現reference-counted字串的類庫所採用。考察如下程式,

#include <iostream>
#include <string>
using namespace std;

std::string a="lvlv";

int main()
{
    char* p=&a[1];
    *p='a';
    std:: string b=a;
    std::cout<<"b:"<<b<<endl;
    return 0;
}

上面程式碼在VS2017中編譯執行輸出”lalv”。

(2)警告
有些編譯器知道會有這種問題,並給出警告。雖然無力解決,卻會說明不要那麼做,如果違背,後果不可預期。

(3)避免
徹底解決這種問題,採取零容忍態度。但是會降低物件之間共享的資料實體的個數。基本解決辦法是:為每一個StringValue物件加上一個flag標誌,用以指示是否可被共享。一開始,我們先樹立此標誌為true,表示物件可被共享,但只要non-const operator[]作用於物件值時就將標誌清除。一旦標誌被設為false,那麼資料實體可能永遠不會再被共享了。

下面是StringValue的修改版,包含一個可共享標誌flag。

Struct StringValue
{
    int refCount;
    char* data;
    bool shareable;

    StringValue(const char* initValue);
    ~StringValue();
};

String::StringValue::StringValue(const char* initValue):refCount(1),shareable(true)
{
     data=new char[strlen(initValue)+1];
     strcpy(data,initValue);
 }

String::StringValue::~StringValue()
{
    delete[] data;
}

相比之前的StringValue的建構函式和解構函式,並沒有什麼大的修改。當然String member functions也要做相應的修改。以copy constructor為例,修改如下:

String::String(const String& rhs)
{
    if(rhs.value->shareable)
    {
        value=rhs.value;
        ++valus->refCount;
    }
}

其他的String的成員函式都應該以類似的方法檢查shareable。對於Non-const operator[]是唯一將shareable設為false者,其實現程式碼可為:

char& String::operator[](size_t index)
{  
    if (value->refCount>1)
    {  
        --value->refCount;  
        value = new StringValue(value->data);  
    }  
    value->shareable=false;//新增此行
    if (index<strlen(value->data))  
        return value->data[index];  
}  

4.小結

以上描述了引用計數的作用和使用引用計數來實現自定義的字串類String。使用引用計數來實現自定義類時,需要考慮很多細節問題,尤其是寫時複製是提升效率的有效手段。

要幾本掌握引用計數這項技術,需要我們明白引用計數是什麼,其作用還有如何在自定義類中實現引用計數,如果這些都掌握了,那麼引用計數也算是基本掌握了。

參考文獻

[1]記憶體管理之引用計數
[2]More Effective C++.Scott Meyers著,侯捷譯.P183-213.
[3]more effective c++讀書筆記