(Boolan) C++ STL與泛型編程——容器1

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 貓身上有毛,那么他必須有一個方法來管理他的毛,也就是舔毛()這個函數。只需要貓咪.舔毛();來調用這個函數,就可以管理和操作對應的數據)
  • GP
    • GP的主要思想是將datas和methods分開
      在STL中大量使用到了GP的思想,來實現了數據和算法的分離,那么,算法如何才能操作數據呢,這中間的橋梁就是Iterators(迭代器)了,通過Iterator,算法可以從容器中獲取到需要的數據,同樣也就可以起到操作數據的目的。
      為何STL會采用GP的思想呢?其實使用了GP思想,類和類之間的關系不會那么緊密,也就不會產生很強的耦合性,便于不同的成員,協同開發不同的模塊,有助于加快項目開發得效率,大家只需要依據“中間商”Iterator來編寫各自代碼就行了。

對于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所分配的內存圖

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的解決方案:
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);
}
    ...
}
分配器的UML
  • 在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
  • 關聯式容器(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

容器 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;
}
UML
內存關系示意圖
  • 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;
    }
};
4.9版本list的UML

Iterator的設計原則

算法要求這幾項的類型必須指定出來
  • 算法(algorithms)在操作容器(Container)中的數據需要通過Iterator知道的信息如下:
    1. 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之后的結構
UML

容器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(雙向鏈表)
UML
內存示意圖
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,182評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,489評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,290評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,776評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,510評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,866評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,860評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,036評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,585評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,331評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,536評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,058評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,754評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,154評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,469評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,273評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,505評論 2 379

推薦閱讀更多精彩內容