首先,vector 在VC 2008 中的实现比较复杂,虽然vector 的声明跟VC6.0 是一致的,如下:
但在VC2008 中vector 还有基类,如下:
稍微来看一下基类_Vector_val:
为了理解_Alty 的类型,还得看一下allocator模板类:
typedef typename _Alloc::template rebind<_Ty>::other _Alty; 整体来看是类型定义,假设现在我们这样使用
vector<int>, 那么_Ty 即 int, _Ax 即 allocator<int>,由vector 类传递给 基类_Vector_val,则_Alloc 即
allocator<int> ;可以看到 allocator<void> 是allocator 模板类的特化, rebind<_Ty> 是成员模板类,other是成员模板类
中自定义类型,_Ty 即是int , 那么other 类型也就是allocator<int>, 也就是说_Alty 是类型 allocator<int> 。
_Alty _Alval; 即 基类定义了一个allocator<int> 类型的成员,被vector 继承后以后用于为vector 里面元素分配内存等操作。
如 iterator new_data = alloc.allocate(new_size); 注意,标准的vector::iterator 是以模板类实现的,下面的实现简单地将其等同为指
针,实际上真正的iterator 类的实现是内部有一个指针成员,指向容器元素。
对比list 的实现,继承它的基类_List_nod 的一个成员 allocator<_Node> _Alnod; 如下:
typename _Alloc::template rebind<_Node>::other _Alnod; // allocator object for nodes
其中 _Node有三个成员,如下:
_Nodeptr _Next; // successor node, or first element if head
_Nodeptr _Prev; // predecessor node, or last element if head
_Ty _Myval; // the stored value, unused if head
如果是list<int> ,那么_Ty 即是int 类型。双向链表在创建一个新结点时,这样实现:
_Nodeptr _Pnode = this->_Alnod.allocate(1); // 即分配一个节点的空间,返回指向这个节点的指针。
实际上list 还继承另外两个基类的两个成员,如下:
typename _Alloc::template rebind<_Nodeptr>::other _Alptr;// allocator object for pointers to nodes
typename _Alloc::template rebind<_Ty>::other _Alty _Alval; // allocator object for values stored in nodes
在VC6.0 vector 的实现,_Alval 是直接作为vector 自身的成员存在的。此外还有一个比较大的不同点在于,两个版本对于capacity 也就是容量的
计算方式不同,接下去的测试可以看到这种不同,在这里可以先说明一下:
VC2008:容量每次增长为原先容量 + 原先容量 / 2;
VC6.0 :容量每次增长为原先容量的2倍。
容量跟vector 大小的概念是不一样的,capacity 》= size,如下图所示:

size 指的是avail - data 的区间;capacity 指的是 limit - data 的区间;也就是说存在尚未使用的空间。
data, avail 和 limit 是vector 类的三个指针成员,对比list 的实现,自身也许只有两个成员,如下:
Nodeptr _Myhead; // pointer to head node
size_type _Mysize; // number of elements
实际上 list不仅是一个双向链表,而且还是一个环状双向链表。另外,插入操作和结合操作都不会造成原有的list迭代器失效,这在vector是不成立的。
因为vector的插入操作可能造成存储空间重新配置,导致原有的迭代器全部失效。list的元素删除操作(erase),也只有“指向被删除元素”的那个
迭代器失效,其他迭代器不受任何影响。
下面是模仿VC6.0 中vector 的实现写的Vec 类,程序主要参考《Accelerated C++》 ,略有修改,比如将接口修改成与VC6.0 一致,
这样做的好处是可以传递第二个参数,也就是说可以自己决定内存的分配管理方式;实现capacity() 函数等;
Vec.h:
先介绍一下用到的一些类和函数:
allocator 模板类:
当然实际的接口没实现没那么简单,但大概实现的功能差不多:
allocate 调用operator new ;deallocate 调用 operator delete; construct 调用placement new (即在分配好的内
存上调用拷贝构造函数),destroy 调用析构函数。
两个std函数:
如 std::uninitialized_copy(i, j, data); 即将i ~ j 指向区间的数值都拷贝到data 指向的区间,返回的是最后一个初始化值的下一个位置。
std::uninitialized_fill(data, limit, val); 即将 data ~ limit 指向的区间都初始化为val 。
为了理解push_back 的工作原理,写个小程序测试一下:
从输出可以看出,构造函数调用3次,拷贝构造函数调用6次,析构函数调用9次,下面来分析一下,首先看下图:
首先定义t1, t2, t3的时候调用三次构造函数,这个没什么好说的;接着第一次调用push_back,调用grow进而调用alloc.allocate,
allocate 函数调用operator new 分配一块内存,第一次uncreate 没有效果,接着push_back 里面调用uncheck_append,进而调用
alloc.construct,即调用placement new(new (_Vptr) _T1(_Val); ),在原先分配好的内存上调用一次拷贝构造函数。
接着第二次调用push_back,一样的流程,这次先分配两块内存,将t1 拷贝到第一个位置,调用uncreate(),先调用alloc.destroy,即
调用一次析构函数,接着调用alloc.deallocate,即调用operator delete 释放内存,最后调用uncheck_append将t2 拷贝到第二个位置。
第三次调用push_back,也一样分配三块内存,将t1, t2 拷贝下来,然后分别析构,最后将t3 拷贝上去。
程序结束包括定义的三个Test 对象t1, t2, t3 ,析构3次,Vec<Test> v2; v2是局部对象,生存期到则调用析构函数~Vec(); 里面调用
uncreate(), 调用3次Test 对象的析构函数,调用operator delete 释放3个对象的内存。故总共析构了6次。
在VC2008 中换成 vector<Test> v2; 来测试的话,输出略有不同,如下:
输出的次数是一致的,只是拷贝的顺序有所不同而已,比如第二次调用push_back 的时候,VC2008 中的vector 是先拷贝t2, 接着拷
贝t1, 然后将t1 释放掉。
最后再来提一下关于capacity 的计算,如下的测试程序:
输出为 1 2 4 4 8 8 8 即在不够的情况下每次增长为原来的2倍。
如果换成 vs2008 下 vector<int> v; 测试,那么输出是 1 2 3 4 6 6 9,即在不够的情况每次增长为原来大小 + 原来大小 / 2;
看到这里,有些朋友会疑惑了,由1怎么会增长到2呢?按照原则不是还是1?其实只要看一下vector 的源码就清楚了:
_Insert_n 是被push_back 调用的,当我们试图增长为_Capacity + _Capacity / 2; 时,下面还有一个判断:
if (_Capacity < size() + _Count)
_Capacity = size() + _Count;
现在试图增长为 1 + 1/ 2 = 1; 此时因为 1 < 1 + 1 ; 所以 _Capacity = 1 + 1 = 2;
其他情况都是直接按公式增长。
从上面的分析也可以看出,当push_back 的时候往往带有拷贝和析构多个操作,所以一下子分配比size() 大的空间capacity,可以减轻频繁操作造成的
效率问题。
参考:
C++ primer 第四版
Effective C++ 3rd
C++编程规范
Accelerated C++