(源码很重要,只有熟悉源码,才能知道怎么用)

源码分布(VC , GCC)

C++ STL(二)_红黑树
C++ STL(二)_迭代器_02

C++ STL(二)_c++_03

C++ STL(二)_c++_04

OOP vs GP

C++ STL(二)_数据_05

OOP:数据放在class里面,操作这些数据的函数也放在class里面

为什么list不能用::sort排序呢?
因为::sort的在设计中采用了随机访问迭代器(由下图红色部分),
而list(类似链表)是不支持这个操作的

C++ STL(二)_父类_06

iterator迭代器是泛化指针,借助迭代器可以算法可以实现对数据的操作

C++ STL(二)_红黑树_07
C++ STL(二)_父类_08

操作符重载和模板(泛化,全特化,偏特化)

这一集再看看,尤其是关于模板部分

C++ STL(二)_父类_09

这里的特化说的很清楚

C++ STL(二)_迭代器_10
C++ STL(二)_红黑树_11
C++ STL(二)_红黑树_12

分配器

C++ STL(二)_迭代器_13

所有的内存分配动作最终都会到malloc,这个malloc它就会根据不同的操作系统调用API,从而拿到真正的内存

在标准层面上,大家都最终跑到malloc上

C++ STL(二)_迭代器_14

从上图中可以看到operator new调用malloc

右图为malloc调用的内存(详情见内存管理)

C++ STL(二)_迭代器_15

C++ STL(二)_c++_16

由上图可知allocate调用_Allocate,而_Allocate又调用operator new,然后由前面可知,operator new是调用malloc的
结论:在VC所提供的分配器allocate,当它要分配内存的时候,它是一层一层下去最终调用的是malloc函数

malloc对应free

而VC的deallocate调用的是operator delete,而operator delete它又是调用的free
上图中的allocate<int>()表示的一个临时对象object

注意这里deallocate在归还内存的时候是还512个,这件事是很费心的,所以还是容器好
string是个指针????
结论:**VC**的分配器没有独特设计,它就是调用c的malloc和free函数来分配和释放内存,而且它的接口设计对我们不友好,
但是如果是容器去用就没问题

BC的分配器

C++ STL(二)_数据_17
C++ STL(二)_红黑树_18

困扰:我的容器如果放100万个元素,而且每个元素是小小的,由于这些元素的内存分配都是用malloc得到的,所以额外开销就会很大(由于区块是小小的)

GNC分配器

C++ STL(二)_数据_19

结论:上面三种分配器都是使用malloc和free来分配/释放内存,会造成大量的额外开销
下面这个分配器是比较好用的,可以解决上面三种分配器的问题

分配器用的是alloc√(这是G2.9版本)

C++ STL(二)_父类_20
C++ STL(二)_迭代器_21

注意G4.9又恢复到前面三种的用法,调用malloc,造成额外开销

C++ STL(二)_红黑树_22
C++ STL(二)_红黑树_23

想要用回G2.9的那个,见下面的用例和分析

C++ STL(二)_父类_24

容器之间的实现与分类

分配器是为容器服务的

C++ STL(二)_红黑树_25

上面的复合表示的是拥有的意思
这里的缩排表示的是复合的关系
rb_tree就是红黑树的意思,这里就相当于说map/set里面有一个红黑树帮它管理支撑
在标准库里,尽量用复合而非继承

探索list

再看一遍,尤其是关于迭代器的部分

list容器不是最简单的,但是是最具代表性的容器

C++ STL(二)_红黑树_26

C++ STL(二)_红黑树_27
C++ STL(二)_数据_28

注意先+和后+的返回值类型,一个有&,一个没有(原因见上图的左下角的++++i和i++++

C++ STL(二)_父类_29

这里的*node就是获取node所指向的整个节点
所有容器的迭代器iterator都有两代部分:1typedef 2、操作符重载

C++ STL(二)_c++_30

注意G2.9到G4.9的改进,正好解决了G2.9的三处问题,见上图

C++ STL(二)_红黑树_31

iterator需要遵守的原则

和模板联系紧密,回头再看看
C++ STL(二)_c++_32
C++ STL(二)_红黑树_33
C++ STL(二)_c++_34
C++ STL(二)_父类_35
C++ STL(二)_红黑树_36

下面是其他的traits

C++ STL(二)_红黑树_37

vector深度扩充

vector不能原地扩充

C++ STL(二)_红黑树_38

三个指针控制容器的大小,三个指针所以前面展示的是12字节

上图中size()的实现是调用两个函数相减实现的,为什么不直接用finish-start呢???
首先直接减快不了多少,其实这么做是为了应对里面不是finish,start这种情况

C++ STL(二)_c++_39

上图呈现出两次检查,为什么呢?
那是因为insert_aux不仅会被push_back调用,可能还会被其他函数调用

C++ STL(二)_红黑树_40

假设现在是第8个元素,当放了第9个元素的时候,vector要做两倍增长,于是在空间中找出一个16个元素的大小,
然后把原来的8个元素copy到这个16处,再把第9个元素放入新的内存中去

C++ STL(二)_父类_41
C++ STL(二)_迭代器_42

注意上面的第二次copy(可能是在中间插入值的时候发生了扩充,所以安插点之后的值也要copy)
注意:每次只要增长,就会有大量的元素拷贝动作,元素的拷贝会引发拷贝构造函数,拷贝之后,原来的元素也要被删除,要调用析构函数,这是一个大的成本

vector的迭代器

链表的迭代器是需要单独设计成一个class(节点是分离的)
而vector中的元素是连续的,设计成指针即可

C++ STL(二)_迭代器_43

这里的设计和前面list的设计基本一样(不熟悉)

C++ STL(二)_父类_44

vector的大小就是_vector_base的大小,而_vector_base的大小就是数据_M_impl的大小,而数据_M_impl的大小就得看它的类型_Vector_impl,
而这个类型的大小三个指针这么大(12),然而这个类型还有父类,在计算大小的时候也应当把父类加进去(父类是子类的一部分),然而这里的父类是一个allocator,没有大小(没有data),所以vector的大小为12
G 4.9这个没有G 2.9好用

老师认为这里不应该用public,而应该是用private
public有is a的含义,这里只是使用而已

C++ STL(二)_迭代器_45
C++ STL(二)_迭代器_46

这里老师疯狂diss为什么要G 4.9要设计的这么复杂

容器array

C++ STL(二)_父类_47

array就是数组,但是为什么要把array包装成容器来用呢???
因为变成容器之后,它就要遵循容器的规律,规则,要提供iterator迭代器,而迭代器又要提供5种相应的类型,
以便于可以让算法去询问必要的信息,算法就可以决定采取最优化的动作,如果没有这么包装的话,array就会被摒弃在六大部件之外,他就不能享受仿函数,算法等和它的交互关系

TR1:技术报告1

其他容器都是可扩充的,但是array要指定大小
array没有构造函数和析构函数
是连续的地址空间,迭代器可以用指针,不需要另外设计class

C++ STL(二)_父类_48

forward_list是单向链表,具体实现参考前面的list(双向链表)

C++ STL(二)_红黑树_49

deque,queue和stack深度探索

C++ STL(二)_数据_50

deque对外号称连续,其实它是分段的(不同段是用vector来串接起来,vector里面存的是指针,这些指针分别指向各个缓冲区buffer(所以上图五个buffer的次序就是看五个指针的次序))
如果我们push_back放入元素,把尾端buffer用光,它要怎么扩充呢???
deque就会新分配一个缓冲区,并且串接到vector的末端

这里有一个疑问?老师说是vector,但是当vector的头部存满了,怎么扩充???

deque的迭代器是一个class,这个class内部有四个元素,其中node指向控制中心

控制中心就是map所指的东西,其实是一个vector
当创建一个deque的时候,这个对象本身40个字节(16+16+4+4

C++ STL(二)_迭代器_51
C++ STL(二)_父类_52

首先定义五个相关的类型

C++ STL(二)_父类_53

deque有个很聪明的地方,这里用insert函数举例,比如deque中有1万个元素,现在要在第5位插入元素,
deque会选择通过移动头部的前4个元素来实现插入操作(而不是移动1-5个元素),每次移动时都会调用拷贝构造函数和析构函数

C++ STL(二)_父类_54

通过和中间位置的长度相比,来判断是在前面插入还是在后面插入

deque怎么模拟连续空间

关键在于迭代器的加加减减,加等于,减等于这些动作上,还一定要能检查边界,判断跳到另一个缓冲区去

C++ STL(二)_父类_55

上图中的finish-start怎么实现的?
3*8+3+0

C++ STL(二)_迭代器_56

C++ STL(二)_红黑树_57

怎么跳到下一个缓冲区??
先回到控制中心,然后移动到下一个位置(存的指针),这个指针指向另一个缓冲区

C++ STL(二)_数据_58

+n的时候,有一个判断,判断是否还在该缓冲区内(如上图注解)

C++ STL(二)_迭代器_59

只要连续空间这种,或者假象,都应该提供[]中括号这种操作符

C++ STL(二)_c++_60

C++ STL(二)_红黑树_61

这里解释了,在copy的过程时,比如8->16,会把8 copy 到16的中端,使它可以向左或者向右扩充缓冲区

C++ STL(二)_红黑树_62
C++ STL(二)_c++_63

所以说stack和queue内含一个deque

C++ STL(二)_数据_64

C++ STL(二)_数据_65

编译器对模板不会进行全面检查,比如上面的pop(),当没有用到pop()操作的时候queue就可以用vector做底层结构

C++ STL(二)_数据_66

rb_tree容器

有点疑问

C++ STL(二)_迭代器_67

红黑树设计成高度平衡的树,有利于查找

不能用迭代器改变元素的值,因为会破坏排序树
红黑树是按照key来排序的

C++ STL(二)_父类_68

C++中空类占一个字节,上面的rb_tree其实是4+4+1(仿函数为1,因为里面没有数据,又不能弄成0,凑成4的倍数到12

C++ STL(二)_红黑树_69

疑问:为什么key和value都设置成int,就相当于没有data了????
value不是包含key和data嘛?

C++ STL(二)_红黑树_70
C++ STL(二)_红黑树_71
C++ STL(二)_父类_72

C++ STL(二)_红黑树_73

这里老师终于说了为啥G 4.9要设计成这种复杂难用的形式,handle 和 body这种设计模式(桥接模式)

上图为啥是24个字节???
color这里怎么看

C++ STL(二)_父类_74

这里说set有规定,value就是key,但是不知道为什么???

C++ STL(二)_数据_75

像上诉这种,自己不做事情,把全部事情都交给底层去实现的,一般称为容器适配器(功能使用上市容器,但是技术分类上不是)

C++ STL(二)_父类_76
C++ STL(二)_迭代器_77

C++ STL(二)_父类_78
C++ STL(二)_数据_79

map的迭代器只能改data,不能改key(注意上图中的key是const类型)

C++ STL(二)_数据_80
C++ STL(二)_父类_81

这里的中括号[]的实现,找到就返回,找不到就插入

hashtable容器

C++ STL(二)_迭代器_82

C++ STL(二)_数据_83

hash表的扩容,打散,篮子2倍
(元素个数大于篮子个数就需要扩容)

C++ STL(二)_迭代器_84

把对象object反射为一个编号

上面1+1+1+12+4=19(调整为4的倍数),对齐操作
上面的迭代器设计可以看看deque(这里走到边界后,要有能力回到篮子,走到下一个位置)

C++ STL(二)_红黑树_85
这里用的是C风格的字符串,没有针对C++的字符串定义,需要自己设计

C++ STL(二)_红黑树_86

C++ STL(二)_迭代器_87

C++ STL(二)_迭代器_88

篮子个数一定大于元素个数

参考