STL標準庫是開發中的利器,也是開發的寶庫。
這次的源碼分析主要以GNU C++的2.9和4.9版本為例,因為4.9之后代碼重構,核心部分發生了巨大的變化,再次分別分析一下。
以GCC為例的標準庫位置: ....\x.x.x\include\c++\
對于2.9和4.9最大的差別其實就是,2.9主要采用了泛型編程的思想,4.9引入了大量的面向對象編程的思想。
OOP(Object-Oriented Programming 面向對象編程) vs. GP(Generic Programming 泛型編程)
- OOP
- OOP主要的思想是將datas和methods關聯在一起的思想。
也就是數據放在類中,操作數據的方法也是放在類中。(就像我以前舉的一個例子,如果class 貓
身上有毛,那么他必須有一個方法來管理他的毛,也就是舔毛()
這個函數。只需要貓咪.舔毛();
來調用這個函數,就可以管理和操作對應的數據)
- OOP主要的思想是將datas和methods關聯在一起的思想。
- GP
- GP的主要思想是將datas和methods分開
在STL中大量使用到了GP的思想,來實現了數據和算法的分離,那么,算法如何才能操作數據呢,這中間的橋梁就是Iterators(迭代器)了,通過Iterator,算法可以從容器中獲取到需要的數據,同樣也就可以起到操作數據的目的。
為何STL會采用GP的思想呢?其實使用了GP思想,類和類之間的關系不會那么緊密,也就不會產生很強的耦合性,便于不同的成員,協同開發不同的模塊,有助于加快項目開發得效率,大家只需要依據“中間商”Iterator來編寫各自代碼就行了。
- GP的主要思想是將datas和methods分開
對于OOP來說最好的一點就是,方法和數據在同一個類中,那么方法是專門為類所設計的。比較方便能夠管理其中的數據。GP由于數據和方法分離,操作的時候,難免有些數據,不能被這個方法所操作。比如,list 不能使用::sort() 進行排序,那到底是為什么呢?
- 看看::sort()的源碼,發現問題所在
template <class RandomAccessIterator>
inline void sort(RandomAccessIterator first, RandomAccessIterator last)
{
if(first != last)
{
_introsort_loop(first, last, value_type(first), __lg(last-first)*2);
__final_insertion_sort(first, last);
}
}
.....
template <class RandomAccessIterator, class T, class Size>
void __introsort_loop(RandomAccessterator first, RandomAccessIterator last, T*, Size depth_limit)
{
......
RandomAccessIterator cut = __unguarded_partition(first, last, T(__median(*first, *(first + (last - first)/2), *(last - 1))));
//由于此處牽扯到了Iterator的下標運算
//list不是一個連續空間,前后節點之間靠指針相連,所以list 的Iterator不具備下表直接運算的能力,所以,list不能直接使用::sort()來進行排序
//也正是由于這個原因::sort() 只能為RandomAccessIterator來進行排序
......
}
- 那既然如此,在STL中,難道數據不適合就不能使用了,是否有其他方式來使用呢?
- 以max()為例
//標準庫中的兩個函數
template<class T>
inline const T& max(const T& a, const T& b){
return a < b ? b: a;
}
template<class T, class Compare>
inline const T& max(const T& a, const T& b, Compare comp){
return comp(a, b)? b: a;
}
//如何使用
//定義一個依據長度比較大小的函數
bool strLonger(const T& a, const T& b){
return a.size() < s2.size();
}
cout << "max of zoo and hello:"
<< max(string("zoo"), string("hello")) << endl;
cout << "longer of zoo and hello: "
<< max(string("zoo"), string("hello"), strLonger) << endl;
分配器
分配器是容器管理內存的工具,對于容器的效率起著比較重要的作用
在正式開始說allocator之前,先說幾句operator new()和 malloc()以及operator delete() 和free()
在創建對象時,會調用operator new(),而operator new()中分配內存實際也還是調用有C語言的Runtime Library所提供的malloc(),再由系統來分配所需要的內存;銷毀對象時,則會使用operator delete(),而他實際會調用free()。
- vc中的operator new()
void *operator new (size_t size, const std::nothrow_t&)
{
void *p;
while((p = malloc(size)) == 0)
{
_TRY_BEGIN
if(_callnewh(size) == 0) break;
_CATCH(std::bad_alloc) return(0);
_CATCH_END
}
return (p);
}
malloc所分配的內存圖,如上圖所示,其中藍色部分為真正需要的內存。其余部分為系統分配的管理這部分空間的配套內存,其中保存了需要的這塊內存的相關信息
灰色部分為調試模式系統分配的內存空間
根據vc版本,容器主要的使用的是allocator這個分配器的情況
template<class _Ty, class _a = allocator<_Ty> >
class vector{....}
template<class _Ty, class _a = allocator<_Ty> >
class list{....}
template<class _Ty, class _a = allocator<_Ty> >
class deque{....}
template<class _Ty, class _a = allocator<_Ty> >
class set{....}
template <class _Ty>
class allocator{
public:
typedef _SIZT size_type;
typedef _PDFT difference_type;
typedef _Ty _FARQ *pointer;
typedef _Ty value_type;
pointer allocate(size_type _N, const void *){return (_Allocate((difference_type)_N, (pointer)0)); }
void deallocate(void _FAQ *_P, size_type){operator delete(_P); }
}
///.....
//其中_Allocate()如下:
template<class _Ty> inline
_Ty _FARQ*_Allocate(_PDFT _N, _FARQ *){
if (_N < 0) _N = 0;
return (( _Ty _FARQ*) operator new ((_SIZT) _N * sizeof(_Ty)));
}
//如果使用allocator來申請內存
int *p = allocator<int>.allocate(512, (int*)0); //申請空間
allocator<int>().dellocate(p, 512);//釋放空間
由源代碼可以看出,VC分配器實際是通過operator new和delete來調用malloc和free來管理元素的內存
- GNU2.9的allocator也沒有過多的設計,依然是通過::operator new和::operator delete來完成allocate()和deallocate(),但是,在2.9版本中,實際容器使用的并非allocator,而是alloc
template<class _Ty, class _a = alloc >
class vector{....}
template<class _Ty, class _a = alloc >
class list{....}
template<class _Ty, class _a = alloc >
class deque{....}
template<class _Ty, class _a = alloc >
class set{....}
- alloc這個分配器的主要目的是為了減少malloc的調用次數
malloc申請空間時,多余的空間的主要目的是為了free時能夠快速的知道申請的空間到底是多大。而對于容器來說,其中所保存的元素大小是相同的,不需要在每個元素的前頭都記錄空間到底是多大。 - alloc的解決方案:
- 設計了十六條鏈表,每條鏈表都負責對應大小的管理工作
- 元素的大小會被調整到8的倍數,然后在管理,比如,50字節會被調整為56字節
- 第一條鏈表負責8個字節大小元素的部分,第二條鏈表負責16個字節大小元素的部分,第三條負責24個字節大小元素的部分,以此類推,一直到第十六條鏈表,負責管理128字節的元素的部分
- 如果沒有內存,則一次性申請較大的空間,然后將這些空間等分,所以相對于只malloc一次,則只有大空間具有那些額外的空間,而中間等分的部分實際上沒有那么多額外的空間的浪費
那么對于GNU4.5之后還在使用alloc這個分配器嗎?
template<typename _Tp, typename _Alloc = std::allocator<_Tp> >
class vector: protected _Vector_base<_Tp, _Alloc>{....}
#define __allocator_base __gnu_cxx::new_allocator
template<typename _Tp>
class allocator: public __allocator_base<_Tp>
{
.....
}
template<typename _Tp>
class new_allocator
{
...
pointer allocator(size_type __n, const void* = 0){
if(__n > this ->max_size())
std::__throw_bad_alloc();
return static_cast<_Tp*> (::operator new (_n * sizeof(_Tp)));
}
void deallocate(pointer __p, size_type){
::operator delete(__p);
}
...
}
- 在4.9版本以后,gnu的分配器也沒有特殊設計,也是采用直接調用operator new來分配空間
- 之前設計的分配器被放入到了擴充分配器中(extention allocators),其中__pool_alloc就是GNU2.9的alloc,可以
vector<string,__gun::_cxx::__pool_alloc<string> > vec;
來使用
容器結構分類
-
序列式容器(Sequence Container)的衍生關系
- array
(C++2.0)連續空間
- vector
連續空間
- heap
以算法形式呈現(xxx_heap())
- priority_queue
- list
雙向鏈表
- slist
C++2.0中為forward_list,單向鏈表
- deque
分段連續空間
- stack
Container Adapter
- queue
Container Adapter
- stack
- array
-
關聯式容器(Associative Containers)的衍生關系(復合)
- rb_tree
紅黑樹,非公開
- set
- map
- multiset
- multimap
- hashtable
非公開
- hash_set
非標準,C++2.0為unordered_set
- hash_map
非標準,C++2.0為unordered_map
- hash_multiset
非標準,C++2.0為unordered_multiset
- hash_mulitmap
非標準,C++2.0為unordered_multimap
- hash_set
- rb_tree
容器 list
template <class T>
struct __list_node{
typedef void* void_pointer;
void_pointer prev;
void_pointer next;
T data;
};
template<class T, class Alloc = alloc>
class list{
protected:
typedef __list_node<T> list_node;
public:
typedef list_node* link_type;
typedef __list_iterator<T, T&, T*> iterator;
protected:
link_type node;
};
template<class T, class Ref, class Ptr>
struct __list_iterator{
typedef T value_type;
typedef Ptr pointer;
typedef Ref reference;
}
- list為一個循環鏈表(如圖),但是對于迭代器來說,end()獲取到的并非容器中的最后一個元素,而應該是,最后一個元素之后的空元素,所以在list實現的時,可以看到,end()指向了一個灰色的區域,這個區域實際就是end()指向的非容器內元素的區域
- 由于list非連續空間,所以Iterator在++時,如果不作調整,不會默認的移動到下一個不連續空間,所以,為了讓Iterator能夠和指針的用法相似,Iterator一定是一個class
template<class T, class Ref, class Ptr>
struct __list_iterator{
typedef __list_iterator(T, Ref, Ptr> self;
typedef bidirectional_iterator_tag iterator_category;
typedef T value_type;
typedef Ptr pointer;
typedef Ref reference;
typedef __list_node<T>* link_type;
typedef ptrdiff_t difference_type;
link_type nod;
reference operator*() const{
return (*node).data;
}
pointer operator->() const {
return &(operator*());
}
self& operator++(){//前++
node = (link_type)((*node).next); return *this;
}
self operator++(int){//后++,參數實際無意義
self temp = *this; ++*this; return tmp;
}
};
Iterator的設計原則
- 算法(algorithms)在操作容器(Container)中的數據需要通過Iterator知道的信息如下:
- iterator_category:Iterator的性質,例如是否可以雙向查詢
- difference_type:兩個Iterator之間的距離的type(int、unsigned int),決定了容器可以容納多少元素
- value_type:元素本身的type
- reference:引用
- pointer:指針
在Iterator的設計時,必須有這五種associated types
- traits的引入
- 如果Iterator不是一個class的情況,如果這樣的情況,無法從一個指針中獲取以上的幾種類型,那么這時候,需要一個“中介”來去協調這件事,這時候就出現了一個traits的機制
- 這個traits可以區分到底是class設計的Iterator,也能夠區分是指針傳入的Iterator
//traits的設計
template<class I>
struct iterator_traits{
typedef typename I::value_type value_type;
typedef typename I::iterator_category
typedef typename I::difference_type
typedef typename I::pointer
typedef typename I::reference
};
//針對指針的兩種偏特化
template<class T>
struct iterator_traits<T*>{
typedef T value_type;
typedef random_access_iterator_tag iterator_category;
typedef ptrdiff_t difference_type;
typedef T* pointer;
typedef T& reference;
};
template <class T>
struct iterator_traits<const T*>{
typedef T value_type;
typedef random_access_iterator_tag iterator_category;
typedef ptrdiff_t difference_type;
typedef T* pointer;
typedef T& reference;
}
//traits的使用
template<typename I, ....>
void algorithm(......){
typename iterator_traits<I>::value_type v1;
}
根據偏特化,如果傳入的為指針就會自動進入偏特化的部分,那么就根據偏特化來獲取響應信息
-
各式各樣的traits以及對應的頭文件
- type traits : .../c++/type_traits
- iterator traits: .../c++/bits/stl_iterator.h
- char traits: .../c++/bits/char_traits.h
- allocator traits:.../c++/bits/alloc_traits.h
- pointer traits: .../c++/bits/ptr_traits.h
- array traits:.../c++/bits/array.h
容器Vector
- vector根據三個指針就可以控制全部內容 iterator start;、 iterator finish;、iterator end_of_storage;
其中finish指向最后一個元素之后的位置。
template <class T, class Alloc = alloc>
class vector
{
public:
typedef T value_type;
typedef value_type* iterator;
typedef value_tyle& reference;
typedef size_t size_type;
protected:
iterator start;
iterator finish;
iterator end_of_storage;
public:
iterator begin(){return start;}
iterator end() {return finish;}
size_type size() const{
return size_type(end() - begin());
}
size_type capacity() const {
return size_type(end_of_storage - begin());
}
bool empty() const {
return begin() == end();
}
reference operator[](size_type n){return *(begin() + n); }
reference front() {return *begin();}
reference back(){ return *(end() - 1); }
}
- 二倍成長
-
對于內存來說沒辦法實現原地擴充,因為前后都可能存在著其他程序的數據,如果擴充,意味著會要影響到其他程序,并且操作系統也不允許這樣干。那么對于vector來說,hi如何來實現擴充的呢?那么再擴充的時候,需要在內存的其他區域找到空間,在新找到的空間進行擴充完成后,再將數據復制到新開辟的空間中。而且每次增長的空間都是以兩倍作為基準。
vector的內存圖
-
存入元素和兩杯增長的代碼
void push_back()(const T& x)
{
if(finish != end_of_storage){//尚有備用空間
construct(finish, x);
++finish;
}
else{
insert_aux(end(), x);
}
}
template<class T, class Alloc>
void vector<T, Alloc>::insert_aux(iterator position, const T& x){
if(finish != end_of_storage){//空間夠用
//在備用空間起始處建一個元素,并以vector最后一個元素為其初值
construct(finish, *(finish - 1);
++finish;
T x_copy = x;
copy_backward(postion, finish - 2, finish - 1);
*postion = x_copy;
}
else{ //空間不夠用
const size_type old_size = size();
const size_type len = old_size != 0? 2*old_size: 1;
iterator new_start = data_allocator::allocate(len);
//以上分配原則:剮原大小為0,分配1;不為0,分配原大小的兩倍;前半段用來放置原數據,后半段用來放置新數據
iterator new_finish = new start;
try{
//將原vector的內容拷貝到新的vector
new_finish = uninitialized_copy(start, position, new_start);
construnct(new_finish, x);//為新元素設置初值x
++new_finish;
//拷貝安插點后的原內容
new_finish = uninitialized_copy(postion, finish, new_finish);
}
catch(...){
destory(new_start, new_finish);
data_allocator::deallocate(new_start, len);
throwl
}
//析構并釋放元vector
destory(begin(), end());
//調整迭代器,指向新的vector
deallocate();
start = new_start;
finish = new_finish;
end_of_storage = new_start + len;
}
}
- GNU4.9之后的結構
容器array
- 沒有ctor,沒有dtor
template<typename _Tp, std::size_t _Nm>
struct array{
typedef _Tp;
typedef _Tp*;
typedef value_type*;
value_type _M_instance[_Nm? _Nm: 1];
iterator begin(){
return iterator(&_M_instance[0]);
}
iterator end(){
return iterator(&_M_instance[_Nm]);
}
}
forward_list
- 單向鏈表,具體可以參考list(雙向鏈表)