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

目錄

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

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

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

  4. 分配器(allocators)

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

  6. 容器之間的實現(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)庫:...\include\C++\bits
    • 擴充標(biāo)準(zhǔn)庫:...\include\C++\ext

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

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

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

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

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

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

3.1 操作符重載(Operator Overloading)

因為之前的筆記中對操作符重載已經(jīng)有比較詳細(xì)的介紹,這里就不贅述了,只簡單的羅列一些要注意的點:

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

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

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

3.2 模板(Templates)

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

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

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

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

3.2.1 模板類型

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

3.2.1.1 類模板(Class Templates)

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

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

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

3.2.1.3 成員模板(Member Templates)

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

3.2.2 模板特化(Specialization)

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

特化

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

4. 分配器(allocators)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

迭代器是一種行為類似指針的對象,而指針的各種行為中最常見也最重要的便是內(nèi)容提領(lǐng)(dereference)和成員訪問(member access),因此,迭代器最重要的編程工作就是對 operator* 和 operator-> 進(jìn)行重載(overloading)工作。關(guān)于這一點,C++ 標(biāo)準(zhǔn)程序庫有一個 auto_ptr 可供我們參考。這是一個用來包裝原生指針(native pointer)的對象,聲名狼藉的內(nèi)存泄漏(memory leak)問題可藉此獲得解決。簡化版的 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īng)型別(associated type)。什么是相應(yīng)型別呢?迭代器所指之物的型別便是其一。假設(shè)算法中有必要聲明一個變量,以“迭代器所指對象的型別”為型別,如何是好?畢竟 C++ 只支持 sizeof(),并未支持 typeof()!即便動用 RTTI 性質(zhì)中的 typeid(),獲得的也只是型別名稱,不能拿來做變量聲明之用。

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


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() 為對外接口,卻把實際操作全部置于 func_impl() 之中。由于 func_impl() 是一個function template,一旦被調(diào)用,編譯器會自動進(jìn)行 template 參數(shù)推導(dǎo)。于是導(dǎo)出型別 T,順利解決了問題。

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

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

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


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

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

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

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

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

完整的 iterator_traits 如下:

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

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

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

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

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

7. 深度探索 vector

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

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

  • vector 是動態(tài)空間,隨著元素的加入,它的內(nèi)部機制會自行擴充空間以容納新元素。因此,vector 的運用對于內(nèi)存的合理利用與運用的靈活性有很大的幫助,我們再也不必害怕空間不足而一開始就要求一個大塊頭 array 了,我們可以安心使用 vector,吃多少用多少。

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

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

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

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

8. 深度探索 list

8.1 list 概述

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

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

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

8.2 list 的迭代器

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

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

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

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

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

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

9. 淺談 array & forward_list

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容