STL源碼學習----內存管理

1, allocator
SGI STL 的頭文件defalloc.h中有一個符合標準的名爲allocator的內存分配器,它只是簡單地將::operator new 和::operator delete做了一層薄薄的封裝。在SGI STL的容器和算法部分從來沒有用到這個內存分配器。在此略過。

2, STL 的內存分配策略

首先簡要介紹一下STL中對內存分配的規劃

當用戶用new構造一個對象的時候,其實內含兩種操作:1)調用::operator new申請內存;2)調用該對象的構造函數構造此對象的內容

當用戶用delete銷燬一個對象時,其實內含兩種操作:1)調用該對象的析構函數析構該對象的內容;2)調用::operator delete釋放內存

SGI STL中對象的構造和析構由::construct()和::destroy()負責;內存的申請和釋放由alloc:allocate()和alloc:deallocate()負責;此外,SGI STL還提供了一些全局函數,用來對大塊內存數據進行操作。

上一段提到的三大模塊分別由stl_construct.h stl_alloc.h stl_uninitialized.h 負責

下面的各小節分別分析這三大模塊的主要內容

3, 對象的構造和析構工具(stl_construct.h)

stl_construct.h中提供了兩種對象的構造方法,默認構造和賦值構造:

1 template <class _T1, class _T2>
2 inline void _Construct(_T1* __p, const _T2& __value) {
3   new ((void*) __p) _T1(__value);
4 }
5 
6 template <class _T1>
7 inline void _Construct(_T1* __p) {
8   new ((void*) __p) _T1();
9 }

  上面兩個函數的作用是構造一個類型爲T1的對象,並由作爲參數的指針p返回。
其中的new (_p) _T1(_value); 中使用了placement new算子,它的作用是通過拷貝的方式在內存地址_p處構造一個_T1對象。(placement new能實現在指定的內存地址上用指定類型的構造函數來構造一個對象)。
在對象的銷燬方面,stl_construct.h也提供了兩種析構方法:

1 template <class _Tp>
2 inline void _Destroy(_Tp* __pointer) {
3   __pointer->~_Tp();
4 }
5 
6 template <class _ForwardIterator>
7 inline void _Destroy(_ForwardIterator __first, _ForwardIterator __last) {
8   __destroy(__first, __last, __VALUE_TYPE(__first));
9 }

  第一個版本的析構函數接受一個指針,將該指針所指的對象析構掉;第二個版本的析構函數接受first和last兩個迭代器,將這兩個迭代器範圍內的對象析構掉。

  在第二個版本的destroy函數裏面,運用了STL中慣用的traits技法,traits會得到當前對象的一些特性,再根據特性的不同分別對不同特性的對象調用相應的方法。在stl_construct.h中的destroy中,STL會分析迭代器所指對象的has_trivial_destructor特性的類型(只有兩種:true_type和false_type),如果是true_type,STL就什麼都不做;如果是false_type,就會調用每個對象的析構函數來銷燬這組對象。

  除此之外,stl_construct還爲一些基本類型的對象提供了特化版本的destroy函數,這些基本類型分別是char, int, float, double, long。當destroy的參數爲這些基本類型時,destroy什麼都不做。

4,內存空間管理工具alloc
我想以自底向下的順序介紹一下STL的allocator。首先說說STL內建的兩種分配器,然後介紹STL如何封裝這兩種分配器對外提供統一的接口,最後用一個vector的例子看看容器如何使用這個allocator。

4.1 兩種內存分配器

4.1.1 __malloc_alloc_template分配器

該分配器是對malloc、realloc以及free的封裝:

 1   static void* allocate(size_t __n)
 2   {
 3     void* __result = malloc(__n);
 4     if (0 == __result) __result = _S_oom_malloc(__n);
 5     return __result;
 6   }
 7 
 8   static void deallocate(void* __p, size_t /* __n */)
 9   {
10     free(__p);
11   }
12 
13   static void* reallocate(void* __p, size_t /* old_sz */, size_t __new_sz)
14   {
15     void* __result = realloc(__p, __new_sz);
16     if (0 == __result) __result = _S_oom_realloc(__p, __new_sz);
17     return __result;
18   }

  當調用malloc和realloc申請不到內存空間的時候,會改調用oom_malloc()和oom_realloc(),這兩個函數會反覆調用用戶傳遞過來的out of memory handler處理函數,直到能用malloc或者realloc申請到內存爲止。如果用戶沒有傳遞__malloc_alloc_oom_handler,__malloc_alloc_template會拋出__THROW_BAD_ALLOC異常。

  所以,內存不足的處理任務就交給類客戶去完成。

  4.1.2 __default_alloc_template分配器

  這個分配器採用了內存池的思想,有效地避免了內碎片的問題(順便一句話介紹一下內碎片和外碎片:內碎片是已被分配出去但是用不到的內存空間,外碎片是由於大小太小而無法分配出去的空閒塊)。

  如果申請的內存塊大於128bytes,就將申請的操作移交__malloc_alloc_template分配器去處理;如果申請的區塊大小小於128bytes時,就從本分配器維護的內存池中分配內存。

  分配器用空閒鏈表的方式維護內存池中的空閒空間,空閒鏈表大概類似於下面的形狀:
這裏寫圖片描述
如圖所示,s_free_list是這些空閒分區鏈的起始地址組成的數組,大小爲16。這16個鏈表中每個鏈表中的空閒空間的大小都是固定的,第一個鏈表的空閒塊大小是8bytes, 依次是16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128bytes。

  另外還有三個指針s_start_free, s_end_free, s_heap_size。它們分別指向整個內存池的起始地址,結束地址和可用空間大小。

分配內存過程:

  1)如果申請的內存空間大於128bytes, 則交由第一個分配器處理

  2)分配器首先將申請內存的大小上調至8的倍數n,並根據n找出其對應的空閒鏈表地址__my_free_list

  3)如果該空閒鏈表中有可用的空閒塊,則將此空閒塊返回並更新__my_free_list,否則轉到4)

  4)到這一步,說明__my_free_list中沒有空閒塊可用了,分配器會按照下面的步驟處理:

a) 試着調用_s_chunk_alloc()申請大小爲n*20的內存空間,注意的是,此時不一定能申請到n*20大小的內存空間

b) 如果只申請到大小爲n的內存空間,則返回給用戶,否則到c)

c) 將申請到的n*x(a中說了,不一定是n*20)內存塊取出一個返回給用戶,其餘的內存塊鏈到空閒鏈表__my_free_list中
_s_chunk_alloc()的具體過程爲:

1)如果_s_start_free和_s_end_free之間的空間足夠分配n*20大小的內存空間,則從這個空間中取出n*20大小的內存空間,更新_s_start_free並返回申請到的內存空間的起始地址,否則轉到2)

2) 如果_s_start_free和_s_end_free之間的空間足夠分配大於n的內存空間,則分配整數倍於n的內存空間,更新_s_start_free,由nobj返回這個整數,並返回申請到的內存空間的起始地址;否則轉到3)

3) 到這一步,說明內存池中連一塊大小爲n的內存都沒有了,此時如果內存池中還有一些內存(這些內存大小肯定小於n),則將這些內存插入到其對應大小的空閒分區鏈中

4) 調用malloc向運行時庫申請大小爲(2*n*20 + 附加量)的內存空間, 如果申請成功,更新_s_start_free, _s_end_free和_s_heap_size,並重新調用_s_chunk_alloc(),否則轉到5)

5) 到這一步,說明4)中調用malloc失敗,這時分配器依次遍歷16個空閒分區鏈,只要有一個空閒鏈,就釋放該鏈中的一個節點,重新調用_s_chunk_alloc()
內存釋放過程:
內存的釋放過程比較簡單,它接受兩個參數,一個是指向要釋放的內存塊的指針p,另外一個表示要釋放的內存塊的大小n。分配器首先判斷n,如果n>128bytes,則交由第一個分配器去處理;否則將該內存塊加到相應的空閒鏈表中。

4.2 對外提供的分配器接口

SGI STL 爲了方便用戶訪問,爲上面提到的兩種分配器包裝了一個接口,該接口如下:

 1 template<class _Tp, class _Alloc>
 2 class simple_alloc {
 3 
 4 public:
 5     static _Tp* allocate(size_t __n)
 6       { return 0 == __n ? 0 : (_Tp*) _Alloc::allocate(__n * sizeof (_Tp)); }
 7     static _Tp* allocate(void)
 8       { return (_Tp*) _Alloc::allocate(sizeof (_Tp)); }
 9     static void deallocate(_Tp* __p, size_t __n)
10       { if (0 != __n) _Alloc::deallocate(__p, __n * sizeof (_Tp)); }
11     static void deallocate(_Tp* __p)
12       { _Alloc::deallocate(__p, sizeof (_Tp)); }
13 };

用戶調用分配器的時候,爲simple_alloc的第二個模板參數傳遞要使用的分配器。

4.3 用戶使用分配器的方式

下面是vector使用STL分配器的代碼

 1 template <class _Tp, class _Alloc>
  //cobbliu 注:STL vector 的基類 
 2 class _Vector_base {  
 3 public:
 4   typedef _Alloc allocator_type;
 5   allocator_type get_allocator() const { return allocator_type(); }
 6 
 7   _Vector_base(const _Alloc&)
 8     : _M_start(0), _M_finish(0), _M_end_of_storage(0) {}
 9   _Vector_base(size_t __n, const _Alloc&)
10     : _M_start(0), _M_finish(0), _M_end_of_storage(0) 
11   {
12     _M_start = _M_allocate(__n);
13     _M_finish = _M_start;
14     _M_end_of_storage = _M_start + __n;
15   }
16 
17   ~_Vector_base() { _M_deallocate(_M_start, _M_end_of_storage - _M_start); }
18 
19 protected:
20   _Tp* _M_start;
21   _Tp* _M_finish;
22   _Tp* _M_end_of_storage;
23 
24   typedef simple_alloc<_Tp, _Alloc> _M_data_allocator;
25   _Tp* _M_allocate(size_t __n)
26     { return _M_data_allocator::allocate(__n); }
27   void _M_deallocate(_Tp* __p, size_t __n) 
28     { _M_data_allocator::deallocate(__p, __n); }
29 };

  我們可以看到vector的基類調用simple_alloc作爲其分配器

5,基本內存處理工具

除了上面的內存分配器之外,STL還提供了三類內存處理工具:uninitialized_copy(), uninitialized_fill()和uninitialized_fill_n()。這三類函數的實現代碼在頭文件stl_uninitialized.h中。

uninitialized_copy()像下面的樣子:

1 template <class _InputIter, class _ForwardIter>
2 inline _ForwardIter
3   uninitialized_copy(_InputIter __first, _InputIter __last,
4                      _ForwardIter __result)
5 {
6   return __uninitialized_copy(__first, __last, __result,
7                               __VALUE_TYPE(__result));
8 }

uninitialized_copy()會將迭代器_first和_last之間的對象拷貝到迭代器_result開始的地方。它調用的__uninitialized_copy(__first, __last, __result,__VALUE_TYPE(__result))會判斷迭代器_result所指的對象是否是POD類型(POD類型是指擁有constructor, deconstructor, copy, assignment函數的類),如果是POD類型,則調用算法庫的copy實現;否則遍歷迭代器_first~_last之間的元素,在_result起始地址處一一構造新的元素。

uninitialized_fill()像下面的樣子:

1 template <class _ForwardIter, class _Tp>
2 inline void uninitialized_fill(_ForwardIter __first,
3                                _ForwardIter __last, 
4                                const _Tp& __x)
5 {
6   __uninitialized_fill(__first, __last, __x, __VALUE_TYPE(__first));
7 }

uninitialized_fill()會將迭代器_first和_last範圍內的所有元素初始化爲x。它調用的__uninitialized_fill(__first, __last, __x, __VALUE_TYPE(__first))會判斷迭代器_first所指的對象是否是POD類型的,如果是POD類型,則調用算法庫的fill實現;否則一一構造。

uninitialized_fill_n()像下面這個樣子:

1 template <class _ForwardIter, class _Size, class _Tp>
2 inline _ForwardIter 
3 uninitialized_fill_n(_ForwardIter __first, _Size __n, const _Tp& __x)
4 {
5   return __uninitialized_fill_n(__first, __n, __x, __VALUE_TYPE(__first));
6 }

uninitialized_fill_n()會將迭代器_first開始處的n個元素初始化爲x。它調用的__uninitialized_fill_n(__first, __n, __x, __VALUE_TYPE(__first))會判斷迭代器_first所指對象是否是POD類型,如果是,則調用算法庫的fill_n實現;否則一一構造。

6,總結
STL的內存分配和迭代器是理解一切容器實現細節的基礎,本文主要粗略地介紹了一下STL中兩種內存分配器的分配機制,沒有涉及很多alloc_traits的內容,關於這部分的內容會在迭代器部分詳細介紹。

7,參考文獻
1)《STL源碼剖析》第二章:空間配置器
2)sgi-stl-3.3 源代碼

轉載自
http://www.cnblogs.com/cobbliu/archive/2012/04/05/2431804.html

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