欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

標(biāo)準(zhǔn)C++類string的Copy-On-Write技術(shù)

 更新時間:2013年11月06日 22:33:08   投稿:mdxy-dxy  
這里,我想從C++類或是設(shè)計模式的角度為各位揭開Copy-On-Write技術(shù)在string中實現(xiàn)的面紗,以供各位在用C++進行類庫設(shè)計時做一點參考

標(biāo)準(zhǔn)C++類std::string的內(nèi)存共享和Copy-On-Write技術(shù) 陳皓

1、概念
 
Scott Meyers在《More Effective C++》中舉了個例子,不知你是否還記得?在你還在上學(xué)的時候,你的父母要你不要看電視,而去復(fù)習(xí)功課,于是你把自己關(guān)在房間里,做出一副正在復(fù)習(xí)功課的樣子,其實你在干著別的諸如給班上的某位女生寫情書之類的事,而一旦你的父母出來在你房間要檢查你是否在復(fù)習(xí)時,你才真正撿起課本看書。這就是“拖延戰(zhàn)術(shù)”,直到你非要做的時候才去做。
 
當(dāng)然,這種事情在現(xiàn)實生活中時往往會出事,但其在編程世界中搖身一變,就成為了最有用的技術(shù),正如C++中的可以隨處聲明變量的特點一樣,Scott Meyers推薦我們,在真正需要一個存儲空間時才去聲明變量(分配內(nèi)存),這樣會得到程序在運行時最小的內(nèi)存花銷。執(zhí)行到那才會去做分配內(nèi)存這種比較耗時的工作,這會給我們的程序在運行時有比較好的性能。必竟,20%的程序運行了80%的時間。
 
當(dāng)然,拖延戰(zhàn)術(shù)還并不只是這樣一種類型,這種技術(shù)被我們廣泛地應(yīng)用著,特別是在操作系統(tǒng)當(dāng)中,當(dāng)一個程序運行結(jié)束時,操作系統(tǒng)并不會急著把其清除出內(nèi)存,原因是有可能程序還會馬上再運行一次(從磁盤把程序裝入到內(nèi)存是個很慢的過程),而只有當(dāng)內(nèi)存不夠用了,才會把這些還駐留內(nèi)存的程序清出。
 
寫時才拷貝(Copy-On-Write)技術(shù),就是編程界“懶惰行為”——拖延戰(zhàn)術(shù)的產(chǎn)物。舉個例子,比如我們有個程序要寫文件,不斷地根據(jù)網(wǎng)絡(luò)傳來的數(shù)據(jù)寫,如果每一次fwrite或是fprintf都要進行一個磁盤的I/O操作的話,都簡直就是性能上巨大的損失,因此通常的做法是,每次寫文件操作都寫在特定大小的一塊內(nèi)存中(磁盤緩存),只有當(dāng)我們關(guān)閉文件時,才寫到磁盤上(這就是為什么如果文件不關(guān)閉,所寫的東西會丟失的原因)。更有甚者是文件關(guān)閉時都不寫磁盤,而一直等到關(guān)機或是內(nèi)存不夠時才寫磁盤,Unix就是這樣一個系統(tǒng),如果非正常退出,那么數(shù)據(jù)就會丟失,文件就會損壞。
 
呵呵,為了性能我們需要冒這樣大的風(fēng)險,還好我們的程序是不會忙得忘了還有一塊數(shù)據(jù)需要寫到磁盤上的,所以這種做法,還是很有必要的。

2、標(biāo)準(zhǔn)C++類std::string的Copy-On-Write
 
在我們經(jīng)常使用的STL標(biāo)準(zhǔn)模板庫中的string類,也是一個具有寫時才拷貝技術(shù)的類。C++曾在性能問題上被廣泛地質(zhì)疑和指責(zé)過,為了提高性能,STL中的許多類都采用了Copy-On-Write技術(shù)。這種偷懶的行為的確使使用STL的程序有著比較高要性能。
 
這里,我想從C++類或是設(shè)計模式的角度為各位揭開Copy-On-Write技術(shù)在string中實現(xiàn)的面紗,以供各位在用C++進行類庫設(shè)計時做一點參考。
 
在講述這項技術(shù)之前,我想簡單地說明一下string類內(nèi)存分配的概念。通過常,string類中必有一個私有成員,其是一個char*,用戶記錄從堆上分配內(nèi)存的地址,其在構(gòu)造時分配內(nèi)存,在析構(gòu)時釋放內(nèi)存。因為是從堆上分配內(nèi)存,所以string類在維護這塊內(nèi)存上是格外小心的,string類在返回這塊內(nèi)存地址時,只返回const char*,也就是只讀的,如果你要寫,你只能通過string提供的方法進行數(shù)據(jù)的改寫。
 
2.1、特性
 
由表及里,由感性到理性,我們先來看一看string類的Copy-On-Write的表面特征。讓我們寫下下面的一段程序:
 

復(fù)制代碼 代碼如下:

#include
#include
using namespace std;
 
main()
{
       string str1 = "hello world";
       string str2 = str1;
      
       printf ("Sharing the memory:/n");
       printf ("/tstr1's address: %x/n", str1.c_str() );
       printf ("/tstr2's address: %x/n", str2.c_str() );
      
    str1[1]='q';
       str2[1]='w';
 
       printf ("After Copy-On-Write:/n");
       printf ("/tstr1's address: %x/n", str1.c_str() );
       printf ("/tstr2's address: %x/n", str2.c_str() );
 
       return 0;
}

 
這個程序的意圖就是讓第二個string通過第一個string構(gòu)造,然后打印出其存放數(shù)據(jù)的內(nèi)存地址,然后分別修改str1和str2的內(nèi)容,再查一下其存放內(nèi)存的地址。程序的輸出是這樣的(我在VC6.0和g++ 2.95都得到了同樣的結(jié)果):
復(fù)制代碼 代碼如下:

> g++ -o stringTest stringTest.cpp
> ./stringTest
Sharing the memory:
        str1's address: 343be9
        str2's address: 343be9
After Copy-On-Write:
        str1's address: 3407a9
        str2's address: 343be9

 
從結(jié)果中我們可以看到,在開始的兩個語句后,str1和str2存放數(shù)據(jù)的地址是一樣的,而在修改內(nèi)容后,str1的地址發(fā)生了變化,而str2的地址還是原來的。從這個例子,我們可以看到string類的Copy-On-Write技術(shù)。
 

2.2、深入

在深入這前,通過上述的演示,我們應(yīng)該知道在string類中,要實現(xiàn)寫時才拷貝,需要解決兩個問題,一個是內(nèi)存共享,一個是Copy-On-Wirte,這兩個主題會讓我們產(chǎn)生許多疑問,還是讓我們帶著這樣幾個問題來學(xué)習(xí)吧:
1、  Copy-On-Write的原理是什么?
2、  string類在什么情況下才共享內(nèi)存的?
3、  string類在什么情況下觸發(fā)寫時才拷貝(Copy-On-Write)?
4、  Copy-On-Write時,發(fā)生了什么?
5、  Copy-On-Write的具體實現(xiàn)是怎么樣的?
 
喔,你說只要看一看STL中stirng的源碼你就可以找到答案了。當(dāng)然,當(dāng)然,我也是參考了string的父模板類basic_string的源碼。但是,如果你感到看STL的源碼就好像看機器碼,并嚴(yán)重打擊你對C++自信心,乃至產(chǎn)生了自己是否懂C++的疑問,如果你有這樣的感覺,那么還是繼續(xù)往下看我的這篇文章吧。
 
OK,讓我們一個問題一個問題地探討吧,慢慢地所有的技術(shù)細節(jié)都會浮出水面的。
 
2.3、Copy-On-Write的原理是什么?
 
有一定經(jīng)驗的程序員一定知道,Copy-On-Write一定使用了“引用計數(shù)”,是的,必然有一個變量類似于RefCnt。當(dāng)?shù)谝粋€類構(gòu)造時,string的構(gòu)造函數(shù)會根據(jù)傳入的參數(shù)從堆上分配內(nèi)存,當(dāng)有其它類需要這塊內(nèi)存時,這個計數(shù)為自動累加,當(dāng)有類析構(gòu)時,這個計數(shù)會減一,直到最后一個類析構(gòu)時,此時的RefCnt為1或是0,此時,程序才會真正的Free這塊從堆上分配的內(nèi)存。
 
是的,引用計數(shù)就是string類中寫時才拷貝的原理!
  
2.3.1、      string類在什么情況下才共享內(nèi)存的?
 
這個問題的答案應(yīng)該是明顯的,根據(jù)常理和邏輯,如果一個類要用另一個類的數(shù)據(jù),那就可以共享被使用類的內(nèi)存了。這是很合理的,如果你不用我的,那就不用共享,只有你使用我的,才發(fā)生共享。
 
使用別的類的數(shù)據(jù)時,無非有兩種情況,1)以別的類構(gòu)造自己,2)以別的類賦值。第一種情況時會觸發(fā)拷貝構(gòu)造函數(shù),第二種情況會觸發(fā)賦值操作符。這兩種情況我們都可以在類中實現(xiàn)其對應(yīng)的方法。對于第一種情況,只需要在string類的拷貝構(gòu)造函數(shù)中做點處理,讓其引用計數(shù)累加;同樣,對于第二種情況,只需要重載string類的賦值操作符,同樣在其中加上一點處理。
 
 
嘮叨幾句:
 
1)構(gòu)造和賦值的差別

對于前面那個例程中的這兩句:
       string str1 = "hello world";
       string str2 = str1;

不要以為有“=”就是賦值操作,其實,這兩條語句等價于:

       string str1 ("hello world");   //調(diào)用的是構(gòu)造函數(shù)
       string str2 (str1);   //調(diào)用的是拷貝構(gòu)造函數(shù)
 
如果str2是下面的這樣情況:

string str2;      //調(diào)用參數(shù)默認(rèn)為空串的構(gòu)造函數(shù):string str2(“”);
str2 = str1;     //調(diào)用str2的賦值操作:str2.operator=(str1);
 
2) 另一種情況
       char tmp[]=”hello world”;
     string str1 = tmp;
       string str2 = tmp;
    這種情況下會觸發(fā)內(nèi)存的共享嗎?想當(dāng)然的,應(yīng)該要共享??墒歉鶕?jù)我們前面所說的共享內(nèi)存的情況,兩個string類的聲明和初始語句并不符合我前述的兩種情況,所以其并不發(fā)生內(nèi)存共享。而且,C++現(xiàn)有特性也無法讓我們做到對這種情況進行類的內(nèi)存共享。
 
 
 
2.3.2、      string類在什么情況下觸發(fā)寫時才拷貝(Copy-On-Write)?
 
哦,什么時候會發(fā)現(xiàn)寫時才拷貝?很顯然,當(dāng)然是在共享同一塊內(nèi)存的類發(fā)生內(nèi)容改變時,才會發(fā)生Copy-On-Write。比如string類的[]、=、+=、+、操作符賦值,還有一些string類中諸如insert、replace、append等成員函數(shù),包括類的析構(gòu)時。
 
修改數(shù)據(jù)才會觸發(fā)Copy-On-Write,不修改當(dāng)然就不會改啦。這就是托延戰(zhàn)術(shù)的真諦,非到要做的時候才去做。
 
2.3.3、Copy-On-Write時,發(fā)生了什么?
 
我們可能根據(jù)那個訪問計數(shù)來決定是否需要拷貝,參看下面的代碼:

復(fù)制代碼 代碼如下:

If  ( RefCnt>0 ) {
    char* tmp =  (char*) malloc(strlen(_Ptr)+1);
    strcpy(tmp, _Ptr);
    _Ptr = tmp;
}

 
上面的代碼是一個假想的拷貝方法,如果有別的類在引用(檢查引用計數(shù)來獲知)這塊內(nèi)存,那么就需要把更改類進行“拷貝”這個動作。
 
我們可以把這個拷的運行封裝成一個函數(shù),供那些改變內(nèi)容的成員函數(shù)使用。

2.3.4、      Copy-On-Write的具體實現(xiàn)是怎么樣的?

復(fù)制代碼 代碼如下:

string h1 = “hello”;
string h2= h1;
string h3;
h3 = h2;
 
string w1 = “world”;
string w2(“”);
w2=w1;

 
很明顯,我們要讓h1、h2、h3共享同一塊內(nèi)存,讓w1、w2共享同一塊內(nèi)存。因為,在h1、h2、h3中,我們要維護一個引用計數(shù),在w1、w2中我們又要維護一個引用計數(shù)。
 
如何使用一個巧妙的方法產(chǎn)生這兩個引用計數(shù)呢?我們想到了string類的內(nèi)存是在堆上動態(tài)分配的,既然共享內(nèi)存的各個類指向的是同一個內(nèi)存區(qū),我們?yōu)槭裁床辉谶@塊區(qū)上多分配一點空間來存放這個引用計數(shù)呢?這樣一來,所有共享一塊內(nèi)存區(qū)的類都有同樣的一個引用計數(shù),而這個變量的地址既然是在共享區(qū)上的,那么所有共享這塊內(nèi)存的類都可以訪問到,也就知道這塊內(nèi)存的引用者有多少了。
 
請看下圖:



于是,有了這樣一個機制,每當(dāng)我們?yōu)閟tring分配內(nèi)存時,我們總是要多分配一個空間用來存放這個引用計數(shù)的值,只要發(fā)生拷貝構(gòu)造可是賦值時,這個內(nèi)存的值就會加一。而在內(nèi)容修改時,string類為查看這個引用計數(shù)是否為0,如果不為零,表示有人在共享這塊內(nèi)存,那么自己需要先做一份拷貝,然后把引用計數(shù)減去一,再把數(shù)據(jù)拷貝過來。下面的幾個程序片段說明了這兩個動作:
 
復(fù)制代碼 代碼如下:

   //構(gòu)造函數(shù)(分存內(nèi)存)
    string::string(const char* tmp)
{
    _Len = strlen(tmp);
    _Ptr = new char[_Len+1+1];
    strcpy( _Ptr, tmp );
    _Ptr[_Len+1]=0;  // 設(shè)置引用計數(shù)  
}
 
//拷貝構(gòu)造(共享內(nèi)存)
    string::string(const string& str)
    {
if (*this != str){
     this->_Ptr = str.c_str();   //共享內(nèi)存
     this->_Len = str.szie();
     this->_Ptr[_Len+1] ++;  //引用計數(shù)加一
}
}
 
//寫時才拷貝Copy-On-Write
char& string::operator[](unsigned int idx)
{
    if (idx > _Len || _Ptr == 0 ) {
static char nullchar = 0;
return nullchar;
 }
   
_Ptr[_Len+1]--;   //引用計數(shù)減一
    char* tmp = new char[_Len+1+1];
    strncpy( tmp, _Ptr, _Len+1);
    _Ptr = tmp;
    _Ptr[_Len+1]=0; // 設(shè)置新的共享內(nèi)存的引用計數(shù)
   
    return _Ptr[idx];
}
 
//析構(gòu)函數(shù)的一些處理
~string()

_Ptr[_Len+1]--;   //引用計數(shù)減一
  
// 引用計數(shù)為0時,釋放內(nèi)存
    if (_Ptr[_Len+1]==0) {
        delete[] _Ptr;
}
}

 
哈哈,整個技術(shù)細節(jié)完全浮出水面。
 
不過,這和STL中basic_string的實現(xiàn)細節(jié)還有一點點差別,在你打開STL的源碼時,你會發(fā)現(xiàn)其取引用計數(shù)是通過這樣的訪問:_Ptr[-1],標(biāo)準(zhǔn)庫中,把這個引用計數(shù)的內(nèi)存分配在了前面(我給出來的代碼是把引用計數(shù)分配以了后面,這很不好),分配在前的好處是當(dāng)string的長度擴展時,只需要在后面擴展其內(nèi)存,而不需要移動引用計數(shù)的內(nèi)存存放位置,這又節(jié)省了一點時間。
 
STL中的string的內(nèi)存結(jié)構(gòu)就像我前面畫的那個圖一樣,_Ptr指著是數(shù)據(jù)區(qū),而RefCnt則在_Ptr-1 或是 _Ptr[-1]處。
   
不信?!那么讓我們來看一個測試案例:
 
假設(shè)有一個動態(tài)鏈接庫(叫myNet.dll或myNet.so)中有這樣一個函數(shù)返回的是string類:

復(fù)制代碼 代碼如下:

string GetIPAddress(string hostname)
{
    static string ip;
    ……
    ……
    return ip;
}

 
而你的主程序中動態(tài)地載入這個動態(tài)鏈接庫,并調(diào)用其中的這個函數(shù):
 
復(fù)制代碼 代碼如下:

main()
{
//載入動態(tài)鏈接庫中的函數(shù)
hDll = LoadLibraray(…..);
pFun =  GetModule(hDll, “GetIPAddress”);
 
//調(diào)用動態(tài)鏈接庫中的函數(shù)
string ip = (*pFun)(“host1”);
……
……
//釋放動態(tài)鏈接庫
FreeLibrary(hDll);
……
cout << ip << endl;
}

 
讓我們來看看這段代碼,程序以動態(tài)方式載入動態(tài)鏈接庫中的函數(shù),然后以函數(shù)指針的方式調(diào)用動態(tài)鏈接庫中的函數(shù),并把返回值放在一個string類中,然后釋放了這個動態(tài)鏈接庫。釋放后,輸入ip的內(nèi)容。
 
根據(jù)函數(shù)的定義,我們知道函數(shù)是“值返回”的,所以,函數(shù)返回時,一定會調(diào)用拷貝構(gòu)造函數(shù),又根據(jù)string類的內(nèi)存共享機制,在主程序中變量ip是和函數(shù)內(nèi)部的那個靜態(tài)string變量共享內(nèi)存(這塊內(nèi)存區(qū)是在動態(tài)鏈接庫的地址空間的)。而我們假設(shè)在整個主程序中都沒有對ip的值進行修改過。那么在當(dāng)主程序釋放了動態(tài)鏈接庫后,那個共享的內(nèi)存區(qū)也隨之釋放。所以,以后對ip的訪問,必然做造成內(nèi)存地址訪問非法,造成程序crash。即使你在以后沒有使用到ip這個變量,那么在主程序退出時也會發(fā)生內(nèi)存訪問異常,因為程序退出時,ip會析構(gòu),在析構(gòu)時就會發(fā)生內(nèi)存訪問異常。
 
內(nèi)存訪問異常,意味著兩件事:1)無論你的程序再漂亮,都會因為這個錯誤變得暗淡無光,你的聲譽也會因為這個錯誤受到損失。2)未來的一段時間,你會被這個系統(tǒng)級錯誤所煎熬(在C++世界中,找到并排除這種內(nèi)存錯誤并不是一件容易的事情)。這是C/C++程序員永遠的心頭之痛,千里之堤,潰于蟻穴。而如果你不清楚string類的這種特征,在成千上萬行代碼中找這樣一個內(nèi)存異常,簡直就是一場噩夢。
 
備注:要改正上述的Bug,有很多種方法,這里提供一種僅供參考:
string ip = (*pFun)(“host1”).cstr();
 
3、    后記
 
文章到這里也應(yīng)該結(jié)束了,這篇文章的主要有以下幾個目的:
 
1)    向大家介紹一下寫時才拷貝/內(nèi)存共享這種技術(shù)。
2)    以STL中的string類為例,向大家介紹了一種設(shè)計模式。
3)    在C++世界中,無論你的設(shè)計怎么精巧,代碼怎么穩(wěn)固,都難以照顧到所有的情況。智能指針更是一個典型的例子,無論你怎么設(shè)計,都會有非常嚴(yán)重的BUG。
4)    C++是一把雙刃劍,只有了解了原理,你才能更好的使用C++。否則,必將引火燒身。如果你在設(shè)計和使用類庫時有一種“玩C++就像玩火,必須千萬小心”的感覺,那么你就入門了,等你能把這股“火”控制的得心應(yīng)手時,那才是學(xué)成了。

相關(guān)文章

  • c語言數(shù)據(jù)結(jié)構(gòu)與算法之順序表的定義實現(xiàn)詳解

    c語言數(shù)據(jù)結(jié)構(gòu)與算法之順序表的定義實現(xiàn)詳解

    這篇文章主要介紹了c語言數(shù)據(jù)結(jié)構(gòu)與算法之順序表的定義實現(xiàn)詳解,用順序存儲的方式實現(xiàn)線性表順序存儲,把邏輯上相鄰的元素存儲在物理位置上也相鄰的存儲單元中,元素之間的關(guān)系由存儲單元的鄰接關(guān)系來體現(xiàn),需要的朋友可以參考下
    2023-08-08
  • C++運行時類型識別與轉(zhuǎn)換實現(xiàn)方法

    C++運行時類型識別與轉(zhuǎn)換實現(xiàn)方法

    運行時類型識別可能被認(rèn)為是C++中一個”次要“的特征,當(dāng)程序員在編程過程中陷入非常困難的境地時,實用主義將會幫助他走出困境
    2022-10-10
  • C++ 數(shù)據(jù)結(jié)構(gòu)之布隆過濾器

    C++ 數(shù)據(jù)結(jié)構(gòu)之布隆過濾器

    這篇文章主要介紹了C++ 數(shù)據(jù)結(jié)構(gòu)之布隆過濾器的相關(guān)資料,需要的朋友可以參考下
    2017-06-06
  • C++無法打開源文件bits/stdc++.h的問題

    C++無法打開源文件bits/stdc++.h的問題

    這篇文章主要介紹了C++無法打開源文件bits/stdc++.h的問題以及解決方案,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2023-08-08
  • c語言實現(xiàn)整蠱朋友小程序(附源碼)

    c語言實現(xiàn)整蠱朋友小程序(附源碼)

    這篇文章主要給大家介紹了關(guān)于c語言實現(xiàn)整蠱朋友小程序的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-02-02
  • 獲取一個文件行數(shù)的方法

    獲取一個文件行數(shù)的方法

    獲取一個文件行數(shù)的方法,需要的朋友可以參考一下
    2013-03-03
  • 利用C語言實踐OOP,以及new,delete的深入分析

    利用C語言實踐OOP,以及new,delete的深入分析

    本篇文章是對用C語言實踐OOP,new,delete進行了詳細的分析介紹,需要的朋友參考下
    2013-05-05
  • C語言數(shù)組超詳細講解中篇三子棋

    C語言數(shù)組超詳細講解中篇三子棋

    數(shù)組是一組有序的數(shù)據(jù)的集合,本篇將帶你結(jié)合數(shù)組來實現(xiàn)三子棋小游戲,上手實練更快的能夠掌握數(shù)組使用,感興趣的朋友來看看吧
    2022-04-04
  • C++簡單實現(xiàn)Dijkstra算法

    C++簡單實現(xiàn)Dijkstra算法

    這篇文章主要為大家詳細介紹了C++簡單實現(xiàn)Dijkstra算法,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2020-05-05
  • C語言入門篇--充分理解操作符

    C語言入門篇--充分理解操作符

    本篇文章是基礎(chǔ)篇,適合c語言剛?cè)腴T的朋友,本文主要介紹了c語言的操作符基礎(chǔ)理論,希望可以幫助大家快速入門c語言的世界,更好的理解c語言
    2021-08-08

最新評論