STL string 類型探祕

一【概述】

在C語言中,我們一般用char數組來定義一個字符串,那麼既然是數組我們往往需要提前判斷字符串的最大長度,可問題是誰也不知道這最大長度究竟有多長,而且這也是很多編程BUG的根源。那麼在C++標準庫中,我們可以通過string類型來定義一個字符串,就不必考慮數組長度等這麼多底層的東西,只需要考慮業務功能的實現就可以了。

雖然有了現成的string類型,可是string這個類是如何實現的呢?是不是僅僅對一個傳統的char數組進行了一個簡簡單單的類封裝呢?這個字符數組的末尾還是以’\0’結尾的嗎?它的數組長度是如何定義的?string類型的內存是如何管理的?我們沒必要重複發明輪子,但是作爲一個職業的代碼玩家應該懂得輪子是如何發明的!

這兒以g++4.1.0的標準庫的string實現爲例。String其實是basic_string<char>,所以本質上是basic_string。basic_string另外一種常用的形式是basic_string<wchar_t>,這個一看就知道是用來處理寬字符串的。那麼下面先看下basic_string一般情況下的內存結構的示意圖:

 

該圖顯示出在每個字符串的尾部一般都會有一個'\0'結尾,這個跟傳統的字符串是一樣的,但是它的頭部會有一個Rep對象,該對象的作用在於記錄該basic_string對象的描述信息,如:字符串的長度、basic_string對象目前能包容的最大字符串長度等。這兒得分清一個概念就是字符串長度不等於字符串佔用的內存大小,因爲在寬字符情況下,一個字符的佔用空間往往大於1個字節。當然,我們還注意到basic_string分配的內存空間往往要比比實際需要的內存空間大。這個是出於多種原因,如:字節對齊、儘量是虛擬內存頁大小的整數倍、在字符串長度增長的時候不必頻繁增加內存空間,等等。

二【類設計】

好了,到這兒應該對basic_string以及常用的string有個大概的印象了。那麼下面我們在看一下basic_string的具體實現了。如圖:

 

basic_string會有3個模板參數_CharT用來定義字符類型的,目前就兩種:char和wchar_t。_Traits這個參數封裝了所有_CharT類型特性以及相關操作。_Alloc設定了內存配置器的類型,它主要負責頂層的內存分配工作的。_Traits和_Alloc都有默認值,通常情況下,我們不用去管他。再看_M_dataplus成員變量,該變量是_Alloc_hider類型,而真正指向實際字符串的是它的一個字符串指針變量_M_p。每個字符串的尾端都會有一個terminal(結尾標識),一般是’\0’,這樣當我們調用c_str()函數的時候它就直接將_M_p指針返回就可以了。還有一個函數data(),網上說它在返回字符串的時候不含有’\0’,不過我從這個版本的源碼來看它跟c_str()的功能是完全一樣的。

_Rep繼承自_Rep_base,該類負責對字符串的內存進行管理,_M_length定義了當前字符串的長度,而_M_capacity定義了當前分配的內存所容納的最長字符串大小。_M_refcount定義了該字符串被引用的次數(因爲爲了提高性能, basic_string使用了COPY-ON-WRITE技術,所以一塊內存空間可能被多次引用)。而_Rep中的3個變量都是靜態變量,它們也定義了字符串類型的一般特性,如:string類型所能定義的最大字符串長度、字符串結尾標識字符、空字符串的內存空間(這也意味着多個空字符串用的其實都是同一分內存拷貝)。

三【內存分配策略】

下面我們關注一下下字符串的內存分配策略。該版本的STL默認通過new_allocator來爲string類型分配內存,源碼如下:

      const size_type __pagesize = 4096;
      const size_type __malloc_header_size = 4 * sizeof(void*);

      // The below implements an exponential growth policy, necessary to
      // meet amortized linear time requirements of the library: see
      // http://gcc.gnu.org/ml/libstdc++/2001-07/msg00085.html.
      // It's active for allocations requiring an amount of memory above
      // system pagesize. This is consistent with the requirements of the
      // standard: http://gcc.gnu.org/ml/libstdc++/2001-07/msg00130.html
      if (__capacity > __old_capacity && __capacity < 2 * __old_capacity)
	__capacity = 2 * __old_capacity;

      // NB: Need an array of char_type[__capacity], plus a terminating
      // null char_type() element, plus enough for the _Rep data structure.
      // Whew. Seemingly so needy, yet so elemental.
      size_type __size = (__capacity + 1) * sizeof(_CharT) + sizeof(_Rep);

      const size_type __adj_size = __size + __malloc_header_size;
      if (__adj_size > __pagesize && __capacity > __old_capacity)
	{
	  const size_type __extra = __pagesize - __adj_size % __pagesize;
	  __capacity += __extra / sizeof(_CharT);
	  // Never allocate a string bigger than _S_max_size.
	  if (__capacity > _S_max_size)
	    __capacity = _S_max_size;
	  __size = (__capacity + 1) * sizeof(_CharT) + sizeof(_Rep);
	}

 

假設原有字符串長度爲__old_capacity,那麼當字符串的長度__capacity > __old_capacity同時__capacity < 2*__old_capacity時,請求字符串的長度將被調整爲__capacity = 2*__old_capacity。這也是爲了當字符串長度不斷增長時,避免因頻繁的分配內存空間而導致性能下降。那麼,這時爲string類型分配的內存空間大小爲:長度爲__capacity的字符串空間+結尾標識字符+_Rep對象空間大小。最後,basic_string還會考慮以虛擬頁大小爲模向上取整(這個頁大小不一定是實際的頁大小,默認一般是4096)。

在確定了大小之後,我們就通過operator new來分配內存空間,接着通過placement new來初始化該內存塊。最後再設置下_Rep對象中的成員變量(主要是_M_length_M_refcount)和字符串結尾標識。這樣,字符串的內存構造就算結束了。

另外,由於basic_string採用了寫時拷貝技術(COPY-ON-WRITE),所以有時會等到真正需要的時候纔會去分配內存。比如:

 

string str1 = "Apple";
string str2 = str1;\\str2 與 str1共用同一份內存拷貝
str2 = "Orange"; \\str2 與 str 1各自使用不同的內存拷貝


str2在初始化時直接就與str1共用同一份內存拷貝,可是當再次給str2賦另外一個值的時候,string會爲str2分配一塊獨立的內存空間。這樣做確實提高了程序的性能,避免了某些情況下無謂的性能開銷,可是在多線程運行的情況下,這樣做也有可能帶來string對象的讀寫同步問題。

四【結束語】

唉,STL真是考慮全面啊,搞個字符串都能整這麼多東西出來,很多細節性的東西都想到了,不過越是複雜的東西越是不可控,學習成本就越高,難怪Linus會說:C++ is a horrible language

另外:侯捷的《STL源碼分析》一書中對traitsallocator都有詳細的論述,不過在STL源代碼中我沒有找到該書所講的那個默認配置器。根據書中描述該默認配置器當請求的內存大小大於128字節時通過new分配,而當小於該值時,又會通過內存池分配。這個,額,應該是新版本里面給去掉了吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章