【C++ vector 深度解析】:动态数组的使用与底层原理实战
 
 
一篇吃透扩容机制、迭代器失效与 OJ 实战的核心教程 ✨
💬 前言
在上一篇《C++ string 类模拟实现》中,我们掌握了字符串容器的资源管理逻辑。而 vector 作为 STL 中最常用的 “动态数组” 容器,更是笔试面试与日常开发的高频考点。但很多开发者对它的理解仅停留在 “动态扩容的数组”,遇到这些问题时容易踩坑:
为什么 VS 和 G++ 下 vector 扩容倍数不一样?迭代器为什么会失效?如何用 vector 实现动态二维数组?删除元素时为什么有的代码崩溃有的正常?
本篇文章将从 “实战用法” 到 “底层原理”,带你全面掌握 vector:先详解核心接口的正确用法,再拆解扩容机制与迭代器失效的本质,最后通过 OJ 题巩固实战技巧,让你不仅 “会用 vector”,更 “懂 vector 的设计逻辑”。
✨ 阅读后,你将掌握:
- vector 核心接口(构造、容量、增删查改)的使用场景与边界处理;
- VS(1.5 倍)与 G++(2 倍)的扩容差异及优化技巧;
- 迭代器失效的 3 大场景、底层原因与解决办法;
- 用 vector 高效解决 OJ 经典问题(杨辉三角、删除重复项等);
- 动态二维数组的正确实现与内存布局。
文章目录
- 一、vector 的核心定位:动态数组的设计初衷
- 1. vector 的核心优势
- 2. 与数组、string 的核心区别
 
- 二、vector 核心接口详解:从构造到增删查改
- 1. 构造函数:3 种常用初始化方式
- 2. 容量操作:扩容机制与优化技巧
- 3. 增删查改:核心接口的正确用法
 
- 三、vector 的核心痛点:迭代器失效的本质与解决
- 1. 迭代器失效的 3 大场景
- 2. 迭代器失效的解决办法
 
- 四、OJ 实战:用 vector 解决经典问题
- 1. 杨辉三角(LeetCode 118)
- 2. 删除排序数组中的重复项(LeetCode 26)
- 3. 只出现一次的数字(LeetCode 136)
 
- 五、vector 的进阶用法:动态二维数组
- 1. 动态二维数组的初始化与访问
- 2. 内存布局说明
 
- 六、思考与总结 ✨
- 七、自测题与答案解析 🧩
- 八、延伸阅读推荐
- 九、下篇预告:C++ vector 模板模拟实现 —— 动态数组的资源管理
一、vector 的核心定位:动态数组的设计初衷
vector 本质是 “可动态扩容的数组”,它完美结合了数组的随机访问优势与动态内存管理的灵活性。与 string 专注字符串不同,vector 支持任意类型的元素存储,是最通用的容器之一。
1. vector 的核心优势
- 随机访问高效:支持 operator[],访问任意元素时间复杂度 O(1)O (1)O(1),与数组一致;
- 动态扩容:无需手动管理内存,元素超过容量时自动扩容,避免数组越界;
- 接口简洁:提供 push_back(尾插)、pop_back(尾删)、resize(调整大小)等接口,用法直观;
- 兼容算法:可与 STL 算法(如 find、sort)无缝配合,降低编码复杂度。
2. 与数组、string 的核心区别

二、vector 核心接口详解:从构造到增删查改
vector 的接口众多,我们聚焦 “高频核心接口”,结合代码示例与底层逻辑,讲清 “怎么用” 和 “为什么这么用”。
1. 构造函数:3 种常用初始化方式
vector 提供 4 种构造函数,重点掌握前 3 种,覆盖绝大多数场景:

#include <iostream>
#include <vector>
using namespace std;void TestVectorConstructor() {// 1. 无参构造vector<int> v1;cout << "v1: size=" << v1.size() << ", capacity=" << v1.capacity() << endl; // 0, 0// 2. 构造5个3vector<int> v2(5, 3);cout << "v2: ";for (auto e : v2) cout << e << " "; // 3 3 3 3 3cout << endl;// 3. 拷贝构造vector<int> v3(v2);cout << "v3: size=" << v3.size() << ", capacity=" << v3.capacity() << endl; // 5, 5// 4. 迭代器区间构造(数组转vector)int arr[] = {1,2,3,4,5};vector<int> v4(arr, arr + sizeof(arr)/sizeof(int));cout << "v4: ";for (auto e : v4) cout << e << " "; // 1 2 3 4 5cout << endl;
}int main() {TestVectorConstructor();return 0;
}
2. 容量操作:扩容机制与优化技巧
容量相关接口是 vector 的核心,直接影响性能,重点掌握 size、capacity、reserve、resize 的区别与用法。

 (1)扩容机制:VS 与 G++ 的核心差异
vector 的扩容不是 “按需扩容”,而是 “倍数扩容”,不同编译器倍数不同:
- VS(PJ 版本 STL):按 1.5 倍扩容(1→2→3→4→6→9→13…);
- G++(SGI 版本 STL):按 2 倍扩容(1→2→4→8→16→32…)。
扩容测试代码
void TestVectorExpand() {size_t sz;vector<int> v;sz = v.capacity();for (int i = 0; i < 100; ++i) {v.push_back(i);if (sz != v.capacity()) {sz = v.capacity();cout << "capacity changed: " << sz << endl; // 输出:1,2,4,8,16,32,64,128}}
}
(2)扩容优化:用reserve避免频繁扩容
扩容的本质是 “重新开空间→拷贝旧元素→释放旧空间”,开销较大。如果已知元素个数,提前用 reserve 预留空间:
void TestVectorExpandOP() {vector<int> v;v.reserve(100); // 提前预留100个容量,避免100次push_back触发多次扩容size_t sz = v.capacity();cout << "提前reserve后的扩容过程:" << endl;for (int i = 0; i < 100; ++i) {v.push_back(i);if (sz != v.capacity()) {sz = v.capacity();cout << "capacity changed: " << sz << endl; // 仅输出100,无多次扩容}}
}
3. 增删查改:核心接口的正确用法
vector 的增删查改接口设计简洁,重点掌握尾插、尾删、插入、删除、随机访问的用法。

 增删查改代码演示
void TestVectorMod() {vector<int> v;// 尾插v.push_back(1);v.push_back(2);v.push_back(3);cout << "尾插后:";for (auto e : v) cout << e << " "; // 1 2 3cout << endl;// 头部插入0v.insert(v.begin(), 0);cout << "头部插0后:";for (auto e : v) cout << e << " "; // 0 1 2 3cout << endl;// 删除第二个元素(值1)auto it = v.erase(v.begin() + 1);cout << "删除后:";for (auto e : v) cout << e << " "; // 0 2 3cout << endl;cout << "erase返回的下一个元素:" << *it << endl; // 2// 随机访问v[1] = 4;cout << "修改后:";for (auto e : v) cout << e << " "; // 0 4 3cout << endl;
}
三、vector 的核心痛点:迭代器失效的本质与解决
迭代器失效是 vector 最容易踩的坑,底层原因是 “迭代器本质是指针或指针封装,指向的空间被销毁或移动”,继续使用会导致程序崩溃或逻辑错误。
1. 迭代器失效的 3 大场景
(1)扩容操作导致失效(最常见)
 任何会改变底层空间的操作(resize、reserve、insert、push_back、assign),都可能触发扩容,导致旧空间被释放,迭代器指向野指针:
void TestIteratorInvalid1() {vector<int> v{1,2,3,4,5};auto it = v.begin();// 触发扩容:旧空间释放,it失效v.reserve(100); // 错误:访问失效的迭代器,VS下直接崩溃,G++下输出乱码while (it != v.end()) {cout << *it << " ";++it;}
}
(2)erase 删除导致失效
erase 删除元素后,后续元素会往前搬移,若删除的是最后一个元素,迭代器会指向 end(无效位置);VS 下更严格,删除任意位置后,该迭代器直接失效:
void TestIteratorInvalid2() {vector<int> v{1,2,3,4};auto it = find(v.begin(), v.end(), 3);// 错误:erase后it失效,访问*it非法v.erase(it);cout << *it << endl; // VS崩溃,G++输出4(未严格检测)
}
(3)删除多个元素的常见错误
 删除 vector 中所有偶数,错误代码会崩溃,正确代码需用 erase 的返回值更新迭代器:
// 错误代码:删除后it++,访问失效迭代器
void TestEraseError() {vector<int> v{1,2,3,4};auto it = v.begin();while (it != v.end()) {if (*it % 2 == 0) {v.erase(it); //  erase后it失效}++it; // 错误:失效的it++}
}// 正确代码:用erase返回值更新迭代器
void TestEraseCorrect() {vector<int> v{1,2,3,4};auto it = v.begin();while (it != v.end()) {if (*it % 2 == 0) {it = v.erase(it); // erase返回下一个有效元素的迭代器} else {++it;}}// 输出:1 3for (auto e : v) cout << e << " ";
}
2. 迭代器失效的解决办法
- 扩容后重新赋值:扩容操作后,若需继续使用迭代器,重新调用 begin()获取:
v.reserve(100);
it = v.begin(); // 重新赋值,指向新空间的起始位置
- 用 erase 返回值更新:删除元素时,通过 it = v.erase(it)更新迭代器,避免访问失效位置;
- 避免在循环中同时遍历和修改:若需大量修改,可先标记要删除的元素,最后批量删除。
四、OJ 实战:用 vector 解决经典问题
vector 在 OJ 中应用极广,以下是 3 道高频题,结合 vector 的核心接口实现。
1. 杨辉三角(LeetCode 118)
👉杨辉三角(LeetCode 118)
题目:生成 n 行杨辉三角,每行头尾为 1,中间元素为上一行相邻两元素之和。
核心考点:vector 动态二维数组、resize 初始化。
class Solution {
public:vector<vector<int>> generate(int numRows) {vector<vector<int>> vv(numRows); // 构造numRows个空vectorfor (int i = 0; i < numRows; ++i) {vv[i].resize(i + 1, 1); // 第i行resize为i+1个元素,默认值1}// 填充中间元素for (int i = 2; i < numRows; ++i) {for (int j = 1; j < i; ++j) {vv[i][j] = vv[i-1][j-1] + vv[i-1][j];}}return vv;}
};
2. 删除排序数组中的重复项(LeetCode 26)
👉删除排序数组中的重复项(LeetCode 26)
题目:删除有序数组中的重复项,返回新长度,原地修改。
核心考点:vector 遍历、erase用法、迭代器更新。
class Solution {
public:int removeDuplicates(vector<int>& nums) {if (nums.empty()) return 0;auto it = nums.begin() + 1;while (it != nums.end()) {if (*it == *(it - 1)) {it = nums.erase(it); // 删除重复元素,更新迭代器} else {++it;}}return nums.size();}
};
3. 只出现一次的数字(LeetCode 136)
👉 只出现一次的数字(LeetCode 136)
题目:数组中除一个元素出现一次外,其余均出现两次,找出该元素。
核心考点:vector 遍历、异或运算。
class Solution {
public:int singleNumber(vector<int>& nums) {int res = 0;for (auto e : nums) {res ^= e; // 异或:相同为0,不同为1,最终剩下唯一元素}return res;}
};
五、vector 的进阶用法:动态二维数组
vector 可嵌套实现动态二维数组,相比原生二维数组( int arr[M][N] ),优势是 “行数和列数均可动态调整”,内存布局更灵活。
1. 动态二维数组的初始化与访问
void Test2DVector() {// 方式1:构造3行,每行初始化为2个0vector<vector<int>> vv1(3, vector<int>(2, 0));// 方式2:先构造3行空vector,再逐个resizevector<vector<int>> vv2(3);for (int i = 0; i < 3; ++i) {vv2[i].resize(i + 1, 1); // 第0行1个1,第1行2个1,第2行3个1}// 访问与修改vv1[0][0] = 1;cout << "vv1[0][0]: " << vv1[0][0] << endl; // 1// 遍历cout << "vv2: " << endl;for (auto& row : vv2) {for (auto e : row) {cout << e << " ";}cout << endl;}
}
2. 内存布局说明
动态二维数组的内存不是连续的:
- 外层 vector 存储的是 “内层 vector 的对象”(每个内层 vector 有自己的 _str、_size、_capacity);
- 内层 vector 的元素存储在各自的堆空间中,因此整个二维数组的内存是离散的。
六、思考与总结 ✨

 💡 一句话总结:
vector 是 “动态数组” 的完美实现,核心优势是随机访问高效与动态扩容,核心痛点是迭代器失效。使用时需注意编译器扩容差异,避免在扩容后使用旧迭代器,删除元素时正确更新迭代器,就能充分发挥其优势。
七、自测题与答案解析 🧩
- 判断题:vector 的 capacity在resize(n)(n < 当前 size)后会缩小吗?
 ❌ 不会。resize仅修改有效元素个数(size),不会改变容量(capacity),缩容需手动实现(如创建新 vector 拷贝)。
 
- 选择题:下列哪种操作不会导致 vector 迭代器失效?()
 A.push_back触发扩容
 B.erase删除中间元素
 C.pop_back删除最后一个元素
 D.reserve扩大容量
 答案:✅ C。pop_back仅修改size,不改变底层空间,迭代器指向未删除元素时不会失效(指向最后一个元素时会失效)。
 
- 简答题:为什么 G++ 的 vector 扩容倍数(2 倍)比 VS(1.5 倍)大?
 答案:2 倍扩容减少扩容次数(拷贝开销小),但浪费更多空间;1.5 倍扩容空间利用率更高,但扩容次数更多。G++ 优先追求效率,VS 兼顾空间与效率。
八、延伸阅读推荐
📗 建议阅读顺序
- 《C++ string 类实战指南:从接口用法到 OJ 解题》
- 《C++ string 类模拟实现:从浅拷贝陷阱到深拷贝》
- 《C++ vector 深度解析:动态数组的使用与底层原理实战》(本文)
- 《C++ vector 模板模拟实现:动态数组的资源管理》(下篇)
九、下篇预告:C++ vector 模板模拟实现 —— 动态数组的资源管理
掌握了 vector 的使用与底层原理后,下一篇我们将深入其模拟实现:
- 如何实现 vector 的核心构造、拷贝构造、赋值重载(深拷贝)?
- 扩容时如何正确拷贝元素(避免 memcpy 的浅拷贝陷阱)?
- 迭代器的封装与实现(支持++、*等运算符)?
- 如何处理边界情况(空 vector、插入到末尾、删除最后一个元素)?
✨ 敬请期待,我们将从 “使用 vector” 走向 “实现 vector”,彻底掌握动态数组的资源管理逻辑与模板编程技巧。
🖋 作者寄语
vector 看似简单,却藏着 C++ 容器设计的核心思想 ——“平衡效率与空间”。扩容机制的选择、迭代器的设计、接口的兼容性,都是工程化思维的体现。学习 vector 不仅是掌握一个容器,更是理解 “如何设计一个高效、通用的动态数据结构”,这种思维会贯穿你整个 STL 学习过程。
