c++标准库体系结构与内核分析笔记
文章目录
泛型编程(Generic Programming,GP)
即使用模板为主要工具来编程。
c++标准库SL
由c++编译器提供的各种头文件。
c++标准模板库STL
占据c++标准库的大部分。并含有六大部分。
STL的编写者们规定,他们写的组件都放在 std 这个命名空间中。
STL六大部件
容器 (是一个class template)
分配器(是一个 class template)
算法 (是一个function template)
迭代器 (是一个class template)
适配器 (是一个class template)
仿函数 (是一个class template)
关系图:
我们先从容器看起,在使用容器时,我们存入取出都涉及到内存的问题,但是这些问题我们并不需要考虑,因为有分配器做容器的内存分配的支撑。
当我们的容器中有数据后,也需要对这些数据进行操作,一些常用的操作本封装成函数,称作算法。
这我们就发现STL在设计思想上就与OOP(面向对象编程)不同,即STL是数据与方法分离,而OOP是将数据与方法封装进同一个类里。
既然数据与方法分离了,那么算法想操作容器,就需要迭代器。
容器——结构与分类
注:若类a想要使用类b的方法,可以让a继承b,或者让a含有一个b(即复合)。
序列式容器(sequence containers)
1、array
我们常用的数组在c++11以后也被定义为一个类,从图中可以看出它两端封死,长度无法改变。
创建时要指定大小:array<int,10>(当大小为0时,默认当作1)
2、vector
单向自动增长数组。
vector类中保持三个迭代器:start、finish(元素末)、end_of_storage(容量末)。
所谓自动增长,就是元素数量要超过容量时,vector自动reallocation一定倍数(有的是2倍,有的1.5等等)。
新空间不是接在原内存后面,而是另找一块足够的内存,将原数据拷贝过来,并将原vector释放。
3、deque
双端队列。
deque支持随机存取,但实际上它的存储是分段的,而不是连续的。如图:
迭代器有四个成员cur(指向一段空间的数据所在位置)、first(指向数据所在段的首)、last(指向数据所在段的尾,满足前闭后开)、node(指向段地址,这个段地址在专门存储每段地址的空间中)
而queue和stack都是基于deque实现的,只是禁用了deque的一些方法。
所以默认是queue<string,deque<string>>
但是queue和stack也可以基于其他容器实现(只要该容器能提供queue或stack所需的所有方法)
如:stack<string,list<string>>
注:queue和stack都可以基于list,都不可基于map、set,queue不可基于vector,而stack可以。
注:queue和stack不提供iterator,不允许遍历。
4、list
双向链表。
源码分析:
list中只有一个属性(数据):node(是一个指针list_node*)。
而list_node含有三个属性:prev(指针)、next(指针)、data(T)。
list的迭代器list_iterator重载的操作:
1)*
return (*node).data;
2)->
return &(operator*());
3)++
这是重载前置++
node=(link_type)((*node).next);
4)++(int)
这是重载后置++
STL的实现中,刻意在环状list尾端加一空白的节点,以符合“前闭后开”区间。
5、forward-list
c++11新增的单向链表。
关联式容器(associative containers)
特点:基于红黑树实现,元素含有key和value,通过key快速查找value。
红黑树(rb_tree)
是一种平衡二分搜索树,平衡即深度大致相同,没有任何一个节点过深。
它的排列规则有利于search和insert。
红黑树提供遍历操作以及迭代器。
按正常规则++it即可遍历,得到排序状态。
但不应通过其迭代器来修改值(红黑树没有禁止,但是基于红黑树的容器禁止了)。
红黑树的insert分为insert_unique()和insert_equal(),即不可添加重复元素和可添加重复元素。
注:keyofvalue用于从value中取key。
1、set/multiset
set中key和data合二为一。
所有的操作都由set中封装的那个红黑树t来做,所以一定程度上可以把set理解为container adapter。
2、map/multimap
map既有key也有data,同样含有一个红黑树。
它可以通过迭代器修改data的值。
map重载的[]符号会通过key找data,若key不存在,则查入此key。
不定序容器(unordered containers)
这是c++11后新出现的容器(其实也属于关联式容器),可以当哈希表来用:
hashtable
初始化hashtable时要指定桶(buckets)的个数(常为质数),每个桶后面跟着一条链表,用于存放冲突的数据。
当一条链表的长度大于桶的个数时,认为该链表过长(查找效率低下),调用rehashing,扩展桶的数量,并重新安排链表位置。
分配器
分配器可以用在定义容器时的模板参数中,控制内存分配的方式,默认使用的分配器是:std::allocator<_Tp>
分配器也可以单独直接使用(而不是配合容器),因为它也是一个类。不过没有必要。
注:单独使用分配器释放内存时,需要写出释放内存的大小,很麻烦。如:
int*p=allocator<int>().allocate(512,(int*)0);
allocator<int>().deallocate(p,512);
VC\GCC等编译器的allocator只是以::operator new和::operator delete完成allocate()和deallocate(),没有任何特殊设计。
/*别看
甚至GCC2.9注释说明,他们在STL中不使用allocator这个分配器,而是使用alloc。(4.9后又默认使用allocator,alloc换名为_pool_alloc)
没有任何特殊设计的allocator在内存分配上最终是调用malloc()。而malloc分配的内存都要在首尾附加的一些信息(overhead),你分配的内存越小,overhead的相对占比就越大,内存使用效率越低。
而alloc采用了16个链表分别存八的倍数的内存,进而使内容不需要每个都带overhead,效率提高。
*/
迭代器
interator在设计时要遵循一定的原则,它必须规定好自己的类别(或属性),以回答traits的“提问”,所谓traits是类似萃取机一般的东西,用于得到对应事物(不只是迭代器)的特征(或类别、或属性)。
例如:
return typename iterator_traits<...>::iterator_category();
//返回迭代器种类。
return typename iterator_traits<...>::value_type();
//返回容器中所放元素种类。
//除此外还有difference_type、reference、pointer一共5种。
这5种属性在迭代器类的实现中都有定义,以便算法的询问(使用)。
由图中左侧框内内容看出,如果迭代器是一个类的话,算法可以直接访问它的属性,但是当一个迭代器并不是类(比如自然指针)时,就必须依赖于traits了。
traits使用偏特化(见本文”模板的特化“)来实现对class iterator和non-class iterator的区分:
关于萃取机:
除iterator traits外,还有
type traits
char traits
allocator traits
pointer traits
array traits
迭代器种类
struct input_iterator_tag{};
struct output_iterator_tag{};
struct forward_iterator_tag:public input_iterator_tag{};
//单向不能跳,如forward_list
struct bidirectional_iterator_tag:public forward_iterator_tag{};
//双向不能跳,如list、set、map
struct random_access_iterator_tag:public bidirectional_iterator_tag{};
//随机存取,如array、vector、deque...
注:至于unordered容器,它的迭代器类型取决于桶后链表的单双向。
我们可以使用如下方式获得迭代器名字:
#include<typeinfo>
cout<<typeid(itr).name();
//itr是一个迭代器。
注:迭代器间存在继承关系。
算法
容器对于算法是透明的,算法只能通过迭代器获取容器的信息。
算法的模板类似这个样子:
template<typename iterator,typename cmp>
algorithm(iterator it1,iterator it2,cmp cmpfunc){
...
}
由于算法要依赖于迭代器,所以迭代器的类型对算法具有较大影响。
不同的迭代器允许算法有不同的操作,所以算法一般的结构都是在“主函数”中得到迭代器种类,之后跳转到重载的合适函数中执行。(function template没有特化,而是用重载)
又因为迭代器具有继承关系,由于多态性,所以不需要重载每一种迭代器,妙。
注:容器本身自带的算法比标准库提供的算法要快。
find算法也如上。
仿函数
常常用到容器或算法的参数中,来规定如何操作,就像从小到大(默认)或从大到小。
举例:
注意图中的仿函数都有继承的类,而我们自己写的函数或仿函数(重载了“()”)虽然也可以用,但是不继承这些类就不能算融入STL体系中(像算法问迭代器问题一样,适配器要问仿函数一些问题,同样如果得不到回答就会出错)。
这些类是:
unary_function(单操作数)
binary_function(双操作数)
适配器
起到一种桥梁的作用,如适配器a对b进行一定的修饰来满足被使用的需求,即通过a来使用真正做事的b。
我们知道a想要使用b的功能,编程上有两种做法,即:a继承b、a含有b。
在适配器中,使用“含有”来实现。如:
容器适配器:
如:stack和queue都是内含一个deque,然后只开放需要的功能,并改改函数名等。
迭代器适配器:
1、rbegin和rend
2、inserter(重载=,让赋值变为插入)
函数适配器:
和算法要问迭代器问题一样,函数适配器也要问仿函数一些问题:
1、binder2nd(固定第二个参数),为了方便使用,STL提供了bind2nd。注:binder2nd和bind2nd都被建议用bind取代。
2、not1(对仿函数返回值取反)
3、bind
其他适配器:
1、ostream_iterator
2、istream_iterator
其他组件
tuple
将多种基本数据类型组合成一个新的类型。
用法:
实现:
可以看到模板中有 template… 和 Tail… 的字样,这里 … 不是代码有省略,而是一种语法,表示数量可扩展。(可变参数模板)
tuple初始化时每次赋值一个head,然后继承只包含tail的tuple继续为新head赋值。
type_traits
如我们之前见到的算法问迭代器的traits一样,traits也可以问类的许多属性。
使用起来如:cout<<is_abstract<myclass>::value;
OOP(面向对象编程) vs GP(泛型编程)
oop企图将数据和方法联系在一起,即都放到类里。
而gp却想将数据与方法分开来。
gp这样做的好处是:
容器和算法的团队可以各自闭门造车,通过迭代器沟通。
模板的特化
一般认为,一个模板的类或函数可以接收任意的类型作为模板参数(泛化模板)。但是往往会遇到一种情况,就是当你传入的类型为某几种特别的类型时,模板类或函数对这种类型有更高效的实现方法,此时可以使用模板的特化(全特化)。
偏特化:如,只特化部分模板参数,或特化传入的参数为指针的情况。
零碎知识点
1、前闭后开区间
意思是指begin()迭代器指向第一个元素,而end()迭代器指向最后一个元素的后一个位置。(注,容器空时,begin同于end)
2、容器的遍历
一种老方法就是从begin一直++到end。
从c++11以后就可以使用
for(decl : coll){
statement
}
//例如
vector<double> myVector;
...
for(auto elem : myVector){
cout<<elem;
}
3、abort()函数与exit()函数区别
exit和abort都是用来终止程序的函数,他们的不同如下:
exit会做一些释放工作:释放所有的静态的全局的对象,缓存,关掉所有的I/O通道,然后终止程序。如果有函数通过atexit来注册,还会调用注册的函数。不过,如果atexit函数扔出异常的话,就会直接调用terminate。补充一下,如果是用c++的话,exit调用的时候,对象还是不会被正确析构的,所以在exit前一定要释放应该释放的资源,特别内核驻留的像共享内存之类。
abort:立刻terminate程序,没有任何清理工作。
4、所有算法,其中最终涉及到元素本身的操作无非就是比大小。
5、对于整型的++
int i=7;
++++i; //即++(++i),允许。
i++++; //即(i++)++,不允许。
6、大小为零的class
如空class或仿函数class,编译器将其大小规定为1。
7、继承typedef
子类可以继承使用父类的typedef,这在STL中常常使用。
8、typedef和typename
类型说明typedef
类型说明只定义了一个数据类型的新名字而不是定义一种新的数据类型。定义名表示这个类型的新名字。
类型解释Typename
Typename关键字告诉了编译器把一个特殊的名字解释成一个类型,在下列情况下必须对一个name使用typename关键字:
1) 一个唯一的name(可以作为类型理解),它嵌套在另一个类型中的。
2) 依赖于一个模板参数,就是说:模板参数在某种程度上包含这个name。当模板参数使编译器在指认一个类型时产生了误解。保险起见,你应该在所有编译器可能错把一个type当成一个变量的地方使用typename。