前言
STL 容器是 C++ 开发中绕不开的 “神兵利器”,而vector作为最常用的动态数组容器,更是新手入门 STL 的核心内容。但多数时候,我们只是 “会用”vector,却对它的底层逻辑一知半解 —— 比如它如何动态扩容?push_back的内存管理是怎样的?构造函数的匹配规则为何如此复杂?
一、 vector的介绍
简单说,vector是 C++ 里可以动态变大小的数组—— 它既保留了普通数组 “连续存储、下标访问高效” 的优点,又解决了普通数组 “大小固定、不能自动扩容” 的痛点。
它的底层逻辑是动态分配的数组:
当你往vector里加新元素时,它会自动管理内存 —— 如果当前数组存满了,就会重新分配一块更大的空间,把旧元素全搬过去,再继续存新元素(不过这个 “搬家” 操作开销不低,所以vector会提前多分配一些额外空间,避免频繁搬家)。
vector 是我们学习的第一个真正的 STL 容器,它接口的使用方式和 string 有一点点的不同,但大部分都是一样的,所以这里我们就只演示其中一些接口的使用,大家如果有疑惑的地方直接在 cplusplus 是上面查看对应的文档即可。
二、构造函数
vector 提供了四种构造方式 – 无参构造、n 个 val 构造、迭代器区间构造以及拷贝构造:

其中构造函数的最后一个参数 alloc 是空间配置器,它和内存池有关,作用是提高空间分配的效率;我们日常使用时不用管这个参数,使用它的缺省值即可,但是可能有极少数的人想要用自己实现的空间配置器来代替 STL 库提供的,所以留出了这一个参数的位置。
vector<int> v1; // 默认构造:空向量
vector<int> v2(5, 1); // 5 个元素,每个初始化为 1
vector<int> v3(v2.begin(), v2.end()); // 用迭代器区间拷贝构造
vector<int> v4(v3); // 直接拷贝构造
// 遍历输出 v1:当前为空,循环不会执行
for (size_t i = 0; i < v1.size(); ++i)
{
cout << v1[i];
}
cout << endl; // 仅换行
// 遍历输出 v2:5 个 1
for (size_t i = 0; i < v2.size(); ++i)
{
cout << v2[i];
}
cout << endl;
// 再次遍历输出 v3(与上一段完全相同)
for (size_t i = 0; i < v3.size(); ++i)
{
cout << v3[i];
}
cout << endl;
// 再次遍历输出 v4(与上一段完全相同)
for (size_t i = 0; i < v4.size(); ++i)
{
cout << v4[i];
}
cout << endl;

需要注意的是,迭代器区间构造是一个函数模板,即我们可以用其他类来构造 vector 对象:
string s = "hello";
vector<char> v(s.begin(), s.end()); // 用迭代器区间拷贝构造
for (size_t i = 0; i < v.size(); ++i)
{
cout << v[i];
}
cout << endl;

同时,上面还有一个非常重要的细节:
在 n 个 val 的构造中,val 的缺省值是 T 的匿名对象就像是T(),该对象使用 T 的默认构造来初始化,而不是以 0 作为缺省值,这是因为 T 不仅仅可能是内置类型,也可能是自定义类型,比如 string、list、vector;
当 T 为自定义类型时,0 就不一定能够对 val 进行初始化,所以我们需要使用 T 的匿名对象来调用默认构造完成初始化工作;当 T 为内置类型时,我们仍然可以以这种方式进行初始化,因为 内置类型也具有构造函数,你没听错,内置类型也是有构造函数的,大家可以理解为,为了解决上面这种情况,编译器对内置类型进行了特殊处理,像是把自定义类型也包装了一下;
利用匿名对象调用构造函数来作为缺省值的方法在下面 模拟实现中也有体现。
三、扩容机制
vector 的扩容机制和 string 的扩容机制是一样的,因为它们都是动态增长的数组:VS 下大概是 1.5 被扩容,Linux g++ 下是标准的二倍扩容,多出来的也是一样因为内存对齐的原因,测试用例如下:
void TestVectorExpand()
{
size_t sz;
vector<int> v;
sz = v.capacity();
cout << "making v grow:\n";
for (int i = 0; i < 1000; ++i)
{
v.push_back(i);
if (sz != v.capacity())
{
sz = v.capacity();
cout << "capacity changed: " << sz << '\n';
}
}
}

四、三种遍历方式
和 string 一样,vector 也支持三种遍历方式 – 下标加[]遍历、迭代器遍历、范围for遍历:
vector<int> v(10, 5); // 10 个元素,每个值为 5
// 1. 下标 + []
for (int i = 0; i < v.size(); ++i)
cout << v[i] << " ";
cout << endl;
// 2. 迭代器
vector<int>::iterator it = v.begin();
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
// 3. 范围 for
for (auto& e : v)
cout << e << " ";
cout << endl;

需要注意的是,vector 和 string 之所以支持 下标 + [] 的方式遍历,是因为它们底层都是数组,而数组支持随机访问,但是像我们后面要学习的 list set map 等容器,它们的底层不是数组,不支持随机访问,就只能通过迭代器和范围 for 的方式进行遍历了;不过,范围 for 只是一个外壳,它在使用时也是被替换成迭代器,所以其实迭代器遍历才是最通用的遍历方式。
五、容量操作
vector 有如下容量相关的接口:
| 成员函数 | 功能 |
|---|---|
size() | 返回当前容器中实际存储的元素个数。 |
max_size() | 返回容器理论上能存储的最大元素个数。(很少用) |
resize(n) | 把容器大小改为 n,多删少补(补的默认用 0 值或指定值)。 |
capacity() | 返回当前已分配的存储空间(容量),单位是元素个数。 |
empty() | 容器为空时返回 true,否则返回 false。 |
reserve(n) | 提前一次性分配至少能存 n 个元素的内存,避免后续频繁扩容。 |
其中,最重要的两个函数是 reserve 和 resize,reserve 只用于扩容,它不改变 size 的大小;而 resize 是扩容加初始化,既会改变 capacity,也会改变 size;

注意:reserve 和 resize,包括后面的 clear 函数都不会缩容,因为缩容需要开辟新空间、拷贝数据、释放旧空间,而对于自定义类型又有可能存在深拷贝问题,时间开销极大;vector 中唯一可能缩容的函数就只有 shrink_to_fit,对于它来说,如果 capacity 大于 size,它会进行缩容,让二者相等。
六、元素访问
| 成员函数 | 功能) |
|---|---|
v[pos] | 下标访问:返回第 pos 个元素(不越界检查)。 |
v.at(pos) | 安全下标访问:返回第 pos 个元素,越界时抛出 std::out_of_range。 |
v.front() | 返回首元素 |
v.back() | 返回尾元素 |
其中,operator 和 at 都是返回 pos 下标位置元素的引用,且它们内部都会对 pos 的合法性进行检查;不同的是,operator[] 中如果检查到 pos 非法,那么它会直接终止程序,报断言错误,而 at 则是抛异常;

七、修改 – 迭代器失效
vector 提供了如下接口来进行修改操作:
| 成员函数 | 功能 |
|---|---|
v.assign(n, val) | 清空原内容并重新赋值为 n 个 val。 |
v.assign(beg, end) | 清空原内容并用迭代器区间 [beg, end) 的元素重新填充。 |
v.push_back(x) | 在尾部追加元素 x,容量不足时自动扩容。 |
v.pop_back() | 删除尾部最后一个元素(容器为空行为未定义)。 |
v.insert(pos, x) | 在迭代器 pos 指向的位置插入 x,返回指向新元素的迭代器。 |
v.erase(pos) | 删除迭代器 pos 指向的元素,返回下一个位置的迭代器。 |
v.erase(beg, end) | 删除区间 [beg, end) 内的所有元素,返回下一个位置的迭代器。 |
v.swap(v2) | 与另一个 vector 交换内容(常数时间)。 |
v.clear() | 清空所有元素,使 size() 变为 0,但通常不释放容量。 |
vector<int> v(5,1);
for (auto& e : v)
cout << e << " ";
cout << endl;
v.assign(2, 2);
for (auto& e : v)
cout << e << " ";
cout << endl;
v.push_back(3);
for (auto& e : v)
cout << e << " ";
cout << endl;
v.pop_back();
for (auto& e : v)
cout << e << " ";
cout << endl;
v.insert(v.begin(), -1);
for (auto& e : v)
cout << e << " ";
cout << endl;
auto it= v.erase(v.begin());
for (auto& e : v)
cout << e << " ";
cout << endl;
cout << "删除后下一个的位置"<< * it << endl;
vector<int> tmp(6, 6);
v.swap(tmp);
for (auto& e : v)
cout << e << " ";
cout << endl;
v.clear();
for (auto& e : v)
cout << e << " ";
cout << endl;

- assign && push_back && pop_back
assign 函数用来替换 vector 对象中的数据,支持 n 个 val 替换,以及迭代器区间替换,push_back 尾插、pop_back 尾插,这些接口的使用和 string 一模一样,这里就不再过多阐释; - insert && erase
和 string 不同,为了提高规范性,STL 中的容器都统一使用 iterator 作为 pos 的类型,并且插入/删除后会返回 pos:
所以,以后我们如果要在中间插入或删除元素的话,必须配合算法库里面的 find 函数来使用:
代码演示
vector<int> v;
for (size_t i = 0; i < 20; i++)
{
v.push_back(i);
}
//找到6删除6
vector<int>::iterator tmp= find(v.begin(),v.end(),6);
v.erase(tmp);
for (auto& e : v)
cout << e << " ";
这里其实有一个问题我们来解引用一下*tmp

程序报了一个这个错误这个就是迭代器失效了为什么了我来画个图理解一下 
如何解决了我们让tmp指向删除元素的下一个位置就好了 
那我们继续思考一下是不是insert 也会有问题先分析在验证一下 

但是为了代码的可移植性,我们 统一认为 insert 和 erase 之后迭代器会失效,所以,如果要再次使用迭代器,我们必须对其进行更新;我们以移除 vector 中的所有偶数为例:
vector<int> v;
vector<int>::iterator it=v.begin();
for (size_t i = 0; i < 20; i++)
{
it=v.insert(it, i);
}
while (it != v.end())
{
if (*it % 2 == 0)
it = v.erase(it);
else
it++;
}
for (auto& e : v)
cout << e << " ";
cout << endl;

- swap
和 string 一样,由于算法库 swap 函数存在深拷贝的问题,vector 自己提供了一个不需要深拷贝的 swap 函数,用来交换两个 vector 对象:
同时,为了避免我们不使用成员函数的 swap,vector 还将算法库中的 swap 进行了重载,然后该重载函数的内部又去调用成员函数 swap:

2112

被折叠的 条评论
为什么被折叠?



