文章目录
第二讲 从源代码角度讲解STL(中)
Sequence Containers 讲解:
5. 深度探索 list 容器
1. G2.9版的 list
- 内容:
- data:
- 只有一个数据:list_node* node;
- 由图中可以看出,node指向一个空白节点(尾部后,头部前);
- typedef:各种声明 list_node、list_node* 等;
- functions:各种函数 push_back() 等;
- data:
- 大小:list 本身的 sizeof() 为4个字节;【32位系统,一个指针4个字节】
- list 实质是一个双向环形链表:
- 为了符合STL容器左闭右开区间的原则,list 增加了一个空白 list_node,形成了环形结构;
- list 的数据 node 就是指向这个空白 list_node;
- list 扩容方式:
- 由于 list 是离散空间分布,所以每次扩容一个,按需扩容;
2. G2.9版的 list_node
- 内容:
- data:由于 list 是环形链表,因此 list_node 除了数据部分,还需要有两个指针(prev、next):
- 每次插入一个元素,都要额外花销两个指针的空间大小;
- 设计缺陷:
- list_node 两个指针指向的 object 一定是 list_node,所以 prev 和 next 类型应该是 list_node*;
- 但G2.9设计成了 void*(万能指针),这样在使用时还需要转型,没有必要;(这在G4.9有进一步完善)
3. G2.9版的 list_iterator
-
内容:
- typedef:iterator 必备的五个声明(图中的 (1)~(5) )+ 其他声明;
- data:list_node* 类型的 node;
- functions:各种模仿 pointer 操作的函数;
-
大小:sizeof 为4个字节(一个指针);
-
operator++ 操作的实现:
-
对于 int,编译器允许连续前置++,但不允许连续后置++;(37图左下角)
- 对于实现数值型的操作符重载,预期的行为应该对标 int 的操作符操作;
-
前置++(postfix form):
-
T& operator++() { ... // 实现对应的++操作 return *this; }
-
为了对标 int 可以连续前置++,函数返回reference;
-
-
后置++(prefix form):
-
(const) T operator++(int) { T tmp = *this; ++this; // 调用前置++运算符 return tmp; }
-
后置++调用拷贝构造创建临时对象,调用前置++实现自增操作;
-
为了对标int不能连续后置++,函数返回value;
-
个人感受:个人觉得如果函数只返回value,返回的临时对象一样还是可以继续执行后置++;为了阻止这种行为,应该返回 const value;
-
-
为什么不让 i++++ 也合法呢?
- i++是先返回i值再 +1,所以不可能返回i,那就只能先建立局部变量来保存 i 的初值,然后再返回局部变量**(局部变量不允许返回引用)**,但返回了局部变量之后,如果再连着进行下一次 ++ 运算,参与运算的就是这个局部变量的值了,所以此时 i++++ 其实等效与 i++,也就没有存在的意义了。
- i++是先返回i值再 +1,所以不可能返回i,那就只能先建立局部变量来保存 i 的初值,然后再返回局部变量**(局部变量不允许返回引用)**,但返回了局部变量之后,如果再连着进行下一次 ++ 运算,参与运算的就是这个局部变量的值了,所以此时 i++++ 其实等效与 i++,也就没有存在的意义了。
-
4. G4.9版的 list
-
改善部分:
- list_iterator 模板参数只有一个(之前是三个);
- list_node 的 prev、next 指针类型修改为 List_node*(之前是 void*);
-
G4.9版的 list 整体结构:
- 相比G2.9版的 list 结构(35图上方框),要复杂很多;
-
G4.9版的 sizeof 大小:
- 在容器之间关系中,G2.9版的 list 大小为4个字节,只有一个 pointer 数据;
- G4.9版的 list 大小为8个字节:
- list class 没有数据,所以 list 大小 == _List_base 大小;
- _List_base 只包含一个_list_impl,所以 _List_base 大小 == _list_impl 大小;
- _list_impl 只包含一个_list_node_base,所以 _list_impl 大小 == _list_node_base大小;
- _list_node_base 有两个 pointer,所以有8个字节,所以 list 有8个字节;
- G4.9版 list 也有空白节点:
- 空白节点的内容,只有两个 pointer(刚好对应那8个字节),没有 data 部分;
- 空白节点的内容,只有两个 pointer(刚好对应那8个字节),没有 data 部分;
6. iterator
1. iterator 需要遵循的原则
- iterator 必须提供5个 associated types(相关型别 / 相关类型):
- iterator_category:iterator本身的类别;
- difference_type:iterator之间距离的类型;
- value_type:iterator所指内容的类型;
- reference;iterator所指内容的引用类型;
- pointer;iterator所指内容指针类型;
- algorithm 和 iterator 的关系:
- algorithm 向 iterator 提问(需要知道一些associated types),iterator 需要为 algorithm 提供回答(iterator 必须提供5个 associated types);
- algorithm 向 iterator 提问(需要知道一些associated types),iterator 需要为 algorithm 提供回答(iterator 必须提供5个 associated types);
2. iterator_traits
1. 为什么要 iterator_traits
- 以 list_iterator 为例:list_iterator 声明了5个 associated types,algorithm 想要”提问“,直接采用右边这种形式就可以得到答案;
- 那为什么还要 iterator_traits 呢?
- 原因:如果 container 的 iterator 是 pointer,这种提问方式就失效了,因为 pointer 并不是 class,class 才能 typedef;
- 解决:加一个中间层 traits,用来区分 iterator 的类型;
2. traits介绍
-
traits 必须能区分它所收到的 iterator,是 以class设计出来的iterator 还是 pointer;
- pointer 无法定义 associated types,因为 pointer 不是 class,但它的 associated types 很直观;
- class 有能力(通过typedef)定义 associated types;
-
algorithm、iterator_traits、iterator 三者之间的关系:
- algorithm 向 iterator_traits 获取需要的 associated type:
- 如果 iterator 是 class iterator,则 iterator_traits 再向 iterator 获取;
- 如果 iterator 是 non-class iterator,就使用 iterator_traits 特化的版本提供。
- algorithm 向 iterator_traits 获取需要的 associated type:
-
疑问:
- 为什么要偏特化 <const T*> 版本,仅对 <T*> 进行偏特化不够么?
- 不够的,如果只对 <T*> 进行偏特化,iterator_traits<const T*>::value_type 的类型就是 const T。这一般不会是我们想要的(如下),所以必须对 <const T*> 也进行特化,使 iterator_traits<const T*>::value_type 的类型为 T。
- 【因为 value_type 的主要目的是用来声明暂时变量,声明一个不能修改的暂时变量(const 变量)没什么意义;】
- 为什么要偏特化 <const T*> 版本,仅对 <T*> 进行偏特化不够么?
-
完整的 iterator_traits :
- 有两个偏特化版本:<T*> 、<const T*> ;
- 注意:<const T*> 特化版本的 pointer、reference 是 const 类型;
7. 深度探索 vector
1. G2.9版的 vector
-
内容:
- data:
- 三个指针 T*(iterator 是 pointer):start、finish、end_of_storage;
- 三个指针的指向位置如图左侧;
- typedef:各种声明;
- functions:各种函数;
- data:
-
大小:vector 本身的 sizeof() 为12个字节【32位系统,一个指针4个字节,总共三个指针】;
-
vector扩容方式:
- 每次可用大小不够时,vector采取两倍成长;
- 即另外在内存中找一块两倍大小的连续空间,并将原来的元素转移到新的空间上;
- vector 的每次扩容都会伴随大量的拷贝构造和析构函数的调用;
- 扩容代码:
- insert_aux(Auxiliary 辅助)函数不只是提供给 push_back 使用;还有其他的函数会使用;
- 除了原本空间是0大小,其他情况都是2倍成长;
- 黄色部分:数据复制代码;
- 把数据分为了三部分进行复制操作:
- ① 插入点之前的 data;
- ② 插入的 data;
- ③ 插入点之后的 data;
- 把数据分为了三部分进行复制操作:
- 每次可用大小不够时,vector采取两倍成长;
2. G2.9版的 vector iterator
-
由于 vector 是连续空间分布,所以 vector’s iterator 是 pointer;
-
获取 associated types 过程:
- algorithm 向 iterator_trait 获取 associated types;
- 因为 vector’s iterator 是 pointer,所以使用 iterator_trait <T*> 特化版本;
3. G4.9版的 vector
- G4.9版的 vector 整体结构:
- 从原本的单一类实现,变成了多个类实现,加入了多个继承和包含;
- G4.9版的 sizeof 大小:
- G2.9版的 vector 大小为12个字节,有3个 pointer 数据;
- G4.9版的 vector 大小也是12个字节:
- 只有 _Vector_impl class 有三个 pointer;
- 【设计缺陷:】
- _Vector_impl 继承 std::allocator 采用了 public 继承;
- public 继承意味着二者是 is-a 的关系,但 vector 和 allocator 明显不是 is-a 关系;
- 所以应该采用 private 继承,虽然使用 public 继承功能上也不会出错就是了;
4. G4.9版的 vector iterator
- vector‘s iterator 用 object 进行了包装,不是单纯的定义为 pointer,但归根到底,vector’s iterator 类型还是 T*;
- G4.9版的 vector 通过 traits 获取 associated types 就不是使用特化版本了;
- 因为经过包装,iterator 已经是一个 object,所以 traits 使用泛化版本;
- 因为经过包装,iterator 已经是一个 object,所以 traits 使用泛化版本;
8. 深度探索 array
0. 补充说明
- C++1.0,也叫C++98(1998年);
- C++2.0,也叫C++11(2011年);
- TR1版本是C++1.0(1998年)~ C++2.0(2011年)之间的过渡版本;
1. TR1版的 array
- data:使用了语言本身的数组;
- iterator:pointer;
2. G4.9版的 array
- 经过几层包装,把 data 的定义放在别的 object;
9. 深度探索 forward_list
1. forward_list 介绍
- 内容和list类似;
- forward_list 是单向链表,它的 iterator 类别是单向的 Forward iterator;
- forward_list 有一个空的head节点;
10. 深度探索 deque
1. G2.9版的 deque iterator
- 由于 deque 不是连续空间分布(是分段连续),所以deque_iterator 是一个 class;
- 内容:
- data(4个指针):
- T* cur:指向当前元素;
- T* first:指向当前 buffer 的首部;
- T* last:指向当前 buffer 的尾部;
- T** node:指向当前 buffer 在 map 的位置;【因为 map 存放的内容是 pointer,所以 node 就是一个指向 pointer 的 pointer;】
- data(4个指针):
- 大小:一个 deque iterator 的 sizeof 为16个字节;
2. G2.9版的 deque
- deque是分段连续(并不是整体连续),如图;
- deque 维护一个 vector map,map 的内容存放指针,每个指针都指向一段连续的等量空间 buffer;
- 【但 deque 内部实现会制造假象,让用户使用起来误以为是整体连续的;】
- 内容:
- data:
- iterator start:其中的 cur、first、last 和 node 的信息指向第一个 buffer;(在55图中有表示)
- iterator finish:其中的 cur、first、last 和 node 的信息指向最后一个 buffer;(在55图中有表示)
- map_pointer map:指向 map,也是一个 T** 类型;
- size_type map_size:map 数组的大小;(unsigned int 或者 unsigned long,4个字节)
- data:
- 大小:deque 的 sizeof 为16*2 + 4 + 4 == 40个字节(32位系统,一个指针4个字节);
- 扩容过程:
- 每次扩充一个buffer;
- 当push_back(…)空间不足时,获取一块新的 buffer ,在 map 尾部新增一个指针,指向新的buffer;
- 当push_front(…)空间不足时,获取一块新的 buffer,在 map 头部新增一个指针,指向新的buffer;
- 如果 map 空间不够,会采用两倍成长(vector),在 copy 的过程中,deque 会把数据 copy 到新 map的中段,使两边都有等量的预留空间。
- G2.9版的 deque 允许指定每个 buffer 的大小(第三个参数,如图右上角);
3. deque::insert()
- 步骤:
- 判断插入点是否是头部,是调用 push_front(…);
- 判断插入点是否是尾部,是调用 push_back(…);
- 判断插入点距离头部、尾部,哪一段更近(与中部比较):
- 离头部更近,将插入点之前的元素向头部推动;
- 离尾部更近,将插入点之后的元素向尾部推动;
- 在插入点设置新值;
4. deque 如何模拟连续空间
- 都是 iterator 的功劳,依靠 iterator 的操作符重载;
1. operator*() 和 operator->()
-
operator*() :返回 cur 所指内容;
-
operator->():使用 operator*(),返回 cur 所指内容的地址;
2. operator-(const self& x) const
- 计算两个 iterator 的距离,分为三部分:
- buffer的大小 * 两个 iterator 所指 buffer 之间的 buffers 数量;
- 当前 iterator 所指 buffer 的元素数量;
- x 所指 buffer 的元素数量;
3. 前后置 ++ 和 前后置 –
- 右下角的 set_node() 是跳转 buffer 函数;
- self& operator++():
- 先自增,然后判断是否到达 buffer 尾部;是,则要跳转到下一个 buffer 头部;
- self operator++(int):
- 调用前置 ++,返回临时变量;
- self& operator–():
- 先判断是否 buffer 头部,是,则要跳转到上一个 buffer 的尾部;再自减;
- self operator–(int):
- 调用前置 --,返回临时变量;
- 调用前置 --,返回临时变量;
4. operator+=(difference_type n) 和 operator+(difference_type n)
- operator+=(difference_type n) :
- 移动前,先计算移动到达的位置,判断目标位置是否在同一个 buffer 中;
- 是,则直接修改 cur;
- 不是,则要计算跨越了多少个 buffer,并切换到正确的 buffer;再移动剩余位置到目标点;
- 移动前,先计算移动到达的位置,判断目标位置是否在同一个 buffer 中;
- operator+(difference_type n) 【注意,这是传入一个数值,不是 iterator】:
- 调用 operator+=(difference_type n);
- 调用 operator+=(difference_type n);
5. operator-=(difference_type n)、operator-(difference_type n) 和 operator[](difference_type n)
- operator-=(difference_type n) :
- 调用 operator+=(difference_type n);加一个负值;
- operator-(difference_type n) 【注意,这是传入一个数值,不是 iterator】:
- 调用 operator-=(difference_type n);
- operator[](difference_type n)
- 调用 operator+(difference_type n);
- 调用 operator+(difference_type n);
5. G4.9版的 deque
- G4.9版的 deque 整体结构:
- 从原本的单一类实现,变成了多个类实现,加入了多个继承和包含;
- G4.9版的 sizeof 大小:
- G2.9版的 deque 大小为40个字节;
- G4.9版的 deque 大小也是40个字节:
- 只有 _Deque_impl class 有 data;
- 设计缺陷:一样有继承的缺陷;
- _Deque_base 不能指定 buffer size 了,少了一个参数;
11. queue 和 stack
1. queue 和 stack 介绍
-
queue 和 stack 是 adaptor,缺省底层容器是 deque;
-
queue 和 stack 的所有操作,都是依靠底层容器的操作来实现的;
-
queue:
-
stack:
2. queue 和 stack 不提供 iterator
- 判断有没有 iterator,可以看它能不能进行 insert 操作;
- queue 和 stack 都有特殊的行为,insert 会打乱这种行为,所以 queue 和 stack 没有 iterator,也就不能遍历;
3. 可选底层容器
1. 都可以选择的容器
- queue 和 stack 都可以选择的底层容器:list 或者 deque;
2. 一方可以选择的容器
- queue 对底层容器的要求更高,因为 queue 需要两端操作;
- queue 不能选择 vector 作为底层容器,stack 可以选择 vector 作为底层容器;
- 这里透露出一点:编译器对模板没办法全面检查,定义时编译器不会报错,只有在你使用时,才会检查是否合法;