您好,登錄后才能下訂單哦!
之前的博客已經給出了如何自己定義一個string類,以及其內部應該有的操作,今天就讓我們根據STL庫中給出的string來看看,它重要的寫實拷貝實現和一點點讀時拷貝(也是寫時拷貝)
1、寫時拷貝(copy-on-write)
class String { public: String(const String &str) :_pData(NULL) { String temp(str._pData); swap(_pData,temp._pData); } private: char *_pData; } void test() { String s1("hello world");//構造 String s2(s1);//拷貝構造 }
這里面實現的是用空間換時間的一種方法,定義極其簡單,然而大神們寫出來的STL庫中的string是更為精巧的。就是應用了寫實拷貝的技術,防止淺拷貝發生,并且還省了空間,
那么問題來了????
Q:什么是寫時拷貝呢?
A:寫時拷貝就是一種拖延戰術,當你真正用到的時候才去給它開辟空間,不然它只是看起來存在,實際上只是邏輯上的存在,這種方法在STL的string中體現的很明顯。
由于string類是用char* 實現的,其內存都是在堆上開辟和釋放的。堆上的空間利用要很小心,所以當你定義一個sring類的對象,并且想對這個對象求地址,其返回值是const char*類型,onlyread屬性哦,如果還想對該地址的內容做什么改變,只能通過string給的方法去修改。
舉個栗子:
#include <iostream> #include <string> using namespace std; int main() { string s1("再來一遍:hello world"); string s2(s1); //c++方式打印一個字符串的地址!!!!! //static_cast---c++中的強制類型轉換,不檢查 //string的c_str()方法返回值是const char * cout<<static_cast<const void *>(s1.c_str())<<endl; cout<<static_cast<const void *>(s2.c_str())<<endl; //就讓我們再來復習一下c語言是如何打印一個字符串的地址 //是不是看起來超簡單,~~~~(>_<)~~~~我也這么覺得 printf("%x\n",s1.c_str()); printf("%x\n",s2.c_str()); }
(vs2010版本)
結果是不是和你想的不一樣。。。。(明明應該不變的說~)
vc 6.0版本下:s1,s2的地址是一樣的。這里就不進行截屏了,如果有興趣的同學,下去可以試試哈~
那么當對s1,s2進行修改時是怎么樣的呢
s1[0]='h'; s2[0]='w';
(vs2010版本)
VC6.0版本下:s1,s2的地址不一樣(同vs2010版本)
所以我們得出的結論是:
當對string對象只進行拷貝構造時,發生的是寫時拷貝(假拷貝),只有對其對象進行修改時(有寫的操作),才對其對象另外開辟空間,進行修改。
要想達到這樣的效果,在一定程度上節省了空間。
必須做到兩點:內存的共享,寫時拷貝。
(1)copy-on-write的原理?
“引用計數”,程序猿就是這般機智~~~~
當對象s1實例化,調用構造,引用計數初始化=1;
當有對象對s1進行拷貝時,s1的引用計數+1;
當有對象是由s1拷貝來的或者是s1自身進行析構是,s1的引用計數進行-1;
當有對象是由s1拷貝來的或者是s1自身需要修改時,進行真拷貝,并且引用計數-1;
當引用計數==0的時候,進行真正的析構。
(2)引用計數應該如何設計在?
關于引用計數的實現,你是不是也有這樣的疑惑呢?
當類的對象之間進行共享時,引用計數也是共享的
當類中的對象從公共中脫離出來,引用計數就是它自己的了。
那么如何做到從獨立--->共享--->獨立的呢???
如果你想將引用計數當做String類的成員變量,那么什么樣的類型適合它呢?
int _count; 那么每個對象的實例化都擁有一個自己的引用計數,無法實現共享
class String { public: String(pData=NULL) :_pData(new char[strlen(pData)+1]) ,_count(1) { strcpy(_pData,pData); } ~String() { if(--_count==0) { delete []_pData; } } String(String &str) :_pData(str._pData) { str._count++; _count=str._count; } private: char *_pData; int _count; }; string s1="hello world"; string s2(s1); //s1構造,s2拷貝構造:s1和s2指向同一空間,s1和s2的_count都變成2 //當s2先析構,s2的_count--變成1,不釋放 //當s1析構時,s1的_count--變成1,不釋放 //造成內存泄露
static int _pCount;那么每個對象的實例化都擁有這唯一的一個引用計數,共享范圍過大
class String { public: String(pData=NULL) :_pData(new char[strlen(pData)+1]) { _count=1; strcpy(_pData,pData); } ~String() { if(--_count==0) { delete []_pData; } } String(String &str) //不加const,不然底下的淺拷貝會出錯 :_pData(str._pData) { str._count++; } private: char *_pData; static int _count; //靜態的成員變量要在類外進行初始化 }; int String::_count=0; string s1="hello world"; string s2(s1); string s3("error"); //s1構造,s2拷貝構造:s1和s2指向同一空間,_count都變成2 //s3構造,_count變成1 //當s3先析構,_count--變成0,釋放 //s1,s2造成內存泄露
int *_pCount;可以實現引用計數。
class String { public: String(pData=NULL) :_pData(new char[strlen(pData)+1]) ,_pCount(new int(1)) { strcpy(_pData,pData); } ~String() { if(--(*_pCount)==0) { delete []_pData; delete _pCount; } } String& operator=(const String *str) { if(_pData!=str._pData) { if(--(*_pCount)==0) { delete _pCount; delete []_pData; } (*str._pCount)++; _pCount=str._pCount; _pData=str._pData; } return *this; } String(String &str) //不加const,不然底下的淺拷貝會出錯 :_pData(str._pData) ,_pCount(str._pCount) { (*str._pCount)++; } private: char *_pData; int *_pCount; };
這些字符串都是在堆上開辟的,那么引用計數也可以在堆上開辟,要從邏輯上,看引用計數是個指針,存次數,從物理上看,引用計數應該和字符指針放在一起,便于管理。讓數據相同的對象都可以共享同一片內存。
綜上,引用計數的設計如圖:
(3)引用計數什么時候需要共享呢?
情況1:string s2(s1); //s2拷貝自s1,即s2中的數據和s1的一樣
情況2:string s2; s2=s1;//s2的數據由s1賦值而來,即s2中的數據和s1的一樣
綜上所述:
string類中的拷貝構造和賦值運算符重載需要引用計數
(4)什么情況下需要進行寫時拷貝
對內容有修改時
(5)c++版實現代碼
class String { private: char *_pData; //引用計數存在于_pData[-1] public: //構造函數 String(pData=NULL) :_pData(new char[strlen(pData)+1+sizeof(int)]) { //強轉在頭上4個字節存放引用計數的值 (*(int *)_pData)=1; //恢復其字符串的長度 _pData+=4; strcpy(_pData,pData); } //拷貝構造 String(const String&str) :_pData(str. _pData) { //(*(--(int *)str._pData)) ++; //這個版本是錯的,大家看看錯在哪里?可以留言告訴我哦 (*(--(int*)_pData-1)++; } //賦值運算符重載 String& operator=(const String &str) { if(_pData!=str._pData) { if(--(*(--(int*)_pData-1)==0) { _pData-=4; delete []_pData; } else { _pData=str._pData; (*(--(int*)_pData-1)++; } } return *this; } //析構函數 ~String() { if(--(*(--(int*)_pData-1)==0) { _pData-=4; delete []_pData; } } };
2、讀時拷貝(copy-on-read)
當C++的STL庫中的string被這么利用時:
string s1="hello world"; long begin = getcurrenttick(); for(size_t i=0;i<s1.size();i++) { cout<<s1[i]<<endl; } cout<<getcurrenttick()-begin<<endl; //你會發現這樣的時間和修改內容進行的寫時拷貝的時間一樣長 //這是因為string中對于operator[]無法自主進行判斷 //client是進行讀還是寫,所以一律按寫考慮
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。