Boolan(博览网)——STL与泛型编程(第七周)

目录

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

  2. 面向对象编程(Object-Oriented Programming,OOP) vs. 泛型编程 (Generic Programming,GP)

  3. 阅读C++标准库源码(Source Code)之必要基础:操作符重载(Operator Overloading)and 模板(Templates)

  4. 分配器(allocators)

  5. 迭代器(Iterator)的设计原则和 Iterator Traits 的作用与设计

  6. 容器之间的实现关系与分类

  7. 深度探索 vector

  8. 深度探索 list

  9. 浅谈 array & forward_list

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

  • VC:...\include
  • GCC:...\4.9.2\include\C++
    • 原生标准库:...\include\C++\bits
    • 扩充标准库:...\include\C++\ext

2. 面向对象编程(Object-Oriented Programming,OOP) vs. 泛型编程 (Generic Programming,GP)

问:为什么 list 不能使用 ::sort() 排序?

答:只有随机访问迭代器才能进行上述加减乘除,而 list 在内存里面是一个一个结点用指针串起来的,并不是一个连续空间,所以它所具备的迭代器是不能够跳来跳去的,它只能一个一个前进或后退。
因此标准库中的 ::sort() 所需要的迭代器是 list 中提供的迭代器所不能满足的。

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

如果需要定制特殊的算法,对特定“比大小”功能的设计就显得尤为重要。

3. 阅读C++标准库源码(Source Code)之必要基础:操作符重载(Operator Overloading)and 模板(Templates)

3.1 操作符重载(Operator Overloading)

因为之前的笔记中对操作符重载已经有比较详细的介绍,这里就不赘述了,只简单的罗列一些要注意的点:

当一个重载的运算符是成员函数时, this 绑定到左侧运算对象。成员运算符函数的(显式)参数数量比运算对象的数量要少一个。

可以(不能)被重载的运算符:

通常情况下,不应该重载逗号、取地址、逻辑与和逻辑或运算符。

3.2 模板(Templates)

模板是 C++ 中泛型编程的基础。一个模板就是一个创建类或函数的蓝图或者说公式。

模板定义以关键字 template 开始,后跟一个模板参数列表(template parameter list),其不能为空。

当使用模板时,我们(隐式地或显式地)指定模板实参(template argument),将其绑定到模板参数上。

模板类型参数前必须使用关键字 classtypename
在模板参数列表中,这两个关键字的含义相同,可以互换使用。

3.2.1 模板类型

主要有类模板,函数模板和成员模板三种类型:

3.2.1.1 类模板(Class Templates)

编译器不能为类模板推断模板参数类型,因此必须显式使用。

3.2.1.2 函数模板(Function Templates)

编译器自动为函数模板推断模板参数类型。

3.2.1.3 成员模板(Member Templates)

成员模板不能是虚函数。

3.2.2 模板特化(Specialization)

模板的特化就是一个独立的定义,在其中一个或多个模板参数被指定为特定的类型。
注意:特化的本质是实例化一个模板,而非重载它。因此,特化不影响函数匹配。

特化

偏特化(个数的偏特化和范围的偏特化):

4. 分配器(allocators)

标准库 allocator 类定义在头文件 memory 中,它帮助我们将内存分配和对象构造分离开来。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。

蓝色部分:切实需要的空间大小
灰色部分:debug mode 模式的空间需求
红色部分:cookie,帮助编译器识别此段空间的内容
绿色部分:用来调整边界,使空间分配统一化

4.1 VC6 STL 中的分配器设计

  • (int*)0:只为了告诉函数是 int 型
  • typename 后面加()构成一个临时对象
  • VC 中的分配器最终就是调用 C 中的 malloc 和 free, 没有任何特殊的设计

4.2 BC5 STL 中的分配器设计

4.3 G2.9 STL 中的分配器设计(侯大师推荐!可读性非常高!)

此处的设计并未被使用。

设计目的:减少额外开销(避免之前那种用cookie包起来的大量额外开销)

4.4 G4.9 STL 中的分配器设计

又用回传统方式了-。-

若要使用 G2.9 中设计的 alloc,就要像用例中那样。

5. 迭代器(Iterator)的设计原则和 Iterator Traits 的作用与设计

5.1 迭代器的设计原则

不论是泛型思维或 STL 的实际运用,迭代器都扮演者重要的角色。 STL 的中心思想在于:将数据容器(containers)和算法(algorithms)分开,彼此独立设计,最后再通过“粘合剂”将它们撮合在一起。容器和算法的泛型化,从技术角度来看并不困难,C++ 的 class templates 和 function templates 可分别达成目标。如何设计出两者之间的良好胶粘剂,才是大难题。

迭代器是一种行为类似指针的对象,而指针的各种行为中最常见也最重要的便是内容提领(dereference)和成员访问(member access),因此,迭代器最重要的编程工作就是对 operator* 和 operator-> 进行重载(overloading)工作。关于这一点,C++ 标准程序库有一个 auto_ptr 可供我们参考。这是一个用来包装原生指针(native pointer)的对象,声名狼藉的内存泄漏(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 迭代器相应型别(associated types)

在算法中运用迭代器时,很可能会用到其相应型别(associated type)。什么是相应型别呢?迭代器所指之物的型别便是其一。假设算法中有必要声明一个变量,以“迭代器所指对象的型别”为型别,如何是好?毕竟 C++ 只支持 sizeof(),并未支持 typeof()!即便动用 RTTI 性质中的 typeid(),获得的也只是型别名称,不能拿来做变量声明之用。

那么解决办法是:利用 function template 的参数推导(argument deduction)机制。例如:


template <class I, class T>
void func_impl(I iter, T t)
{
  T tmp; // 这里解决了问题。T 就是 迭代器所指之物的型别,本例为 int

   //  ...  这里做原本 func() 应该做的全部工作
};

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,一旦被调用,编译器会自动进行 template 参数推导。于是导出型别 T,顺利解决了问题。

迭代器相应型别(associated types)不只是“迭代器所指对象的型别”一种而已。最常用的型别有五种,然而并非任何情况下任何一种都可利用上述的 template 参数推导机制来取得。于是 traits 应运而生。

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。

根据经验,最常用到的迭代器相应型别有五种: value type(迭代器所指对象的类型),difference type(两个迭代器之间的距离),pointer,reference,iterator category(迭代器的分类)。

C++ 标准库中只出现过以下三种相应型别:

声明一个无法赋值(因 const 之故)的暂时变量,没什么用!因此,如果迭代器是个 pointer-to-const,我们应该设法令其 value type 为一个 non-const 型别。利用偏特化,我们就可进行如下设计:

完整的 iterator_traits 如下:

除了 iterator traits 以外,标准库里还设计了各式各样的 traits 满足不同的需求,如:

6. 容器之间的实现关系与分类

本图以内缩方式来表达基层与衍生层的关系。这里所谓的衍生,并非派生(inheritance)关系,而是内含(containment)关系。例如 heap 内含一个 vector。

此处 sizeof() 为控制数据结构本身所需要的空间大小,而不是放入其中的数据的占用空间。

接下来我们分门别类讲讲几种典型的序列式容器(sequential containers)。

7. 深度探索 vector

vector 的数据安排以及操作方式,与 array 非常相似。两者的唯一差别在于空间的运用的灵活性:

  • array 是静态空间,一旦配置了就不能改变;要换个大(或小)一点的空间,就非常麻烦:首先配置一块新空间,然后将元素从旧址一一搬往新址,再把原来的空间释还给系统。

  • vector 是动态空间,随着元素的加入,它的内部机制会自行扩充空间以容纳新元素。因此,vector 的运用对于内存的合理利用与运用的灵活性有很大的帮助,我们再也不必害怕空间不足而一开始就要求一个大块头 array 了,我们可以安心使用 vector,吃多少用多少。

vector 的实现技术,关键在于其对大小的控制以及重新配置时的数据移动效率。一旦 vector 旧空间满载,如果客户端每新增一个元素,vector 内部只是扩充一个元素的空间,实为不智,因为所谓扩充空间(不论多大),一如稍早所说,是“配置新空间——数据移动——释还旧空间”的大工程,时间成本很高,应该加入某种未雨绸缪的考虑,因此标准库中的 vector 使用了“二倍成长”的空间配置策略:

当我们以 push_back() 将新元素插入于 vector 尾端时,该函数首先检查是否还有备用空间,如果有就直接在备用空间上构造元素,并调整迭代器 finish,使 vector 变大。如果没有备用空间了,就扩充空间(重新配置、移动数据、释放原空间):

这段代码不仅被 push_back 用,也被 insert 用,所以还要拷贝安插点后的原内容

注意:所谓动态增加大小,并不是在原空间之后接续新空间(因为无法保证原空间之后尚有可供配置的空间),而是以原大小的两倍另外配置一块较大空间,然后将原内容拷贝过来,最后才开始在原内容之后构造新元素,并释放原空间。因此,对 vector 的任何操作,一旦引起空间重新配置,指向原 vector 的所以迭代器就都失效了。这是程序员容易犯的一个错误,务必小心!

8. 深度探索 list

8.1 list 概述

相较于 vector 的连续线性空间,list 就显得复杂许多,它的好处是每次插入或删除一个元素,就配置或释放一个元素空间。因此,list 对于空间的运用有着绝对的精准,一点也不浪费。而且,对于任何位置的元素插入或元素移除,list 永远是常数时间。

list 和 vector 是两个最常被使用的容器。什么时机下最适合使用哪一种容器,必须视元素的多寡、元素的构造复杂度(有无 non-trivial copy constructor,非默认拷贝构造函数,non-trival copy assignment operator,非默认拷贝赋值运算符)、元素存取行为的特性而定。

  • __list_node 中 prev 和 next 的型别都为 void*,使用时还要类型转换,不太理想,其实可设为 __list_node<T>*
  • list 是一个环状双向链表,所以它只需要一个指针,便可以完整表现整个链表。
  • 如果让指针 node 指向刻意置于尾端的一个空白节点,node 便能符合 STL 对于“前闭后开”区间的要求,成为 last 迭代器。这么一来,begin() 、end()、empty()、size() 、front()、back() 等函数便可以轻易完成。

8.2 list 的迭代器

list 不再能够像 vector 一样以普通指针作为迭代器,因为其节点不保证在存储空间中连续存在。list 迭代器必须有能力指向 list 的节点,并有能力进行正确的递增、递减、取值、成员存取等操作(递增时指向下一个节点,递减时指向上一个节点,取值时取的是节点的数据值,成员取用时取用的是节点的成员),如下图:

由于 STL list 是一个双向链表(double linked-list),迭代器必须具备前移、后移的能力,所以 list 提供的是 Bidirectional Iterators。

list 有一个重要性质:插入操作(insert)和接合操作(splice)都不会造成原有的 list 迭代器失效。这在 vector 是不成立的,因为 vector 的插入操作可能造成记忆体重新配置,导致原有的迭代器全部失效。甚至 list 的元素删除操作(erase),也只有“指向被删除元素”的那个迭代器失效,其它迭代器不受任何影响。

  • self operator++(int) 中的 int 无意义,用来表示后置++,函数体{ }内编译器先遇到重载的“=”,调用拷贝构造 copy ctor
  • 前++的返回值带引用,后++不带引用,是向 int 型的设计看齐,保持一致性。(不允许后++两次,而前++可以)

G4.9 中 list iterator 的实现:

与G2.9 相比复杂太多

9. 浅谈 array & forward_list

array 就是固定内存空间的 vector,而 forward_list 是“阉割版”的 list,相关知识这里就不再赘述了。

部分内容参考自《C++ Primer 中文版(第5版)》以及《STL 源码剖析》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容