一、容器 - 结构与分类
下图就是一些基本的容器,红色框代表是 C++11 新增容器。
下面我再用一个图形象的表示各个容器之间的关系
这里需要注意的是:这张图是以缩排方式表示 ”基层与衍生层“ 的关系。而这里的衍生,并非继承而是复合。比如,set 和 map,它们是底层有一个 rb_tree,而不是继承 rb_tree,这里需要弄清楚它们之间的关系。里面也涵盖了了一些非标准的非公开的容器。到了 C++11 之后,slist 换了一个名字叫做 forward_list。所有 hash 开头的都改为 unordered 开头,比如,hash_set 改为 unordered_set。且新增了 array。
二、探索 list
下面我们来探索一下 list 的 节点,list 本身以及 list 的迭代器的源代码。
我们首先来看 list 本身,它本身定义了一个 link_type 类型的 node。但是 link_type 是什么类型呢?往上看,定义了 typedef list_node* link_type。可以看出,link_type 就是一个指针,至于具体是什么类型的指针呢?我们不关心,所以如果我们定义了一个 list 对象,那么它的大小就是 4 字节(32位),但是现在已经变成 12 字节。
而我们现在再来讨论一下 list 的节点,list 是一个环状的双向链表。我们用下图表示 list 的节点。我们可以看到,图中有一个 灰色的节点,这个灰色的节点代表的是什么呢?在 STL 中,我们规定所有的容器必须满足 ”前闭后开“ 区间的原则,即 begin() 指向的是容器的第一个元素,而 end() 指向的是容器的最后一个元素的下一个元素,也就是不存在这个容器里面的元素,图中我们用灰色节点代替。所以我们常常使用与 end() 比较来判断是否到达了容器的结尾。
然后我们仔细看每一个节点,我们可以看到每一个节点内部,有一个 void* 类型的 prev 和 next(下图代码),还有一个 data 字段。所以我们向内存申请一个节点的内存的时候,需要有 2 个指针的大小 + data 的大小,而不仅仅只是一个指针的大小。
下面来看看 list 迭代器,因为 list 是非连续空间,所以这个 iterator 不能够是指针。如果是指针的话,指针++不知道会指向哪里。所以这里的迭代器要足够聪明,要先找到该节点的 next 指针,然后通过 next 指针指向下一个节点。
我们来看看 iterator 是怎么做到足够聪明的?
我们从源代码可以看出,iterator 是一个 class(类),它的内部有很多的 typedef 和 操作符重载,而这些操作符重载要实现指针基本的操作(++,–,*,->等等)。所以 iterator 是一个智能指针。所以所有的容器(除了 vector 和 array)的迭代器都是一个类,即智能指针。
那 iterator的 ++ 是怎样实现的呢?看下图,我们是将 node 的 next 值赋值给迭代器,那么迭代器 ++ 就通过了next指针到了下一个节点,也就实现了 ++。如果是 – 的话,就将 node 的 prev 赋值给迭代器,然后通过 – 找到了前一个结点。
而 ++ 又分为前置 ++ 和 后置 ++,那么它们有什么区别呢?下面来看看源代码。
区别
:
- 后置++带有参数 int,而前置 ++ 没有;
- 前置 ++ 的返回值是一个引用,而后置 ++没有。是因为 C++ 支持 ++++i,而不支持 i++++;
- 后置 ++ 中调用了前置 ++(++*this)。
后置 ++ 的实现
- 1.0(记录原值):其实是唤起 copy ctor(拷贝构造函数),用以创建 tmp 并以 *this 为初值,不会唤起 operator ,因为 *this 已被解释为拷贝构造函数的参数。
- 1.1:不会唤起 operator*
- 2(进行操作):不会唤起 operator*,因为 *this 已被解释为 前置++的参数。
- 3(返回原值):最后的 return value 会唤起拷贝构造。
其实看着这里,我们可能会发现 G2.9 版本的 list 还是有稍微不足的地方:
但是,到了 G4.9 版本的时候,这些都得到了优化,下面来看看对比:
改进
- iterator 只使用了一个模板参数(_Tp)
- node 节点有其 parent,node 的成员的 type 较精确,而不再是 void*。
而在 G4.9 版本中的 list,已经有了继承关系,相比于 G2.9 版本的 list,实现已经较为复杂,如果直接从 G4.9 版本开始研究源代码的话,可能会有很多童鞋放弃。