前言
STL的容器分为序列式容器和关联式容器,其中序列式容器的特点是每个元素均有特定的位置,这个位置和元素的值无关,下面我们介绍四种序列式容器。
vector
vector是一种支持动态扩容的数组,在SGI中,其继承关系如下:
这里与GNU的STL稍有不同,GNU中_vector_base包含一个名为_vector_impl的成员,这个成员实现了_M_allocate()和_M_deallocate(),而SGI中可以看到是直接在_Vector_base中实现的。
vector默认的allocator会为vector分配一段连续的内存空间,class _Vector_base中的_M_start指向这段内存的头部,_M_finish指向这段内存中已使用部分的尾部,_M_end_of_storage则指向内存结束的位置。也就是说,分配给vector的内存不一定全部被使用allocator给vector分配内存时,会留有一定的余量,我们可以调用vector的capacity方法查看内存总量,调用size()方法查看已使用的大小。
#include <bits/stdc++.h>
int main(){
std::vector<int> nums;
for(int i=0;i<5;++i){
nums.emplace_back(i);
}
std::cout << "size:" << nums.size() << ",capacity:" << nums.capacity() << std::endl;
return 0;
}
运行结果如下:
vector扩容机制
上面的运行结果中展示了目前这块内存总大小是8,假设我们向vector中插入9个元素,capacity会变成多少?我们运行下面的代码:
#include <bits/stdc++.h>
int main(){
std::vector<int> nums;
for(int i=0;i<9;++i){
nums.emplace_back(i);
}
std::cout << "size:" << nums.size() << ",capacity:" << nums.capacity() << std::endl;
return 0;
}
capacity变成了以前的两倍,需要说明的是我的测试环境是在Linux下的GCC 11.1.0版本,如果在其他平台下可能会出现不同的情况。这说明当vector内存大小不足时,vector扩大了之前的内存空间,变为了之前的两倍(VC++是1.5倍)。理想状态下,vector可以保持原来的内存空间不变,直接在后面增加一段相同大小的内存空间,使得容量变为两倍,但如果后面的内存空间长度不够了,是不是就无法扩容了呢。这里可以做个实验,我们打印第0个元素的地址空间,看看是否发生了变化。
#include <bits/stdc++.h>
int main(){
std::vector<int> nums;
nums.emplace_back(0);
for(int i=1;i<9;++i){
nums.emplace_back(i);
std::cout << &nums[0] << std::endl;
}
return 0;
}
运行结果:
可以发现在插入第5个和第9个元素的时候,第一个元素的地址发生了变化,而对应的内存大小4和8,正好是需要扩容的内存长度,因此可以得出结论,vector在扩容时,会寻找一段能容纳两倍当前容量大小的内存,将原来的元素复制到新的内存中,下面是尾部插入元素时的代码实现:
void push_back(const _Tp& __x) {
if (_M_finish != _M_end_of_storage) { // 有备用空间
construct(_M_finish, __x); // 全局函数,将 __x 设定到 _M_finish 指针所指的空间上
++_M_finish; // 调整
}
else
_M_insert_aux(end(), __x); // 无备用空间,从新分配再插入
}
template <class _Tp, class _Alloc>
void
vector<_Tp, _Alloc>::_M_insert_aux(iterator __position, const _Tp& __x)
{
if (_M_finish != _M_end_of_storage) {
construct(_M_finish, *(_M_finish - 1));
++_M_finish;
_Tp __x_copy = __x;
copy_backward(__position, _M_finish - 2, _M_finish - 1);
*__position = __x_copy;
}
else {// 没有备用空间
const size_type __old_size = size();
const size_type __len = __old_size != 0 ? 2 * __old_size : 1;
iterator __new_start = _M_allocate(__len);
iterator __new_finish = __new_start;
__STL_TRY {
__new_finish = uninitialized_copy(_M_start, __position, __new_start);
construct(__new_finish, __x);
++__new_finish;
__new_finish = uninitialized_copy(__position, _M_finish, __new_finish);
}
__STL_UNWIND((destroy(__new_start,__new_finish),
_M_deallocate(__new_start,__len)));
destroy(begin(), end());
_M_deallocate(_M_start, _M_end_of_storage - _M_start);
_M_start = __new_start;
_M_finish = __new_finish;
_M_end_of_storage = __new_start + __len;
}
}
vector还有Insert()方法可以在中间插入元素,其原理和在尾部插入大致相同。
iterator insert(iterator __position, const _Tp& __x) {
size_type __n = __position - begin();
if (_M_finish != _M_end_of_storage && __position == end()) {
construct(_M_finish, __x);
++_M_finish;
}
else
_M_insert_aux(__position, __x);
return begin() + __n;
}
迭代器失效
对vector的操作要特别考虑iterator是否会失效的问题。我们前面说过,iterator可以看成是进化的指针,当内存中的元素发生变化,iterator原本指向的地址可能就失效了。对于中间插入操作,由于会导致插入节点后续的元素都向后移动,因此导致插入节点后的iterator指向的元素不再是原先的元素,全部失效。
对vector的插入操作,不论是在哪里插入,都可能会导致vector扩容,扩容由于需要重新寻找一块内存,原先的迭代器将全部失效。
vector扩容大小
前面的实验中我们验证了LInux下扩容大小是原先的2倍,但为什么是两倍呢?为什么没有使用每次增加一个固定容量大小或者使用3倍、4倍呢?
首先我们来比较下成倍扩容和固定量扩容的区别,假设需要插入n个元素,每次插入k个,那么复制的总次数为:
总操作数除以总元素个数n,得到平均插入每个元素需要复制的次数是o(n)级别的。然后我在看成倍扩容的情况,假设需要插入N=k^n个元素,n为任意正整数。由于给一个大小为nk的数组需要复制nk次,那么复制的总次数为:
除以总元素个数k^n,得到下面的平均插入每个元素操作数,是O(1)级别,这显然比固定容量扩容效率高。
显然上图中的表达式在k=2时取到极小值,STL正是考虑到这一点,因此采取了2倍扩容的策略。
array
array是由c++提供的容器。并不在SGI的STL中提供,与vector的不同点是array是静态数组,确定大小后不可改变,其余用法与vector相同。