说在前面
vector 的数据安排以及操作方式,与 array 非常类似。两者唯一的差别在于空间的运用的灵活性。array 是 静态空间,一旦配置了就不能改变。而 vector 是动态空间,随着元素的加入,它的内部机制会自动扩充空间(一般为原空间的两倍)以容纳元素。
vector 内存分配机制
下面,我将以一张图来引入 vector。
假设此时有一个 capacity (容量)为 8 的 vector。但是此时 vector 里面只存放了 6 个元素,即 size = 6;如果此时我继续往 vector 里面放入 6 个元素,很显然,空间不够。那么 vector 将会自动扩充到原来大小的两倍(原 capacity 的两倍)。而且这里有一个很重要的一点,就是扩充并不是在原来的地址空间后面增加内存,而是重新找一块更大的内存,重新分配 capacity * 2 倍大小的内容空间,然后将原内存空间的内容拷贝到新分配的内存空间中,然后释放掉原内存空间。
下面是 vector 的部分源码:
我们可以看到,vector 内部定义了三个指针,start、finish 和 end_of_storage。所以我们 sizeof(vector) 得到的是 12 bytes。而且我们看到 vector 内部重载了 [] 运算符,凡是连续空间的容器,内部都需要重载 [] 运算符,deque 内部也实现了 [] 的重载。
下面我们来看看 vector 的 push_back 操作是怎么实现的?
vector 迭代器
说到 vector,因为它是一段连续的空间,所以它的 iterator 不必设计得太过复杂。之前我们谈到的链表,它 的 iterator 需要单独设计成一个 class,那是因为它的内存不是连续分配的,即链表的节点都是分离的。而 vector 是连续的,按理说,直接使用普通指针就可以实现迭代器。我们来看看 vector 的 iterator 是否是这样设计的?
下面是 G2.9 的 vector 的 iterator 的实现方式。
从上图可以看出,vector 的 iterator 确实是一个普通指针,使用的是 iterator_traits 的偏特化版本。
看完了 G2.9 版本的,下面来看看 G4.9 版本的 iterator。可以看出,相比于 G2.9 来说,iterator 变得更加的复杂,不直观。但是其内部实现的功能是一样的。而我们 sizeof(vector) 得到的大小依然是 12。
在图中,你可能会发现,_Vector_impl<_Tp> public 继承了 allocator。在 C++ 中,public 继承表示 “is -a”,即什么是什么的关系。按照这样解释的话,源码表示着 _Vector_impl 是一种 allocator。显然,这样说是没有道理的。其实这里的 _Vector_impl 就是想用到 allocator 的功能,虽然 public 能够实现,但是个人感觉不太好,如果使用 private,或许可能更好一些。
那么 G4.9 版本的迭代器有什么变化吗?下面一张图是在追踪 vector::iterator。
在步骤 1 中,我们找到了 iterator,而 iterator 是某一种 class(__normal_iterator),然后按照步骤 2 找到了它,发现其内部有一个变量为 _M_current,其类型为 _Iterator,那 _Iterator 是什么类型呢?顺着步骤 4 发现它是一种 pointer,顺着步骤5,发现 pointer 是 _Base::pointer,而 _Base 又是 _Vector_base,顺着步骤 6, 7 找到 _Vector_base 中的 pointer,最后通过 8, 9 才知道了 M current 其实就是 _Tp*。由此可见,想要知道 _M_current 的类型为 _Tp* 那么就得按照图中的 1-9 步来寻找,而在 G2.9 版本中,能够一眼就看出来。
通过上面的追踪,我们发现在 G4.9 版本中的 iterator 不再是个单纯的指针了,发现它就是 _Tp* 外覆一个 iterator adapter,使之能支持 5 种相应型别(associated type)。