1.什么是序列式容器?什么是关联式容器?
书上给出的解释是,序列式容器中的元素是可序的(可理解为可以按序索引,不管这个索引是像数组一样的随机索引,还是像链表一样的顺序索引),但是元素值在索引顺序的方向不一定是有序的。下面这幅图是SGI STL中的各种容器,图中内缩表示内含关系,类似于UML类图里面的组合关系。比如说heap内含一个vector,表示heap是借由vector实现的。
2.vector
- vector概述
- vector的空间是动态分配的,对vector进行size()操作得到的结果是含有的实际元素的个数,capacity()操作得到的结果是当前分配空间能容纳的元素个数,这个在源代码里面一目了然,即如果有vector v,一定有v.capacity()>=v.size()
- vector具有自定义栈和数组的特性,使用时把它当做不用操心底层的栈或数组即可
- 对于vector来说,所需要的空间是连续的,重新配置空间,特别是找到一个连续的空间比之前空间还大的新空间比较耗费时间成本,因此一开始给vector的空间都要比实际需要的空间大,这样可以保证vector扩容的时候不需要每次都重新配置空间(一般扩容以2倍方式扩容,当然,别的版本stl可能实现不一样)。当然,具体实现比这里说的可能要更加精细一些,毕竟,扩充空间(包括配置新空间、数据移动和释放原有空间)都是非常耗时的操作
- vector的源码
/* __STL_USE_EXCEPTIONS */
destroy(start, finish);
deallocate();
start = new_start;
finish = new_finish;
end_of_storage = new_start + len;
}
}
}
};
- vector的数据结构
线性连续空间,一旦空间分配完成,空间类似于数组
- vector的迭代器
- 普通指针即可满足迭代器所有要求,因此vector提供的迭代器就是普通指针。
- vector构造与内存管理
- vector缺省适合用std::alloc为空间配置器,另外定义了一个data_allocator,方便以元素大小配置内存
- vector能动态增加大小,并不是在原空间的后面按续接新的空间,而是以原来大小的2倍配置另一块连续空间,将原来的内容拷贝过来,释放原空间,因此如果vector的操作一旦引起了空间重新配置,之前的迭代器就失效了。
- 在使用的过程中,vector的capacity一般只增不减,若是以类实例的方式声明的vector,等该实例的生命周期结束自然会被析构掉,但是如果是指针形式可能不会被析构掉,这是需要使用swap技法来清楚内存。
- vector相关面试题解析
- vector使用中需要注意的问题?
- 一是要注意扩容的时候迭代器,引用,指针都会失效
- 二是为了效率考虑最好在能遇见到空间需求的情况下提前分配好空间
- 三是对于元素为指针的情况注意防止内存泄露,这个可以手工,当然也可以用智能指针
- vector的迭代器不一定就是原生指针,只是说有的版本可以用指针来实现而已。所以不要将它的迭代器当做元素地址传递给严格需要元素地址的地方。
- clear操作符是不会释放vector所指内存空间的,只是让size变为了0,对capacity没影响
- vector怎么释放内存空间?
- swap技法
swap技法的原理很简单,但是不一定能完全释放,原理是将需要释放的vector(比如说v)在一个临时作用域里面与一个空的vector(比如说tmp)交换,这之后tmp出了临时作用域自动析构,就完成了v的内存释放。代码如下:int> tmp;
v.swap(tmp);
}
当然上述也可以写成函数形式,且由上面的方法也可以看出,缩减内存空间也可以用类似的方法.
3.list(很简单,不需要多说)
list的数据结构就是链表,所以空间配置起来不像vector那么麻烦,迭代器也不会出现失效的问题,但是迭代器的设计就相对比较麻烦,需要实现迭代器的接口。唯一需要注意的就是它的迭代器是bidirectional类型的,不是random的。当然,这也说明了链表是双向链表。
4.deque
- deque概述
- deque是双向开口的连续线性空间,准确地说是分段连续空间,vector在头部效率极差,所以看做是一个尾部开口的单向开口空间。
- deque的迭代器是random_accesss_iterator,要维持这种假象,要求付出更多的代码
- deque迭代器
- 既然是分段连续,要实现随机存储,制造这个假象,肯定要重载[]操作符,+,-,++,--等操作符,以让跨连续空间的迭代器移动看起来平滑,这就需要一个中控的数据结构,在deque的SGI实现中这称作map(不是STL的那种map容器),这个map是一个小段的连续空间,存有各个实际连续区块的指针,由于多了这一层间接性,每当迭代器到达实际连续区块边缘的时需要特别小心。一旦map满载,则需要扩容(同vector的重新配置,拷贝,释放原有空间三部曲)。
- 其他的都是实现问题而已,要满足迭代器的接口。
- 由原理也可以知道,deque也会存在迭代器失效问题。
- deque的操作
- 常见操作都有,当然只是实现细节问题。
5.stack
- stack概述
- stack的特点是只能存取栈顶,因此省了好多操作
- stack默认以deque实现,这种实现方法是adapter模式的典型应用
- stack实现
- SGI STL默认用deque实现stack,相当于封掉一段并禁止遍历的deque。当然亦可以在stack声明时指出第二个模板参数说明底层实现的类别。比如,list也是可以作为stack的底层实现
- stack迭代器
- 不允许遍历,迭代器无效,因此stack不提供迭代器。
6.queue
- queue概述
- queue是一个双向开口的数据结构,只有在两端提供数据访问,元素进出满足先进先出原则
- queue迭代器
- 无迭代器
- queue实现
- 底层默认用deque实现
7.heap
heap是用来实现priority_queue的底层数据结构,封装了heap的构造与内存管理操作,封装了heap算法,因此priority算法实现起来就比较容易了,heap的底部实现一般是vector,因此是有迭代器的。
8.priority_queue
简单地配接heap就好了,是一个有权值的queue,能够以较高效率(o(lgN))实现取权值最高的元素,只在两端操作,因此无迭代器。
9.slist
list是一个双向链表,有时候有点杀鸡用牛刀的感觉,当只需要用到单向链表的功能时,slist就够了,slist的迭代器是一个forward_iterator,当然,这导致它的使用有诸多限制,比如,不提供push_back(),只提供push_front()。
总结:整篇读书笔记中只有vector讲得比较仔细,因为一旦理解了vector之后,其它的理解起来就相对容易了。