我们在使用C++的容器的时候是否想过C++的内部给我们提供的容器内部的实现机制?作为一个C++开发者了解一些实现的内部机制使得能够更好的把握实现机制。
1、vector内部实现机制?
vector内部实现时候是一个在堆上建立的一维数组,它拥有一段连续的内存空间,并且起始地址不变,因此它能非常好的支持随即存取,即[]操作符。vector因为存储在堆上,所以支持erase( ), resieze()(重新划分容器容量)等操作;
vector不用担心越界当空间不够用的时候,系统会自动按照一定的比例(对capacity( )大小)进行扩充。在vector序列末尾添加(push_back( ))或者删除(pop_back( ))对象效率高,在中间进行插入或删除效率很低,主要是要进行元素的移动和内存的拷贝,原因就在于当内存不够用的时候要执行重新分配内存,拷贝对象到新存储区,销毁old对象,释放内存等操作,如果对象很多的话,这种操作代价是相当高的。
为了减少这种代价,使用vector最理想的情况就是事先知道所要装入的对象数目,用成员函式 reserve( )预定下来;vector最大的优点莫过于是检索(用operator[ ])速度在这三个容器中是最快的,
a、vector为了支持快速的随机访问,容器中的元素以连续的方式进行存储;
b、当往容器内添加一个新的元素时,如果容器中没有空间容纳新的元素,此时,由于元素必须连续存储以便索引访问,所以不能在内存中随便找个地方存储这个心元素。于是,vector必须重新分配存储空间,用来存放原来的元素以及新添加的元素:存放在旧存储卡就中的元素被复到新存储空间里,借着插入新元素,最后撤销旧的存储空间。为了实现vector容器的快速内存分配,在实际分配过程中的容量要比当前所需的空间多一些,vector容器预留出这些额外的存储区,用于存放心添加的元素。于是,不必为每一个新元素重新分配容器。(以最小的代价连续存储元素从而带来访问元素的遍历以弥补其存储空间代价)
c、由于vector进行连续存储,所以在vector里面需要定义一个容量capacity的概念,vector的size指的是当前容器中所具有的元素的个数,而capacity则指的是容器在需要重新分配内存空间前所能存储的元素总数,reserve则表示容器需要预留多少个元素的存储空间。capacity>=size,标准库中提供了reserve和capacity函数,程序员可以自己定义额外的存储空间或者容量。当vector的当前容量已经被消耗完全的时候,容器不得不分配新的存储空间,以加倍当前容量的分配策略是先重新分配。当然,vector的每种是先可自由选择内存分配策略,必要时才进行分配,分配多少内存取决于其具体的实现方式。
d、在实现内部容器的地址扩充的时候可以采用扩充固定长度(当前长度的一半)的方式,而不是每次都新增需要增加的元素的长度,这样可以进一步的提高vector的操作速度。
如下:
1、初始化部分
vector< int > ivec;
cout << ivec.size() << << ivec,capacity() << endl;
for( vector< int >::size_type ix = 0;ix != 24;++ix)
ivec.push_back(i);
cout << ivec.size() << << ivec,capacity() << endl;
输出结果: 0 0 24 32
2、预留额外空间
ivec.reserve(50);
cout << ivec.size() << ivec.capacity() << endl;
while(ivec.size() != ivec.capacity())
ivec.push_back(0);
cout << ivec.size() << << ivec,capacity() << endl;
输出结果:24 50 50 50
3、重新分配空间
ivec.push_back(42);
cout << ivec.size() << << ivec,capacity() << endl;
输出结果:51 100
2、list内部实现机制?
list的本质是一个双向链表,内存空间不连续,通过指针进行操作。因为是链表它的高效率首先表现是插入,删除元素,进行排序等等需要移动大量元素的操作。显然链表没有检索操作operator[ ], 也就是说不能对链表进行随机访问,而只能从头至尾地遍历,这是它的一个缺陷。list有不同于前两者的某些成员方法,如合并list的方法splice( ), 排序sort( ),交换list 的方法swap( )等等。
a、不支持下标操作
b、当往容器内添加一个新的元素时,标准库只需要创建一个新的元素,然后将新元素连接在已存在的链表中,不需要重新分配存储空间,也不必复制任何已经存在的元素。
3、deque内部实现机制?
双端队列(deque)是一种支持向两端高效地插入数据、支持随机访问的容器,双端队列的数据被表示为一个分段数组,容器中的元素分段存放在一个个大小固定的数组中,此外容器还需要维护一个存放这些数组首地址的索引数组,如下图所示。
(转载图片)
由于分段数组的大小是固定的,并且它们的首地址被连续存放在索引数组中,因此可以对其进行随机访问,但效率比vector低很多。向两端加入新元素时,如果这一端的分段数组未满,则可以直接加入,如果这一端的分段数组已满,只需创建新的分段数组,并把该分段数组的地址加入到索引数组中即可。无论哪种情况都不需要对已有元素进行移动,因此在双端队列的两端加入新的元素都具有较高的效率。
当删除双端队列容器两端的元素时,由于不需要发生元素的移动,效率也是非常高的。双端队列中间插入元素时,需要将插入点到某一端之间的所有元素向容器的这一端移动,因此向中间插入元素的效率较低,而且往往插入位置越靠近中间,效率越低。删除队列中元素时,情况也类似,由于被删除元素到某一端之间的所有元素都要向中间移动,删除的位置越靠近中间,效率越低。
注意:
在除了首尾两端的其他地方插入和删除元素,都将会导致指向deque元素的任何pointers、references、iterators失效。不过,deque的内存重分配优于vector,因为其内部结构显示不需要复制所有元素。
4、容器之间的比较?
a、vector和deque支持下标操作,list不支持下标操作
b、vector和deque容器均提供了对元素的快速随机访问,但是在容器的任意位置插入或者删除元素,避免容器尾部插入和删除开销更大;而list类型在任何位置都能快速插入和删除,但是元素的随机访问开销较大
c、deque容器与vector容器在与deque的数据结构更为复杂,deque容器同时提供了高效的在其首部insert和erase操作,就如同在尾部进行操作一样
d、在deque容器首尾部插入元素不会使任何迭代器失效,而在首部或者尾部删除元素则只会使指向被删除元素的迭代器失效,在其他的任何位置删除或者插入都会使该容器的所有迭代器失效