《STL源码剖析》面试总结
STL简介
容器、算法、迭代器、仿函数、配接器、配置器。
容器---就是各种数据结构。STL容器是一种class template。
算法---各种常见的算法。
迭代器---扮演算法和容器中的胶合剂,是“泛型指针”。所有的STL容器均有自己的专属的迭代器。
仿函数---做算法的某种策略。仿函数是一种重载了operator()的Class或者class template.
配接器---修饰容器、仿函数、迭代器接口的东西。
配置器--负责空间配置,配置器实现了动态空间配置、空间管理、空间释放的Clas template。
STL六大组件的交互关系:
容器通过配置器获得数据存储空间,算法通过迭代器存取容器内容。仿函数可以协助算法完成不同的策略变化,配接器可以修饰仿函数或者套接。
关于容器的一些问题
1、当vector的内存用完了,它是如何动态扩展内存的?它是怎么释放内存的?用clear可以释放掉内存吗?是不是线程安全的?
- Vector内存用完了,会以当前Size大小重排申请2*size的内存,然后把原来的元素复制过去,把新元素插上,然后释放原来的内存。
- 一般我们释放vector里的元素使用clear,其实它不能释放内存,要想释放内存要使用swap。
vector<type> v; //.... 这里添加许多元素给v //.... 这里删除v中的许多元素 vector<type>(v).swap(v); //此时v的容量已经尽可能的符合其当前包含的元素数量 //对于string则可能像下面这样 string(s).swap(s);
- 引用《effective stl》的第十二条:当涉及 STL容器和线程安全性时,你可以指望一个 STL库允许多个线程同时读一个容器,以及多个线程对不同的容器做写入操作。你不能指望 STL库会把你从手工同步控制中解脱出来,而且你不能依赖于任何线程支持。必须自己去写多线程安全措施。
2、map是怎么实现的?查找的复杂度是多少?能不能边遍历边删除?
- 红黑树实现的;
- O(logn)
- 不可以,map不像vector,它在对容器执行erase操作后不会返回后一个元素的迭代器。所以不能遍历的往后删除。
3、写多读少应该用什么容器?
- 应该用链表。链表的插入操作时,时间复杂度为常数。访问某一节点,则时间复杂度为O(N)
4、vector每次insert或erase之后,以前保存的iterator会不会失效?
理论上会失效,理论上每次insert或者erase之后,所有的迭代器就重新计算的,所以都可以看作会失效,原则上是不能使用过期的内存
但是vector一般底层是用数组实现的,我们仔细考虑数组的特性,不难得出另一个结论,
- insert时,假设insert位置在p,分两种情况:
- 容器还有空余空间,不重新分配内存,那么p之前的迭代器都有效,p之后的迭代器都失效
- 容器重新分配了内存,那么p之前和之后的迭代器都无效咯
- erase时,假设erase位置在p,则p之前的迭代器都有效并且p指向下一个元素位置(如果之前p在尾巴上,则p指向无效尾end),p之后的迭代器都无效
5、hash_map和map的区别在哪里?
- hash_map底层是散列的所以理论上的操作的平均复杂度是常数时间,map底层是红黑树,理论上平均复杂度是O(logn)。下面是借鉴的网上的总结:这里总结一下,选用map还是hash_map,关键是看关键字查询操作次数,以及你所需要保证的是查询总体时间还是单个查询的时间。如果是要很多次操作,要求其整体效率,那么使用hash_map,平均处理时间短。如果是少数次的操作,使用 hash_map可能造成不确定的O(N),那么使用平均处理时间相对较慢、单次处理时间恒定的map,考虑整体稳定性应该要高于整体效率,因为前提在操作次数较少。如果在一次流程中,使用hash_map的少数操作产生一个最坏情况O(N),那么hash_map的优势也因此丧尽了。
6、为何map和set不能像vector一样有个reserve函数来预分配数据?
- map和set内部存储的已经不是元素本身了,而是包含元素的节点。也就是说map内部使用的Alloc并不是map<key, data,="" compare,="" alloc="">声明的时候从参数中传入的Alloc。例如:map<int, int,="" less<int="">, Alloc > intmap;这时候在intmap中使用的allocator并不是Alloc, 而是通过了转换的Alloc,具体转换的方法时在内部通过Alloc::rebind重新定义了新的节点分配器,详细的实现参看彻底学习STL中的Allocator。其实你就记住一点,在map和set里面的分配器已经发生了变化,reserve方法你就不要奢望了。
7、当数据元素增多时(10000和20000个比较),map和set的插入和搜索速度变化如何?
- 算一下就知道了,首先你得知道map和set的底层都是红黑树,红黑树的搜索近似于二分查找,二分查找呢,平均时间复杂度是log2n,这里简写成logn,
狂按计算器:log(10000) = 13.3
log(20000) = 14.3
看到了不,理论上平均就多了一次,所以你懂的,插起来。。。
8、auto_ptr可以做vector的元素呢?为什么?
- 不能。因为所有的容器提供的都是值语义的意思。容器执行插入元素的操作时,内部实施拷贝操作。STL的标准容器规定它所容纳的元素必须是可以拷贝构造和可被转移赋值的。而auto_ptr不能,可以用shared_ptr智能指针代替。STL容器在分配内存的时候,必须要能够拷贝构造容器的元素。而且拷贝构造的时候,不能修改原来元素的值。而auto_ptr在拷贝构造的时候,一定会修改元素的值。所以STL元素不能使用auto_ptr。
-
int costa_foo() { vector<auto_ptr<int>> v(10); int i=0; for(;i<10;i++) { v[i]=auto_ptr<int>(newint(i)); } }
答案是否定的,甚至这段代码是无法编译通过的。
错误出在这一行:
vector<auto_ptr<int>> v(10);1 auto_ptr的拷贝构造函数是这样写的: 2 auto_ptr(auto_ptr& __a) throw(): _M_ptr(__a.release()) 3 { 4 }
可以看出来, 在拷贝构造一个auto_tr的时候, 必需要把参数的那个auto_ptr给release掉,
然后在把参数的_M_ptr指针赋值给自己的_M_ptr指针。补充说明一下, _M_ptr是auto_ptr的一
个成员,指向auto_ptr管理的那块内存,也就是在auto_ptr生命周期之后被释放掉的那块内存。auto_ptr内部有一个指针成员_M_ptr,指向它所管理的那块内存。而拷贝构造的时候,首先把参数的_M_ptr的值赋值给自己的_M_ptr,然后把参数的_M_ptr指针设成NULL,。如果不这样设计,而是直接把参数的_M_ptr指针赋值给自己的, 那么两个auto_ptr的_M_ptr指向同一块内存,在析构auto_ptr的时候就会出问题: 假如两个auto_ptr的_M_ptr指针指向了同一块内存,那么第一个析构的那个auto_ptr指针就把_M_ptr指向的内存释放掉了,造成后一个auto_ptr在析构时释要放一块已经被释放掉的内存,这明显不科学,会产生程序的段错误而crash掉。 而shared_ptr则不存在这个问题, 在拷贝构造和赋值操作的时候,只会引起公用的引用计数的+1,不存在拷贝构造和赋值操作的参数不能是const的问题。 总结: 1 auto_ptr不能作为STL标准容器的元素。 2 auto_ptr在拷贝复制和赋值操作时,都会改变参数。这是因为两个auto_ptr不能管理同一块内存。
9、容器共性机制
- 所有容器提供的都是值语意;
- 容器执行插入元素的操作时,内部实施拷贝动作,故STL存储的元素都必须能够被拷贝。
- 除queue和stack,每个容器都可以返回迭代器的函数,运用返回迭代器就可以访问元素。
- STL通常不跑出异常,故要求使用者确保传入正确的参数。
10、vector迭代器和deque迭代器的区别:
vector迭代器的几种试下情况:
1、当插入(push_back)一个元素后,end操作返回的迭代器肯定失效。
2、当插入(push_back)一个元素后,capacity返回值与没有插入元素之前相比有改变,则需要重新加载整个容器,此时first和end操作返回的迭代器都会失效。
3、当进行删除操作(erase,pop_back)后,指向删除点的迭代器全部失效;指向删除点后面的元素的迭代器也将全部失效。
deque迭代器的失效情况:
1、在deque容器首部或者尾部插入元素不会使得任何迭代器失效。
2、在其首部或尾部删除元素则只会使指向被删除元素的迭代器失效。
3、在deque容器的任何其他位置的插入和删除操作将使指向该容器元素
只有list的迭代器好像很少情况下会失效。也许就只是在删除的时候,指向被删除节点的迭代器会失效吧,其他的还没有发现。
先看两条规制:
1、对于节点式容器(map, list, set)元素的删除,插入操作会导致指向该元素的迭代器失效,其他元素迭代器不受影响。
2、对于顺序式容器(vector)元素的删除、插入操作会导致指向该元素以及后面的元素的迭代器失效。
关于迭代器的一些问题
traits技术原理及应用
- 简单的来说,在STL算法中会使用到迭代器,会用到迭代器所指之物的型别,假设算法中有必要声明一个变量,以迭代器所指之物为型别,但是C++只支持Sizeof(),并未支持typeof()。即使typeid()也只能获得型别名称,不能拿来声明变量,所以这里就要用到作为”特性萃取机“的traits技术。当然,要让traits有效运作,每个迭代器设计的时候的遵守约定,自行以内嵌型别定义的方式定义出相应型别。
- 比如:若声明一个变量,变量类型时迭代器所指之物的型别?该怎么办? 由于RTTI性质的typeid()获得的也只是型别名称,不能拿来做变量声明之用。解法:利用function template的参数推导机制调用一个函数机模板,编译期会自动进行template参数推导。但是函数模板只能推导的是参数类型,不能推导函数的返回值型别。因而就采用了Traits编程技法。
- 若要推导的是函数返回值类型型别,则当迭代器是Class type时,声明内嵌型别(只有迭代器时class type时才可以内嵌型别),且函数的返回类型必须加上关键词typename。
tmplate<class T> struct MyIter { typedef T value_type;//内嵌型别 .... }; tmplate<class I> typename I::value_type func(I iter) { .... } MyIter<int> iter(new int(8)); cout << func(iter);
- 若推导的是函数返回值类型型别,但是迭代器不是class type且推导出返回值型别。此时要做偏特化处理。类模板拥有一个以上的template参数,可以对其中某个template参数进行特化工作。偏特化仅仅适用于T为原生指针情况,便解决迭代器不是Class Type型别,原生指针不是迭代器。原生指针不是Class,则无法为他们定义内嵌型别。
关于算法的一些问题
快排算法的枢轴位置是怎么选择的?
- 三点中值法,取整个序列的头、尾、中央三个位置的元素,以其中值作为枢轴。
简单说一下next_permutation和partition的实现?
- next_permutation(下一个排列)
首先,从最尾端开始往前寻找两个相邻元素,另第一个元素为i,第二个元素为ii,且满足i<ii。找到这样一组相邻元素后,再从尾端开始往前检验,找出第一个大于i的元素j,将i,j元素对调,再将ii之后的所有元素颠倒排列。此即所求“下一个”排列组合。 - partition
令头端迭代器first向尾部移动,尾部迭代器last向头部移动。当first所指的值大于或等于枢轴时就停下来,当last所指的值小于或等于枢轴时也停下来,然后检验两个迭代器是否交错。如果first仍然在last左边,就将连着元素互换,然后各自调整一个位置(向中央逼近),再继续进行相同的行为。如果发现两个迭代器叫错了,表示整个序列已经调整完毕。
关于内存配置的一些问题
1、stl对于小内存块请求与释放的处理
- STL考虑到小型内存区块造成的碎片问题,设计了双层级配置器,
- 第一级配置器直接使用malloc() 和 free()
- 第二级配置器采用不同的策略。
- 当配置区大于128bytes时,直接调用第一级配置器;
- 当配置区块小于128bytes时,便不借助第一级配置器,而使用一个memory pool来实现。
- 究竟是使用第一级配置器还是第二级配置器,由一个宏定义来控制。SGI STL中默认使用第二级配置器。二级配置器会将任何小额区块的内存需求量上调至8的倍数,(例如需求是30bytes,则自动调整为32bytes),并且在它内部会维护16个free-list, 各自管理大小分别为8, 16, 24,…,128bytes的小额区块,这样当有小额内存配置需求时,直接从对应的free list中拔出对应大小的内存(8的倍数);当客户端归还内存时,将根据归还内存块的大小,将需要归还的内存插入到对应free list的最顶端。
小结:STL中的内存分配器实际上是基于空闲列表(free list)的分配策略,最主要的特点是通过组织16个空闲列表,对小对象的分配做了优化。
1)小对象的快速分配和释放。当一次性预先分配好一块固定大小的内存池后,对小于128字节的小块内存分配和释放的操作只是一些基本的指针操作,相比于直接调用malloc/free,开销小。 2)避免内存碎片的产生。零乱的内存碎片不仅会浪费内存空间,而且会给OS的内存管理造成压力。 3)尽可能最大化内存的利用率。当内存池尚有的空闲区域不足以分配所需的大小时,分配算法会将其链入到对应的空闲列表中,然后会尝试从空闲列表中寻找是否有合适大小的区域, 但是,这种内存分配器局限于STL容器中使用,并不适合一个通用的内存分配。因为它要求在释放一个内存块时,必须提供这个内存块的大小,以便确定回收到哪个free list中,而STL容器是知道它所需分配的对象大小的,比如上述: stl::vector array; array是知道它需要分配的对象大小为sizeof(int)。一个通用的内存分配器是不需要知道待释放内存的大小的,类似于free(p)。