出差的這段時間想再次好好地學習了一下STL。接下來的幾篇博文就自己閱讀STL源碼的一些個人理解分享,希望跟同行多多交流,有欠佳的地方還望各位多多指教。
STL中諸多容器和算法都要涉及到向系統申請和釋放內存,所以先讀讀C++的內存管理----C++稱其為allocator
1, default 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