主要内容:
本节主要讲解了面向对象和泛型编程的区别,以及source code所涉及到的基础知识(包括运算符重载、各种模板等),还有利用源码深入剖析了分配器、容器(list, vector, array, forward_list等)、迭代器。
源码之前,了无密码。
1.源码所在目录
- visual c++源代码文件:..\include 下面有这个c++标准库的头文件(自己的环境vs 2017,C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Tools\MSVC\14.10.25017\include在这个目录下)
- gnc c++4.9.2 头文件的位置:..\4.9.2\include\c++
stl开头的头文件:\include\c++\bits
扩充的(分配器)头文件:\include\c++\ext
2. oop(面向对象)与gp(泛型编程)
- oop企图数据和操作放在一起。而gp是将数据和操作分开,容器存放数据,算法实现各种操作,它们之间通过迭代器联系起来。gp的好处在于容器和算法相互独立,他们之间只要具有可以相互使用的接口即可。eg. ::min() 对于两个类对象比较大小,调用这个算法,就要重载小于号或是自定义比较大小的函数。
- list不能调用::sort(),因为只有随机访问的迭代器才能调用::sort(),list不支持随机访问。所以list只能用自己的sort.
- 算法对于元素本身的操作,最终都是比较大小?。
3. 技术基础--运算符重载、模板
四个运算符不能重载: :: , . , .*, :?
类模板、函数模板、成员模板
类模板中又分为泛化、特化、偏特化(局部特化,个数和范围上)
template <typename T> struct __type_traits {...} // 泛化
template<> struct __type_traits<int> {...} // 特化
template <class Iterator> struct iterator_traits {...} // 泛化
template <class T> struct struct iterator_traits<T *> {} //偏特化,传入指针
template <class T> struct struct iterator_traits<const T *> {} //偏特化,传入常量指针
4. 分配器allocators
- malloc会在申请分配的内存大小的基础上附加额外的空间,如果申请分配的内存小,那么额外的空间所占的比例就大。
- 分配器是用来分配内存的,对于VC下的allocator类中有两个重要的函数allocate和deallocate. allocate函数内部会调用::operator new,operator new内部会调用malloc. deallocate内部会调用::operator delete,都没有任何特殊设计。
使用方法:
eg. int *p = allocator<int>().allocate(512, (int *)0); //首先是创建一个临时allocator对象,之后调用allocate函数分配512字节,且全部初始化为0。
allocator<int>().deallocate(p, 512); //释放空间,这里要求告知分配的空间大小。
- 对于BC5下的allocator类,头文件<memory.stl>. 且也是利用::operator new和::operator delete实现allocate和deallocate的,都没有任何特殊设计。
使用方法:
eg. int *p = allocator<int>().allocate(512); //allocate函数第二个参数用默认值是0.
allocator<int>().deallocate(p, 512);
- 对于GCC2.9下也是利用::operator new和::operator delete实现allocate和deallocate的,都没有任何特殊设计。GCC2.9在容器中采用的不是allocator,而是alloc, 头文件<stl_alloc.h>. 在扩展空间时,尽量减少malloc次数,做法是创建16条链表,每条链表存放的字节是以8的倍数增长,第0条链表存放8字节,第1条链表存放16字节...。容器会和分配器要内存,容器中元素的大小会被调整为8的倍数,分配器会找到相应的链表上,再调用malloc分配内存,一个链上的每个内存块不带有上下cookie(8字节),只在链的头尾带有cookie,从而大幅节省额外空间开销。
- GCC4.9则没有在沿用GCC2.9这种好的方式,而是使用的是std::allocator, 继承于new_allocator, 且又是采用和vc和bc一样的分配和回收方法。GCC4.9中alloc还存在,只是名字改为了__pool_alloc. eg.vector<string, __gnu_cxx::__pool_alloc<string>>vec;
5. 容器之间实现关系
对于GCC2.9,属于早期的标准库版本,容器之间是复合的关系,而不是继承的关系。heap中拥有vector,priority-queue中拥有heap, stack和queue都拥有deque. 关联容器set、map、multiset、multimap都拥有rb_tree(高度平衡的二叉树).
6. 深度探索list
- 容器中要提供迭代器,和一系列操作符重载。迭代器内部要定义迭代器相关的5种typedef。
- ++运算符的重载:前++没有参数,后++是有一个函数参数的,为了与前++进行区分。后++首先会用tmp记录原有的指针,再对原有的指针自加,最后返回tmp。
self operator++(int)
{
self tmp = *this; //会调用iterator的拷贝构造函数是对list中的node进行拷贝,并不是调用operator*。
++*this;
return tmp; //这里返回的是对象。而前++返回的是reference。
}
对于整数变量,前++可以加两次,后++则不行。迭代器的自加也是按照这个原则设计的。
- GCC4.9相对于GCC2.9在list容器上的改进:1. 其中的_list_iterator类模板的typename进行了精简,由3个typename改为1个typename. 2. _list_node在2.9中是由data和两个void_pointer类型指针组成,而在4.9中则是先构造_list_node_base这个结构存放两个指向自己这种类型的指针,之后_list_node继承于它,并存放data,这样指针指向的类型就确定了。3. 在32位cpu下,4.9中sizeof(list<int>) = 8,有两个指针,分别指向prev和next节点; 2.9中是4,只有一个指向node的指针。
- list最后一个有数据的节点的下一个节点是空的,end()指向的就是这个节点。这样是为了前闭后开遍历设计。
7. iterator需要遵循的原则
- 迭代器内部定义的5种迭代器相关的typedef是为了给算法调用的。
eg. rotate()算法的内部调用iterator_traits<_Iter>::iterator_category (); 萃取出iterator_category(迭代器的分类,如前向迭代器,只能++,或是双向迭代器等),还萃取出difference_type(两个iterator之间的距离)和value_type(容器中元素类型)。和迭代器相关的还有两种typedef,分别是reference和pointer从没有在标准库中使用过。 - iterator_traits(迭代器的萃取机)有一个泛化的类模板,如果iterator是class,那么会使用这个泛化的类模板,还有两个偏特化类模板,一个模板参数特化为T ,另一个特化为const T。所以无论iterator是类还是指针,都可以萃取其中的5种类型,具体使用eg. iterator_traits<T>::value_type。value_type主要是用于声明变量的,所以即使是const T*类型,value_type也是T。
- 标准库中还有其他的萃取机。
8. 深入探索vector
- 各个编译器下,容量都会2倍增长。vector中有三个iterator(start指向第一个元素的位置,finish指向当前存放的最后一个元素的下一个位置,end_of_storage指向可以容纳最后一个元素的位置),vector的函数begin(),end(),size(),capactity(),empty(),front(),back(),operator[]等都是利用这三个iterator计算的。
- push_back()首先会利用finish和end_of_storage指针判断是否还有空闲位置,如果有,则插入元素,finish自加;如果没有,那么会调用辅助函数insert_aux(end(), x),这是一个函数模板,其内部会再次判断是否还有剩余空间,因为其他函数也可以调用这个函数,如果没有剩余空间,那么会分配原有空间大小的二倍,(如果原有空间是0,那么新的空间大小会是1),之后将原有的元素拷贝到新的空间,再将新的元素放进去,finish自加,之后会在新元素后面(安插点后)将剩余的原有元素拷贝过来(这里也是因为这个函数会被其他函数调用)。
- 32位下sizeof(vector<T>) = 12(有3个指针)。
9. array和forward_list 单向链表
- TR1版(是1998-2011中间的过度版本,大概是2003左右)
array内部是用数组实现的,大小不能扩充,所以定义时要声明大小,eg. array<int, 10> arr; 如果给定的array的大小是0,那么在array内部会变为1. 这里的iterator是指针。 - G4.9 array内部声明数组变得更加复杂了。
typedef int T[100]; T c; 这样写是对的。
10.forward_list
- 与list类似,只是是单向的。