Boolan(博覽網(wǎng))——STL與泛型編程(第七周)

目錄

  1. 源代碼之分布(VC, GCC)

  2. 面向?qū)ο缶幊蹋∣bject-Oriented Programming,OOP) vs. 泛型編程 (Generic Programming,GP)

  3. 閱讀C++標(biāo)準(zhǔn)庫(kù)源碼(Source Code)之必要基礎(chǔ):操作符重載(Operator Overloading)and 模板(Templates)

  4. 分配器(allocators)

  5. 迭代器(Iterator)的設(shè)計(jì)原則和 Iterator Traits 的作用與設(shè)計(jì)

  6. 容器之間的實(shí)現(xiàn)關(guān)系與分類

  7. 深度探索 vector

  8. 深度探索 list

  9. 淺談 array & forward_list

1. 源代碼之分布(VC, GCC)

  • VC:...\include
  • GCC:...\4.9.2\include\C++
    • 原生標(biāo)準(zhǔn)庫(kù):...\include\C++\bits
    • 擴(kuò)充標(biāo)準(zhǔn)庫(kù):...\include\C++\ext

2. 面向?qū)ο缶幊蹋∣bject-Oriented Programming,OOP) vs. 泛型編程 (Generic Programming,GP)

問:為什么 list 不能使用 ::sort() 排序?

答:只有隨機(jī)訪問迭代器才能進(jìn)行上述加減乘除,而 list 在內(nèi)存里面是一個(gè)一個(gè)結(jié)點(diǎn)用指針串起來的,并不是一個(gè)連續(xù)空間,所以它所具備的迭代器是不能夠跳來跳去的,它只能一個(gè)一個(gè)前進(jìn)或后退。
因此標(biāo)準(zhǔn)庫(kù)中的 ::sort() 所需要的迭代器是 list 中提供的迭代器所不能滿足的。

操作符重載扮演了非常重要的角色。

如果需要定制特殊的算法,對(duì)特定“比大小”功能的設(shè)計(jì)就顯得尤為重要。

3. 閱讀C++標(biāo)準(zhǔn)庫(kù)源碼(Source Code)之必要基礎(chǔ):操作符重載(Operator Overloading)and 模板(Templates)

3.1 操作符重載(Operator Overloading)

因?yàn)橹暗墓P記中對(duì)操作符重載已經(jīng)有比較詳細(xì)的介紹,這里就不贅述了,只簡(jiǎn)單的羅列一些要注意的點(diǎn):

當(dāng)一個(gè)重載的運(yùn)算符是成員函數(shù)時(shí), this 綁定到左側(cè)運(yùn)算對(duì)象。成員運(yùn)算符函數(shù)的(顯式)參數(shù)數(shù)量比運(yùn)算對(duì)象的數(shù)量要少一個(gè)。

可以(不能)被重載的運(yùn)算符:

通常情況下,不應(yīng)該重載逗號(hào)、取地址、邏輯與和邏輯或運(yùn)算符。

3.2 模板(Templates)

模板是 C++ 中泛型編程的基礎(chǔ)。一個(gè)模板就是一個(gè)創(chuàng)建類或函數(shù)的藍(lán)圖或者說公式。

模板定義以關(guān)鍵字 template 開始,后跟一個(gè)模板參數(shù)列表(template parameter list),其不能為空。

當(dāng)使用模板時(shí),我們(隱式地或顯式地)指定模板實(shí)參(template argument),將其綁定到模板參數(shù)上。

模板類型參數(shù)前必須使用關(guān)鍵字 classtypename
在模板參數(shù)列表中,這兩個(gè)關(guān)鍵字的含義相同,可以互換使用。

3.2.1 模板類型

主要有類模板,函數(shù)模板和成員模板三種類型:

3.2.1.1 類模板(Class Templates)

編譯器不能為類模板推斷模板參數(shù)類型,因此必須顯式使用。

3.2.1.2 函數(shù)模板(Function Templates)

編譯器自動(dòng)為函數(shù)模板推斷模板參數(shù)類型。

3.2.1.3 成員模板(Member Templates)

成員模板不能是虛函數(shù)。

3.2.2 模板特化(Specialization)

模板的特化就是一個(gè)獨(dú)立的定義,在其中一個(gè)或多個(gè)模板參數(shù)被指定為特定的類型。
注意:特化的本質(zhì)是實(shí)例化一個(gè)模板,而非重載它。因此,特化不影響函數(shù)匹配。

特化

偏特化(個(gè)數(shù)的偏特化和范圍的偏特化):

4. 分配器(allocators)

標(biāo)準(zhǔn)庫(kù) allocator 類定義在頭文件 memory 中,它幫助我們將內(nèi)存分配和對(duì)象構(gòu)造分離開來。它提供一種類型感知的內(nèi)存分配方法,它分配的內(nèi)存是原始的、未構(gòu)造的。

藍(lán)色部分:切實(shí)需要的空間大小
灰色部分:debug mode 模式的空間需求
紅色部分:cookie,幫助編譯器識(shí)別此段空間的內(nèi)容
綠色部分:用來調(diào)整邊界,使空間分配統(tǒng)一化

4.1 VC6 STL 中的分配器設(shè)計(jì)

  • (int*)0:只為了告訴函數(shù)是 int 型
  • typename 后面加()構(gòu)成一個(gè)臨時(shí)對(duì)象
  • VC 中的分配器最終就是調(diào)用 C 中的 malloc 和 free, 沒有任何特殊的設(shè)計(jì)

4.2 BC5 STL 中的分配器設(shè)計(jì)

4.3 G2.9 STL 中的分配器設(shè)計(jì)(侯大師推薦!可讀性非常高!)

此處的設(shè)計(jì)并未被使用。

設(shè)計(jì)目的:減少額外開銷(避免之前那種用cookie包起來的大量額外開銷)

4.4 G4.9 STL 中的分配器設(shè)計(jì)

又用回傳統(tǒng)方式了-。-

若要使用 G2.9 中設(shè)計(jì)的 alloc,就要像用例中那樣。

5. 迭代器(Iterator)的設(shè)計(jì)原則和 Iterator Traits 的作用與設(shè)計(jì)

5.1 迭代器的設(shè)計(jì)原則

不論是泛型思維或 STL 的實(shí)際運(yùn)用,迭代器都扮演者重要的角色。 STL 的中心思想在于:將數(shù)據(jù)容器(containers)和算法(algorithms)分開,彼此獨(dú)立設(shè)計(jì),最后再通過“粘合劑”將它們撮合在一起。容器和算法的泛型化,從技術(shù)角度來看并不困難,C++ 的 class templates 和 function templates 可分別達(dá)成目標(biāo)。如何設(shè)計(jì)出兩者之間的良好膠粘劑,才是大難題。

迭代器是一種行為類似指針的對(duì)象,而指針的各種行為中最常見也最重要的便是內(nèi)容提領(lǐng)(dereference)和成員訪問(member access),因此,迭代器最重要的編程工作就是對(duì) operator* 和 operator-> 進(jìn)行重載(overloading)工作。關(guān)于這一點(diǎn),C++ 標(biāo)準(zhǔn)程序庫(kù)有一個(gè) auto_ptr 可供我們參考。這是一個(gè)用來包裝原生指針(native pointer)的對(duì)象,聲名狼藉的內(nèi)存泄漏(memory leak)問題可藉此獲得解決。簡(jiǎn)化版的 auto_ptr(源代碼在頭文件<memory>中) 如下,可具體說明 auto_ptr 的行為與能力:


template<typename T>
class auto_ptr {
public:
  explicit auto_ptr(T *p = 0):pointee(p) {}
  template<typename U>
  auto_ptr(auto_ptr<U>& rhs):pointee(rhs.release()) {}
  ~auto_ptr() { delete pointee; }

  template<typename U>
  auto_ptr<T>& operator = (auto_ptr<U>& rhs) {
    if (this != &rhs) reset(rhs.release());
    return *this;
  }
  T& operator*() const { return *pointee; }
  T* operator->() const { return pointee; }
  T* get() const { return pointee; }
  // ...
private:
    T *pointee;
};

5.2 迭代器相應(yīng)型別(associated types)

在算法中運(yùn)用迭代器時(shí),很可能會(huì)用到其相應(yīng)型別(associated type)。什么是相應(yīng)型別呢?迭代器所指之物的型別便是其一。假設(shè)算法中有必要聲明一個(gè)變量,以“迭代器所指對(duì)象的型別”為型別,如何是好?畢竟 C++ 只支持 sizeof(),并未支持 typeof()!即便動(dòng)用 RTTI 性質(zhì)中的 typeid(),獲得的也只是型別名稱,不能拿來做變量聲明之用。

那么解決辦法是:利用 function template 的參數(shù)推導(dǎo)(argument deduction)機(jī)制。例如:


template <class I, class T>
void func_impl(I iter, T t)
{
  T tmp; // 這里解決了問題。T 就是 迭代器所指之物的型別,本例為 int

   //  ...  這里做原本 func() 應(yīng)該做的全部工作
};

tempalte <class I>
inline
void func(I iter)
{
  func_impl(iter, *iter);  //  func 的工作全部移往 func_impl
}

int main()
{
  int i;
  func(&i);
}

我們以 func() 為對(duì)外接口,卻把實(shí)際操作全部置于 func_impl() 之中。由于 func_impl() 是一個(gè)function template,一旦被調(diào)用,編譯器會(huì)自動(dòng)進(jìn)行 template 參數(shù)推導(dǎo)。于是導(dǎo)出型別 T,順利解決了問題。

迭代器相應(yīng)型別(associated types)不只是“迭代器所指對(duì)象的型別”一種而已。最常用的型別有五種,然而并非任何情況下任何一種都可利用上述的 template 參數(shù)推導(dǎo)機(jī)制來取得。于是 traits 應(yīng)運(yùn)而生。

5.3 Traits 編程技法——STL 源代碼門鑰

下面這個(gè) class template 專門用來“萃取”迭代器的特性,而 value type 正是迭代器的特性之一:


template <class I>
struct iterator_traits {   // traits 意為“特性”
  typedef typename I::value_type value_type;
};

這個(gè)所謂的 traits,其意義是,如果 I 定義有自己的 value type,那么通過這個(gè) traits 的作用,萃取出來的 value_type 就是 I::value_type。

根據(jù)經(jīng)驗(yàn),最常用到的迭代器相應(yīng)型別有五種: value type(迭代器所指對(duì)象的類型),difference type(兩個(gè)迭代器之間的距離),pointer,reference,iterator category(迭代器的分類)。

C++ 標(biāo)準(zhǔn)庫(kù)中只出現(xiàn)過以下三種相應(yīng)型別:

聲明一個(gè)無法賦值(因 const 之故)的暫時(shí)變量,沒什么用!因此,如果迭代器是個(gè) pointer-to-const,我們應(yīng)該設(shè)法令其 value type 為一個(gè) non-const 型別。利用偏特化,我們就可進(jìn)行如下設(shè)計(jì):

完整的 iterator_traits 如下:

除了 iterator traits 以外,標(biāo)準(zhǔn)庫(kù)里還設(shè)計(jì)了各式各樣的 traits 滿足不同的需求,如:

6. 容器之間的實(shí)現(xiàn)關(guān)系與分類

本圖以內(nèi)縮方式來表達(dá)基層與衍生層的關(guān)系。這里所謂的衍生,并非派生(inheritance)關(guān)系,而是內(nèi)含(containment)關(guān)系。例如 heap 內(nèi)含一個(gè) vector。

此處 sizeof() 為控制數(shù)據(jù)結(jié)構(gòu)本身所需要的空間大小,而不是放入其中的數(shù)據(jù)的占用空間。

接下來我們分門別類講講幾種典型的序列式容器(sequential containers)。

7. 深度探索 vector

vector 的數(shù)據(jù)安排以及操作方式,與 array 非常相似。兩者的唯一差別在于空間的運(yùn)用的靈活性:

  • array 是靜態(tài)空間,一旦配置了就不能改變;要換個(gè)大(或小)一點(diǎn)的空間,就非常麻煩:首先配置一塊新空間,然后將元素從舊址一一搬往新址,再把原來的空間釋還給系統(tǒng)。

  • vector 是動(dòng)態(tài)空間,隨著元素的加入,它的內(nèi)部機(jī)制會(huì)自行擴(kuò)充空間以容納新元素。因此,vector 的運(yùn)用對(duì)于內(nèi)存的合理利用與運(yùn)用的靈活性有很大的幫助,我們?cè)僖膊槐睾ε驴臻g不足而一開始就要求一個(gè)大塊頭 array 了,我們可以安心使用 vector,吃多少用多少。

vector 的實(shí)現(xiàn)技術(shù),關(guān)鍵在于其對(duì)大小的控制以及重新配置時(shí)的數(shù)據(jù)移動(dòng)效率。一旦 vector 舊空間滿載,如果客戶端每新增一個(gè)元素,vector 內(nèi)部只是擴(kuò)充一個(gè)元素的空間,實(shí)為不智,因?yàn)樗^擴(kuò)充空間(不論多大),一如稍早所說,是“配置新空間——數(shù)據(jù)移動(dòng)——釋還舊空間”的大工程,時(shí)間成本很高,應(yīng)該加入某種未雨綢繆的考慮,因此標(biāo)準(zhǔn)庫(kù)中的 vector 使用了“二倍成長(zhǎng)”的空間配置策略:

當(dāng)我們以 push_back() 將新元素插入于 vector 尾端時(shí),該函數(shù)首先檢查是否還有備用空間,如果有就直接在備用空間上構(gòu)造元素,并調(diào)整迭代器 finish,使 vector 變大。如果沒有備用空間了,就擴(kuò)充空間(重新配置、移動(dòng)數(shù)據(jù)、釋放原空間):

這段代碼不僅被 push_back 用,也被 insert 用,所以還要拷貝安插點(diǎn)后的原內(nèi)容

注意:所謂動(dòng)態(tài)增加大小,并不是在原空間之后接續(xù)新空間(因?yàn)闊o法保證原空間之后尚有可供配置的空間),而是以原大小的兩倍另外配置一塊較大空間,然后將原內(nèi)容拷貝過來,最后才開始在原內(nèi)容之后構(gòu)造新元素,并釋放原空間。因此,對(duì) vector 的任何操作,一旦引起空間重新配置,指向原 vector 的所以迭代器就都失效了。這是程序員容易犯的一個(gè)錯(cuò)誤,務(wù)必小心!

8. 深度探索 list

8.1 list 概述

相較于 vector 的連續(xù)線性空間,list 就顯得復(fù)雜許多,它的好處是每次插入或刪除一個(gè)元素,就配置或釋放一個(gè)元素空間。因此,list 對(duì)于空間的運(yùn)用有著絕對(duì)的精準(zhǔn),一點(diǎn)也不浪費(fèi)。而且,對(duì)于任何位置的元素插入或元素移除,list 永遠(yuǎn)是常數(shù)時(shí)間。

list 和 vector 是兩個(gè)最常被使用的容器。什么時(shí)機(jī)下最適合使用哪一種容器,必須視元素的多寡、元素的構(gòu)造復(fù)雜度(有無 non-trivial copy constructor,非默認(rèn)拷貝構(gòu)造函數(shù),non-trival copy assignment operator,非默認(rèn)拷貝賦值運(yùn)算符)、元素存取行為的特性而定。

  • __list_node 中 prev 和 next 的型別都為 void*,使用時(shí)還要類型轉(zhuǎn)換,不太理想,其實(shí)可設(shè)為 __list_node<T>*
  • list 是一個(gè)環(huán)狀雙向鏈表,所以它只需要一個(gè)指針,便可以完整表現(xiàn)整個(gè)鏈表。
  • 如果讓指針 node 指向刻意置于尾端的一個(gè)空白節(jié)點(diǎn),node 便能符合 STL 對(duì)于“前閉后開”區(qū)間的要求,成為 last 迭代器。這么一來,begin() 、end()、empty()、size() 、front()、back() 等函數(shù)便可以輕易完成。

8.2 list 的迭代器

list 不再能夠像 vector 一樣以普通指針作為迭代器,因?yàn)槠涔?jié)點(diǎn)不保證在存儲(chǔ)空間中連續(xù)存在。list 迭代器必須有能力指向 list 的節(jié)點(diǎn),并有能力進(jìn)行正確的遞增、遞減、取值、成員存取等操作(遞增時(shí)指向下一個(gè)節(jié)點(diǎn),遞減時(shí)指向上一個(gè)節(jié)點(diǎn),取值時(shí)取的是節(jié)點(diǎn)的數(shù)據(jù)值,成員取用時(shí)取用的是節(jié)點(diǎn)的成員),如下圖:

由于 STL list 是一個(gè)雙向鏈表(double linked-list),迭代器必須具備前移、后移的能力,所以 list 提供的是 Bidirectional Iterators。

list 有一個(gè)重要性質(zhì):插入操作(insert)和接合操作(splice)都不會(huì)造成原有的 list 迭代器失效。這在 vector 是不成立的,因?yàn)?vector 的插入操作可能造成記憶體重新配置,導(dǎo)致原有的迭代器全部失效。甚至 list 的元素刪除操作(erase),也只有“指向被刪除元素”的那個(gè)迭代器失效,其它迭代器不受任何影響。

  • self operator++(int) 中的 int 無意義,用來表示后置++,函數(shù)體{ }內(nèi)編譯器先遇到重載的“=”,調(diào)用拷貝構(gòu)造 copy ctor
  • 前++的返回值帶引用,后++不帶引用,是向 int 型的設(shè)計(jì)看齊,保持一致性。(不允許后++兩次,而前++可以)

G4.9 中 list iterator 的實(shí)現(xiàn):

與G2.9 相比復(fù)雜太多

9. 淺談 array & forward_list

array 就是固定內(nèi)存空間的 vector,而 forward_list 是“閹割版”的 list,相關(guān)知識(shí)這里就不再贅述了。

部分內(nèi)容參考自《C++ Primer 中文版(第5版)》以及《STL 源碼剖析》

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

推薦閱讀更多精彩內(nèi)容