文章目录
第二讲 从源代码角度讲解STL(上)
1. OOP(面向对象编程)vs. GP(泛型编程)
1. OOP(Object-Oriented programming)
- OOP企图把datas和methods关联在一起;
- 例如STL中的list有自带的sort函数;
- 为什么list不能使用全局的sort函数?
- 因为::sort()函数要求的迭代器类型是RandomAccessIterator(随机访问迭代器。支持O(1)时间复杂度对元素的随机位置访问,支持对元素的读取。),迭代器可以随意移动;
- 而list的迭代器是BidirectionalIterator(双向迭代器。支持向前向后逐个遍历元素,可以对元素读取。);
- 所以list不能使用::sort()函数,要自定义sort()函数。
2. GP(Generic Programming)
-
GP总是将datas和methods分开来;
- SLT中的datas(Containers)和methods(Algorithm)需要借助Iterators才能相互联系;
- SLT中的datas(Containers)和methods(Algorithm)需要借助Iterators才能相互联系;
-
algorithms(算法)的本质:
- 有两个版本的max函数;
- 有两个版本的max函数;
2. allocators(分配器)
- allocator class最重要的部分:
- allocate()函数:用于分配空间;
- deallocate()函数:用于释放空间;
1. operator new() 和 malloc()、operator delete() 和 free()
- 任何allocators进行分配空间都会执行operator new(),最终执行malloc()函数;
- malloc()函数属于C标准库函,底层会调用操作系统的相关API;
- 右图是malloc()实际分配的空间,会有一些额外内容开销overhead;
- 而释放空间都会执行operator delete(),最终执行free()函数;
- 同样free()函数属于C标准库函,底层会调用操作系统的相关API;
- 同样free()函数属于C标准库函,底层会调用操作系统的相关API;
2. 不同版本标准库的allocator
1. VC6(Visual C++ 6)的std::allocator
- VC6 Containers 使用的默认allocator是 std::allocator;
- VC6的allocator只是给 ::operator new(malloc) 和 ::operator delete(free) 简单地套一层外壳 allocate 和 deallocate ,没有特殊设计;
- 直接使用allocator class,释放也需要指定大小,这种设计不好(没人会记住当初分配了多大的空间);
- 注意:
- allocate函数有一个没有名字的形参,这种写法表示这个参数在这个函数里是没有用的(因为没有名字,不知道这种写法有什么用);
- allocate函数有一个没有名字的形参,这种写法表示这个参数在这个函数里是没有用的(因为没有名字,不知道这种写法有什么用);
2. BC5(Borland C++ 5)的std::allocator
- BC5 Containers 使用的默认allocator是 std::allocator;
- 同样没有特殊设计;
3. 没有特殊设计allocator带来的影响
- 由前面第一节,可以知道malloc分配的内存会有额外overhead;
- 我们关心的是,这个overhead占总数据大小的比例,而不是overhead本身的大小;
- 至少首尾表示空间大小的cookie,是肯定不能少的;
- 如果现在需要申请1000000个小数据(比较小的类),只是简单包装的allocator就会调用了1000000次malloc,这样会导致实际数据占用总空间的比例要远小于overhead的比例,这是不能容忍的;
- 因此如果能设计一个allocator,尽可能减少调用malloc的次数,就能降低overhead的比例。
4. G2.9(GUN)的std::allocator
-
同样没有特殊设计;
- 但右边的注释说明了,G2.9并没有使用这个std::allocator;
- 但右边的注释说明了,G2.9并没有使用这个std::allocator;
-
注意:G2.9 Containers 使用的默认allocator是std::alloc,不是 std::allocator ;
-
std::alloc的实现结构:
- 结构:
- 定义一个16长度的指针数组:每个指针指向了8~128个字节大小的单向链表;
- 例如:0索引对应的大小 == 8个字节,1索引对应的大小 == 16个字节,…,15索引对应的大小 == 128个字节;
- 即0索引指向的单向链表,每块大小都是8个字节;
- 作用:
- 尽可能减少调用malloc的次数;因为每次malloc得到的内存都需要overhead;
- 这些空间块都可以直接使用,不需要调用malloc向操作系统申请内存;如果空间块不足,才会调用malloc操作;
- 具体的分配过程在内存管理视频中,这里不详谈:
- 结构:
5. G4.9(GUN)的std::allocator:
-
G4.9 std::allocator 使用了继承;
-
std::allocator 没有特殊设计;
-
注意:G4.9 Containers 使用的默认allocator又用回了没有特殊设计,只是做包装的std::allocator;
-
为什么不用 G2.9 的 std::alloc,std::alloc到哪去了?
- 之前谈到除了标准库的std::allocator,还有几个额外非标准的分配器;
- G2.9 的 std::alloc 变成了 pool_alloc(内存池分配器);
3. 容器之间的关系
1. C++11之前的容器关系
- 下图中的缩进表示复合关系(composition);【例如set拥有rb_tree(红黑树);】
- 下图中的heap是堆数据结构,不是系统的heap内存区;
- 左右两侧是G2.9和G4.9,对应容器的sizeof()大小;
2. C++11之后的容器关系
- 从C++11开始,STL引入了大量的继承关系;
- C++11容器修改(图的下方方框):
- 图中的slist改名forward_list;
- hash_set,hash_map改为unordered_set,unordered_map;
- hash_multiset,hash_multimap改为unordered_multiset,unordered_multimap;
- 新增array;
4. Containers 为什么要使用 Iterators
- array和vector这种内存空间分布连续的容器,它的 iterator 直接使用指针就可以满足需求,iterator 的行为就是指针的行为;
- 而对于其他的容器,它的 iterator 必须是一个class,并且要使 iterator 的行为像 pointer;
- 比如 pointer 的++操作:
- 对于连续空间分布的容器,可以很容易使用 pointer(也是迭代器)访问元素;
- 对于离散空间分布的容器,直接使用 pointer 根本不能访问其他的元素,所以需要对应的 iterator 实现这个操作;
- 比如 pointer 的++操作: