尺度C++類string的Copy-On-Write技巧。本站提示廣大學習愛好者:(尺度C++類string的Copy-On-Write技巧)文章只能為提供參考,不一定能成為您想要的結果。以下是尺度C++類string的Copy-On-Write技巧正文
尺度C++類std::string的內存同享和Copy-On-Write技巧 陳皓
1、概念
Scott Meyers在《More Effective C++》及第了個例子,不知你能否還記得?在你還在上學的時刻,你的怙恃要你不要看電視,而去溫習作業,因而你把本身關在房間裡,做出一副正在溫習作業的模樣,其實你在干著其余諸如給班上的某位女生寫情書之類的事,而一旦你的怙恃出來在你房間要檢討你能否在溫習時,你才真正撿起教材看書。這就是“遷延戰術”,直到你非要做的時刻才去做。
固然,這類工作在實際生涯中時常常會失事,但其在編程世界中搖身一變,就成了最有效的技巧,正如C++中的可以到處聲明變量的特色一樣,Scott Meyers推舉我們,在真正須要一個存儲空間時才去聲明變量(分派內存),如許會獲得法式在運轉時最小的內存花消。履行到那才會去做分派內存這類比擬耗時的任務,這會給我們的法式在運轉時有比擬好的機能。必竟,20%的法式運轉了80%的時光。
固然,遷延戰術還其實不只是如許一品種型,這類技巧被我們普遍地運用著,特殊是在操作體系傍邊,當一個法式運轉停止時,操作體系其實不會急著把其消除出內存,緣由是有能夠法式還會立時再運轉一次(從磁盤把法式裝入到內存是個很慢的進程),而只要當內存不敷用了,才會把這些還駐留內存的法式清出。
寫時才拷貝(Copy-On-Write)技巧,就是編程界“懶散行動”——遷延戰術的產品。舉個例子,好比我們有個法式要寫文件,赓續地依據收集傳來的數據寫,假如每次fwrite或是fprintf都要停止一個磁盤的I/O操作的話,都的確就是機能上偉大的喪失,是以平日的做法是,每次寫文件操作都寫在特定年夜小的一塊內存中(磁盤緩存),只要當我們封閉文件時,才寫到磁盤上(這就是為何假如文件不封閉,所寫的器械會喪失的緣由)。更有甚者是文件封閉時都不寫磁盤,而一向比及關機或是內存不敷時才寫磁盤,Unix就是如許一個體系,假如非正常加入,那末數據就會喪失,文件就會破壞。
呵呵,為了機能我們須要冒如許年夜的風險,還好我們的法式是不會忙得忘了還有一塊數據須要寫到磁盤上的,所以這類做法,照樣很有需要的。
2、尺度C++類std::string的Copy-On-Write
在我們常常應用的STL尺度模板庫中的string類,也是一個具有寫時才拷貝技巧的類。C++曾在機能成績上被普遍地質疑和責備過,為了進步機能,STL中的很多類都采取了Copy-On-Write技巧。這類偷懶的行動切實其實使應用STL的法式有著比擬高要機能。
這裡,我想從C++類或是設計形式的角度為列位揭開Copy-On-Write技巧在string中完成的面紗,以供列位在用C++停止類庫設計時做一點參考。
在講述這項技巧之前,我想簡略地解釋一下string類內存分派的概念。經由過程常,string類中必有一個公有成員,其是一個char*,用戶記載從堆上分派內存的地址,其在結構時分派內存,在析構時釋放內存。由於是從堆上分派內存,所以string類在保護這塊內存上是非分特別當心的,string類在前往這塊內存地址時,只前往const char*,也就是只讀的,假如你要寫,你只能經由過程string供給的辦法停止數據的改寫。
2.1、特征
由表及裡,由理性到感性,我們先來看一看string類的Copy-On-Write的外面特點。讓我們寫下上面的一段法式:
#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結構,然後打印出其寄存數據的內存地址,然後分離修正str1和str2的內容,再查一下其寄存內存的地址。法式的輸入是如許的(我在VC6.0和g++ 2.95都獲得了異樣的成果):
> 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
從成果中我們可以看到,在開端的兩個語句後,str1和str2寄存數據的地址是一樣的,而在修正內容後,str1的地址產生了變更,而str2的地址照樣本來的。從這個例子,我們可以看到string類的Copy-On-Write技巧。
2.2、深刻
在深刻這前,經由過程上述的演示,我們應當曉得在string類中,要完成寫時才拷貝,須要處理兩個成績,一個是內存同享,一個是Copy-On-Wirte,這兩個主題會讓我們發生很多疑問,照樣讓我們帶著如許幾個成績來進修吧:
1、 Copy-On-Write的道理是甚麼?
2、 string類在甚麼情形下才同享內存的?
3、 string類在甚麼情形下觸發寫時才拷貝(Copy-On-Write)?
4、 Copy-On-Write時,產生了甚麼?
5、 Copy-On-Write的詳細完成是怎樣樣的?
喔,你說只需看一看STL中stirng的源碼你便可以找到謎底了。固然,固然,我也是參考了string的父模板類basic_string的源碼。然則,假如你覺得看STL的源碼就似乎看機械碼,並嚴重襲擊你對C++自負心,甚至發生了本身能否懂C++的疑問,假如你有如許的感到,那末照樣持續往下看我的這篇文章吧。
OK,讓我們一個成績一個成績地商量吧,漸漸地一切的技巧細節都邑浮出水面的。
2.3、Copy-On-Write的道理是甚麼?
有必定經歷的法式員必定曉得,Copy-On-Write必定應用了“援用計數”,是的,必定有一個變量相似於RefCnt。當第一個類結構時,string的結構函數會依據傳入的參數從堆上分派內存,當有其它類須要這塊內存時,這個計數為主動累加,當有類析構時,這個計數會減一,直到最初一個類析構時,此時的RefCnt為1或是0,此時,法式才會真實的Free這塊從堆上分派的內存。
是的,援用計數就是string類中寫時才拷貝的道理!
不外,成績又來了,這個RefCnt該存在在哪裡呢?假如寄存在string類中,那末每一個string的實例都有各自的一套,基本不克不及共有一個RefCnt,假如是聲明玉成局變量,或是靜態成員,那就是一切的string類同享一個了,這也不可,我們須要的是一個“平易近主和集中”的一個處理辦法。這是若何做到的呢?呵呵,人生就是一個懵懂後去探知,曉得後和又懵懂的輪回進程。別急別急,在前面我會給你逐個道來的。
2.3.1、 string類在甚麼情形下才同享內存的?
這個成績的謎底應當是顯著的,依據常理和邏輯,假如一個類要用另外一個類的數據,那便可以同享被應用類的內存了。這是很公道的,假如你不消我的,那就不消同享,只要你應用我的,才產生同享。
應用其余類的數據時,不過有兩種情形,1)以其余類結構本身,2)以其余類賦值。第一種情形時會觸發拷貝結構函數,第二種情形會觸發賦值操作符。這兩種情形我們都可以在類中完成其對應的辦法。關於第一種情形,只須要在string類的拷貝結構函數中做點處置,讓其援用計數累加;異樣,關於第二種情形,只須要重載string類的賦值操作符,異樣在個中加上一點處置。
絮聒幾句:
1)結構和賦值的差異
關於後面誰人例程中的這兩句:
string str1 = "hello world";
string str2 = str1;
不要認為有“=”就是賦值操作,其實,這兩條語句等價於:
string str1 ("hello world"); //挪用的是結構函數
string str2 (str1); //挪用的是拷貝結構函數
假如str2是上面的如許情形:
string str2; //挪用參數默許為空串的結構函數:string str2(“”);
str2 = str1; //挪用str2的賦值操作:str2.operator=(str1);
2) 另外一種情形
char tmp[]=”hello world”;
string str1 = tmp;
string str2 = tmp;
這類情形下會觸發內存的同享嗎?想固然的,應當要同享。可是依據我們後面所說的同享內存的情形,兩個string類的聲明和初始語句其實不相符我前述的兩種情形,所以其其實不產生內存同享。並且,C++現有特征也沒法讓我們做到對這類情形停止類的內存同享。
2.3.2、 string類在甚麼情形下觸發寫時才拷貝(Copy-On-Write)?
哦,甚麼時刻會發明寫時才拷貝?很明顯,固然是在同享統一塊內存的類產生內容轉變時,才會產生Copy-On-Write。好比string類的[]、=、+=、+、操作符賦值,還有一些string類中諸如insert、replace、append等成員函數,包含類的析構時。
修正數據才會觸發Copy-On-Write,不修正固然就不會改啦。這就是托延戰術的真理,非到要做的時刻才去做。
2.3.3、Copy-On-Write時,產生了甚麼?
我們能夠依據誰人拜訪計數來決議能否須要拷貝,參看上面的代碼:
If ( RefCnt>0 ) {
char* tmp = (char*) malloc(strlen(_Ptr)+1);
strcpy(tmp, _Ptr);
_Ptr = tmp;
}
下面的代碼是一個設想的拷貝辦法,假如有其余類在援用(檢討援用計數來獲知)這塊內存,那末就須要把更改類停止“拷貝”這個舉措。
我們可以把這個拷的運轉封裝成一個函數,供那些轉變內容的成員函數應用。
2.3.4、 Copy-On-Write的詳細完成是怎樣樣的?
最初的這個成績,我們重要處理的是誰人“平易近主集中”的困難。請先看上面的代碼:
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()
{
_Ptr[_Len+1]--; //援用計數減一
// 援用計數為0時,釋放內存
if (_Ptr[_Len+1]==0) {
delete[] _Ptr;
}
}
哈哈,全部技巧細節完整浮出水面。
不外,這和STL中basic_string的完成細節還有一點點差異,在你翻開STL的源碼時,你會發明其取援用計數是經由過程如許的拜訪:_Ptr[-1],尺度庫中,把這個援用計數的內存分派在了後面(我給出來的代碼是把援用計數分派以了前面,這很欠好),分派在前的利益是當string的長度擴大時,只須要在前面擴大其內存,而不須要挪動援用計數的內存寄存地位,這又節儉了一點時光。
STL中的string的內存構造就像我後面畫的誰人圖一樣,_Ptr指著是數據區,而RefCnt則在_Ptr-1 或是 _Ptr[-1]處。
2.4、臭蟲Bug
是誰說的“有太陽的處所就會有陰郁”?也許我們中的很多人都很科學尺度的器械,以為其是久經考驗,弗成能失足的。呵呵,萬萬不要有這類科學,由於任何設計再好,編碼再好的代碼在某一特定的情形下都邑有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();
3、 跋文
文章到這裡也應當停止了,這篇文章的重要有以下幾個目標:
1) 向年夜家引見一下寫時才拷貝/內存同享這類技巧。
2) 以STL中的string類為例,向年夜家引見了一種設計形式。
3) 在C++世界中,不管你的設計怎樣精致,代碼怎樣穩定,都難以照料到一切的情形。智能指針更是一個典范的例子,不管你怎樣設計,都邑有異常嚴重的BUG。
4) C++是一把雙刃劍,只要懂得了道理,你能力更好的應用C++。不然,勢必惹火燒身。假如你在設計和應用類庫時有一種“玩C++就像玩火,必需萬萬當心”的感到,那末你就入門了,等你能把這股“火”掌握的輕車熟路時,那才是學成了。
最初,照樣應用這個後序,引見一下本身。我今朝從事於一切Unix平台下的軟件研發,重要是做體系級的產物軟件研發,關於下一代的盤算機反動——網格盤算異常地感興致,同於關於散布式盤算、P2P、Web Service、J2EE技巧偏向也很感興致,別的,關於項目實行、團隊治理、項目治理也小有心得,願望異樣和我戰役在“技巧和治理偏重”的戰線上的年青一代,可以或許和我多多地交換。我的MSN和郵件是:[email protected]。