天天看點

标準C++類string的Copy-On-Write技術(三)3、             後記

最後的這個問題,我們主要解決的是那個“民主集中”的難題。請先看下面的代碼:

string h1 = “hello”;

string h2= h1;

string h3;

h3 = h2;

string w1 = “world”;

string w2(“”);

w2=w1;

很明顯,我們要讓h1、h2、h3共享同一塊記憶體,讓w1、w2共享同一塊記憶體。因為,在h1、h2、h3中,我們要維護一個引用計數,在w1、w2中我們又要維護一個引用計數。

如何使用一個巧妙的方法産生這兩個引用計數呢?我們想到了string類的記憶體是在堆上動态配置設定的,既然共享記憶體的各個類指向的是同一個記憶體區,我們為什麼不在這塊區上多配置設定一點空間來存放這個引用計數呢?這樣一來,所有共享一塊記憶體區的類都有同樣的一個引用計數,而這個變量的位址既然是在共享區上的,那麼所有共享這塊記憶體的類都可以通路到,也就知道這塊記憶體的引用者有多少了。

請看下圖:

于是,有了這樣一個機制,每當我們為string配置設定記憶體時,我們總是要多配置設定一個空間用來存放這個引用計數的值,隻要發生拷貝構造可是指派時,這個記憶體的值就會加一。而在内容修改時,string類為檢視這個引用計數是否為0,如果不為零,表示有人在共享這塊記憶體,那麼自己需要先做一份拷貝,然後把引用計數減去一,再把資料拷貝過來。下面的幾個程式片段說明了這兩個動作:

   //構造函數(分存記憶體)

    string::string(const char* tmp)

{

    _Len = strlen(tmp);

    _Ptr = new char[_Len+1+1];

    strcpy( _Ptr, tmp );

    _Ptr[_Len+1]=0;  // 設定引用計數  

}

//拷貝構造(共享記憶體)

    string::string(const string& str)

    {

         if (*this != str){

              this->_Ptr = str.c_str();   //共享記憶體

              this->_Len = str.szie();

              this->_Ptr[_Len+1] ++;  //引用計數加一

         }

//寫時才拷貝Copy-On-Write

char& string::operator[](unsigned int idx)

    if (idx > _Len || _Ptr == 0 ) {

         static char nullchar = 0;

return nullchar;

          }

_Ptr[_Len+1]--;   //引用計數減一

    char* tmp = new char[_Len+1+1];

    strncpy( tmp, _Ptr, _Len+1);

    _Ptr = tmp;

    _Ptr[_Len+1]=0; // 設定新的共享記憶體的引用計數

    return _Ptr[idx];

//析構函數的一些處理

~string()

         // 引用計數為0時,釋放記憶體 

    if (_Ptr[_Len+1]==0) {

        delete[] _Ptr;

         }

哈哈,整個技術細節完全浮出水面。

不過,這和STL中basic_string的實作細節還有一點點差别,在你打開STL的源碼時,你會發現其取引用計數是通過這樣的通路:_Ptr[-1],标準庫中,把這個引用計數的記憶體配置設定在了前面(我給出來的代碼是把引用計數配置設定以了後面,這很不好),配置設定在前的好處是當string的長度擴充時,隻需要在後面擴充其記憶體,而不需要移動引用計數的記憶體存放位置,這又節省了一點時間。

STL中的string的記憶體結構就像我前面畫的那個圖一樣,_Ptr指着是資料區,而RefCnt則在_Ptr-1 或是 _Ptr[-1]處。

是誰說的“有太陽的地方就會有黑暗”?或許我們中的許多人都很迷信标準的東西,認為其是久經考驗,不可能出錯的。呵呵,千萬不要有這種迷信,因為任何設計再好,編碼再好的代碼在某一特定的情況下都會有Bug,STL同樣如此,string類的這個共享記憶體/寫時才拷貝技術也不例外,而且這個Bug或許還會讓你的整個程式crash掉!

不信?!那麼讓我們來看一個測試案例:

假設有一個動态連結庫(叫myNet.dll或myNet.so)中有這樣一個函數傳回的是string類:

string GetIPAddress(string hostname)

    static string ip;

    ……

    return ip;

而你的主程式中動态地載入這個動态連結庫,并調用其中的這個函數:

main()

//載入動态連結庫中的函數

hDll = LoadLibraray(…..);

pFun =  GetModule(hDll, “GetIPAddress”);

//調用動态連結庫中的函數

string ip = (*pFun)(“host1”);

……

//釋放動态連結庫

FreeLibrary(hDll);

cout << ip << endl;

讓我們來看看這段代碼,程式以動态方式載入動态連結庫中的函數,然後以函數指針的方式調用動态連結庫中的函數,并把傳回值放在一個string類中,然後釋放了這個動态連結庫。釋放後,輸入ip的内容。

根據函數的定義,我們知道函數是“值傳回”的,是以,函數傳回時,一定會調用拷貝構造函數,又根據string類的記憶體共享機制,在主程式中變量ip是和函數内部的那個靜态string變量共享記憶體(這塊記憶體區是在動态連結庫的位址空間的)。而我們假設在整個主程式中都沒有對ip的值進行修改過。那麼在當主程式釋放了動态連結庫後,那個共享的記憶體區也随之釋放。是以,以後對ip的通路,必然做造成記憶體位址通路非法,造成程式crash。即使你在以後沒有使用到ip這個變量,那麼在主程式退出時也會發生記憶體通路異常,因為程式退出時,ip會析構,在析構時就會發生記憶體通路異常。

記憶體通路異常,意味着兩件事:1)無論你的程式再漂亮,都會因為這個錯誤變得暗淡無光,你的聲譽也會因為這個錯誤受到損失。2)未來的一段時間,你會被這個系統級錯誤所煎熬(在C++世界中,找到并排除這種記憶體錯誤并不是一件容易的事情)。這是C/C++程式員永遠的心頭之痛,千裡之堤,潰于蟻穴。而如果你不清楚string類的這種特征,在成千上萬行代碼中找這樣一個記憶體異常,簡直就是一場噩夢。

備注:要改正上述的Bug,有很多種方法,這裡提供一種僅供參考:

string ip = (*pFun)(“host1”).cstr();

文章到這裡也應該結束了,這篇文章的主要有以下幾個目的:

1)    向大家介紹一下寫時才拷貝/記憶體共享這種技術。

2)    以STL中的string類為例,向大家介紹了一種設計模式。

3)    在C++世界中,無論你的設計怎麼精巧,代碼怎麼穩固,都難以照顧到所有的情況。智能指針更是一個典型的例子,無論你怎麼設計,都會有非常嚴重的BUG。

4)    C++是一把雙刃劍,隻有了解了原理,你才能更好的使用C++。否則,必将引火燒身。如果你在設計和使用類庫時有一種“玩C++就像玩火,必須千萬小心”的感覺,那麼你就入門了,等你能把這股“火”控制的得心應手時,那才是學成了。

最後,還是利用這個後序,介紹一下自己。我目前從事于所有Unix平台下的軟體研發,主要是做系統級的産品軟體研發,對于下一代的計算機革命——網格計算非常地感興趣,同于對于分布式計算、P2P、Web Service、J2EE技術方向也很感興趣,另外,對于項目實施、團隊管理、項目管理也小有心得,希望同樣和我戰鬥在“技術和管理并重”的陣線上的年輕一代,能夠和我多多地交流。

本文轉自 haoel 51CTO部落格,原文連結:http://blog.51cto.com/haoel/124634,如需轉載請自行聯系原作者

繼續閱讀