跳到主要内容C++ vector 容器:底层原理、扩容机制与实战用法详解 | 极客日志C++算法
C++ vector 容器:底层原理、扩容机制与实战用法详解
C++ vector 作为标准模板库中的动态数组,其底层由三个指针维护连续内存空间。文章详细解析了 vector 的构造方式、迭代器使用规则、动态扩容机制及因子差异。重点探讨了迭代器失效场景下的安全删除策略、二维 vector 的实现细节以及 memcpy 导致的浅拷贝风险,为实际开发提供避坑指南。
追风少年1 浏览 什么是 vector?
std::vector 是 C++ 标准模板库(STL)中的动态数组。与普通数组不同,它可以在运行时自动调整大小,并提供了一系列安全的接口来管理元素。
使用前需要声明头文件 <vector>。
基本语法与核心特性
vector<int> v1;
vector<double> v2;
vector<char> v3;
vector<bool> v4;
vector<string> v5;
vector<MyClass> v6;
- 连续存储:元素在内存中是连续存放的,支持 O(1) 时间复杂度的随机访问。
- 动态扩容:当空间不足时,自动申请更大的内存并迁移数据。
- 尾部高效:在末尾插入或删除元素通常是 O(1),但在中间插入为 O(n)。
vector 常用的构造声明定义
| 构造函数声明 | 接口说明 |
|---|
vector() | 无参构造 |
vector(size_type n, const value_type& val) | 构造并初始化 n 个 val |
vector(const vector& x) | 拷贝构造 |
vector(InputIterator first, InputIterator last) | 使用迭代器进行初始化构造 |
示例代码与详细讲解
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v1;
cout << "v1 size: " << v1.() << << endl;
;
cout << ;
( x : v2) cout << x << ;
cout << endl;
;
cout << ;
( x : v3) cout << x << ;
cout << endl;
arr[] = {, , , , };
;
cout << ;
( x : v4) cout << x << ;
cout << endl;
;
}
size
" (无参构造)"
vector<int> v2(5, 100)
"v2 内容: "
for
int
" "
vector<int> v3(v2)
"v3 (由 v2 拷贝而来) 内容: "
for
int
" "
int
1
2
3
4
5
vector<int> v4(arr, arr + 5)
"v4 (由数组指针初始化) 内容: "
for
int
" "
return
0
注意: string 类拷贝函数执行的也是深拷贝。
vector iterator 的使用
| 迭代器使用 | 接口说明 |
|---|
begin() / end() | 获取第一个数据位置的 iterator / 最后一个数据的下一个位置 |
rbegin() / rend() | 获取反向迭代器 |
示例代码与详细讲解
#include <vector>
#include <iostream>
using namespace std;
int main() {
vector<int> v = {10, 20, 30, 40};
cout << "反向输出结果: ";
for (auto rit = v.rbegin(); rit != v.rend(); ++rit) {
cout << *rit << " ";
}
cout << endl;
cout << "最后一个元素是:" << *v.rbegin() << endl;
return 0;
}
vector 的容量增长问题
| 容量空间 | 接口说明 |
|---|
size() | 获取数据个数 |
capacity() | 获取容量大小 |
empty() | 判断是否为空 |
resize() | 改变 vector 的 size |
reserve() | 改变 vector 的 capacity |
注意事项
1. 增容倍数:VS (1.5 倍) vs g++ (2 倍)
当 vector 的 size == capacity 时再插入元素,就会触发扩容。不同编译器实现略有差异:
- GCC (Linux):通常采用 2 倍 扩容。
- MSVC (Windows):通常采用 1.5 倍 扩容。
2. reserve:只开辟空间,不影响 size
reserve 就像是预订餐厅位子,位子给你留好了,但还没人坐。
意义: 如果你知道要插入 1000 个元素,提前 reserve(1000) 可以避免在插入过程中频繁触发耗时的'重新分配 + 数据搬移'操作。
3. resize:开辟空间 + 初始化 + 影响 size
resize 不仅订了位子,还直接安排了'假人'坐进去。
- 如果你只是想优化性能,避免多次插入导致的频繁搬家,用
reserve。
- 如果你想立刻操作这些元素(比如用下标
[] 赋值),或者想改变容器长度,用 resize。
vector 增删查改
| 接口 | 说明 |
|---|
push_back | 尾插 |
pop_back | 尾删 |
find | 查找(算法模块实现,非成员接口) |
insert | 在 position 之前插入 val |
erase | 删除 position 位置的数据 |
swap | 交换两个 vector 的数据空间 |
operator[] | 像数组一样访问 |
示例代码与详细讲解
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class VectorDemo {
private:
vector<int> _v;
public:
VectorDemo() {}
void initData() {
_v.push_back(10);
_v.push_back(20);
_v.push_back(30);
cout << "[初始化] 已手动添加 10, 20, 30" << endl;
}
void addElements() {
_v.push_back(40);
_v.insert(_v.begin() + 1, 15);
cout << "[增] 执行完毕" << endl;
}
void removeElements() {
if (!_v.empty()) {
_v.pop_back();
_v.erase(_v.begin() + 1);
cout << "[删] 执行完毕" << endl;
}
}
void findAndAccess() {
cout << "[查] 索引 1 的元素为:" << _v[1] << endl;
auto it = find(_v.begin(), _v.end(), 20);
if (it != _v.end()) {
cout << "[查] 找到 20,索引为:" << distance(_v.begin(), it) << endl;
}
}
void modifyElement(int index, int newVal) {
if (index < _v.size()) {
_v[index] = newVal;
cout << "[改] 将索引" << index << "修改为" << newVal << endl;
}
}
void swapData(vector<int>& other) {
_v.swap(other);
cout << "[换] 数据空间已交换" << endl;
}
void display() const {
cout << "当前容器内容:";
if (_v.empty()) cout << "(空)";
for (int x : _v) {
cout << x << " ";
}
cout << "\n" << endl;
}
};
int main() {
VectorDemo demo;
demo.initData();
demo.display();
demo.addElements();
demo.display();
demo.findAndAccess();
demo.modifyElement(0, 100);
demo.display();
demo.removeElements();
demo.display();
vector<int> target = {1, 2, 3};
demo.swapData(target);
demo.display();
return 0;
}
vector 底层原理与模拟实现
底层原理
很多人以为 vector 底层是一个复杂的结构,但实际上,在主流的 STL 实现(如 GCC 的 libstdc++)中,vector 的核心状态仅仅由 三个指针 来维护:
template<class T, class Alloc = alloc>
class vector {
protected:
iterator start;
iterator finish;
iterator end_of_storage;
};
借助这三个核心指针,vector 能够轻松计算出当前的状态:
- 当前元素个数(size):
finish - start
- 当前最大容量(capacity):
end_of_storage - start
- 是否为空(empty):
start == finish
核心要点: vector 的底层就是一段连续的线性内存空间。start 和 finish 之间是已经被初始化的有效数据,而 finish 到 end_of_storage 之间则是备用的未初始化内存。
动态扩容机制
既然底层是连续内存,如果备用空间(finish == end_of_storage)用完了,我们还能继续塞数据吗?答案是能,这就引出了 vector 最核心的机制:动态扩容。
vector 模拟实现
#include <iostream>
#include <algorithm>
#include <assert.h>
using namespace std;
template<class T>
class my_vector {
public:
typedef T* iterator;
my_vector() : _start(nullptr), _finish(nullptr), _end_of_storage(nullptr) {}
~my_vector() { delete[] _start; _start = _finish = _end_of_storage = nullptr; }
size_t size() const { return _finish - _start; }
size_t capacity() const { return _end_of_storage - _start; }
iterator begin() { return _start; }
iterator end() { return _finish; }
T& operator[](size_t i) { return _start[i]; }
void reserve(size_t n) {
if (n > capacity()) {
size_t oldSize = size();
T* tmp = new T[n];
if (_start) {
for (size_t i = 0; i < oldSize; ++i) tmp[i] = _start[i];
delete[] _start;
}
_start = tmp;
_finish = _start + oldSize;
_end_of_storage = _start + n;
}
}
void push_back(const T& x) {
if (_finish == _end_of_storage) {
reserve(capacity() == 0 ? 4 : capacity() * 2);
}
*_finish++ = x;
}
iterator insert(iterator pos, const T& x) {
if (_finish == _end_of_storage) {
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : capacity() * 2);
pos = _start + len;
}
for (iterator it = _finish; it > pos; --it) {
*it = *(it - 1);
}
*pos = x;
++_finish;
return pos;
}
iterator erase(iterator pos) {
for (iterator it = pos + 1; it < _finish; ++it) {
*(it - 1) = *it;
}
--_finish;
return pos;
}
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
};
int main() {
my_vector<int> v;
v.push_back(10);
v.push_back(20);
v.push_back(30);
v.insert(v.begin() + 1, 15);
v.erase(v.begin() + 2);
cout << "Vector contents: ";
for (auto e : v) cout << e << " ";
cout << "\nSize: " << v.size() << ", Capacity: " << v.capacity() << endl;
return 0;
}
关于 vector 动态二维数组
在 C++ 中,使用 std::vector 来构建动态二维数组是非常主流且安全的做法。相比于传统的 C 语言风格指针(int** arr),它能够自动管理内存,避免了内存泄漏和繁琐的 new/delete 操作。
核心结构与内存布局
vector<vector<int>> 的本质是**'容器的容器'**。
它在内存中并不是一块完整连续的矩阵。
- 外层
vector:是一个一维数组,里面存储的是内层 vector 的控制结构(包含指针、大小、容量等信息)。
- 内层
vector:代表每一行。每一行各自在堆区开辟一块连续的内存空间来存储实际的元素。
核心推论: 因为每一行是独立分配的,所以二维 vector 完全支持锯齿数组(Jagged Array),即每一行的列数可以完全不同。
创建与初始化方式
① 创建空的二维数组
常用于一开始不知道行数和列数,需要后续通过 push_back 动态添加的场景。
vector<vector<int>> matrix;
② 指定行数和列数(最常用)
如果已经知道具体的行数 M 和列数 N,可以在创建时直接分配好空间,并赋予初始值。
int M = 3;
int N = 4;
vector<vector<int>> matrix(M, vector<int>(N, 0));
③ 使用初始化列表(硬编码)
vector<vector<int>> matrix = {{1, 2, 3}, {4, 5}, {6, 7, 8, 9}};
动态调整大小与插入数据
① 添加新的一行
vector<vector<int>> matrix;
vector<int> newRow = {10, 20, 30};
matrix.push_back(newRow);
② 在已有行中添加新元素
③ 动态调整矩阵尺寸
matrix.resize(5);
matrix[0].resize(10);
元素的访问与修改
① 使用 operator[] (无边界检查,速度快)
int val = matrix[1][2];
matrix[1][2] = 99;
② 使用 at() (有边界检查,更安全)
如果索引越界,at() 会抛出 std::out_of_range 异常,非常适合调试代码。
try {
int val = matrix.at(1).at(2);
} catch (const out_of_range& e) {
}
遍历二维数组的高效方法
① C++11 范围 for 循环(最推荐,代码最简洁)
如果不需要知道元素的行号和列号,强烈建议使用此方法。
for (const auto& row : matrix) {
for (int val : row) {
cout << val << " ";
}
cout << endl;
}
② 传统的索引遍历
for (size_t i = 0; i < matrix.size(); ++i) {
for (size_t j = 0; j < matrix[i].size(); ++j) {
cout << matrix[i][j] << " ";
}
cout << endl;
}
函数参数传递
将二维 vector 传递给函数时,切记要使用引用传递,否则会触发整个二维数组的深度拷贝,极其消耗性能。
void printMatrix(const vector<vector<int>>& mat) {
}
void modifyMatrix(vector<vector<int>>& mat) {
}
关于 vector 的迭代器失效问题
迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对指针进行了封装。因此迭代器失效,实际就是迭代器底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃。
对于 vector 可能会导致其迭代器失效的操作有:
- 会引起其底层空间改变的操作,都有可能是迭代器失效,比如:
resize、reserve、insert、assign、push_back 等。
- 指定位置元素的删除操作
erase。
经典案例:遍历时删除元素
这是引发迭代器失效最常见的灾难现场。当我们删除一个元素时,该位置之后的所有元素都会向前移动填补空缺,导致当前迭代器变成野指针。
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> vec = {1, 2, 3, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ++it) {
if (*it == 3) {
vec.erase(it);
}
}
return 0;
}
✅ 正确示范:使用返回值'重新赋值'
erase() 函数的设计非常巧妙,它在删除元素后,会返回一个指向被删除元素下一个位置的全新且有效的迭代器。
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> vec = {1, 2, 3, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ) {
if (*it == 3) {
it = vec.erase(it);
} else {
++it;
}
}
for (int val : vec) cout << val << " ";
return 0;
}
memcpy 引起的浅拷贝问题
使用 memcpy 进行的拷贝,以下代码会发生什么问题?
int main() {
vector<string> v;
v.push_back("1111");
v.push_back("2222");
v.push_back("3333");
return 0;
}
深度原因分析
- 扩容前(旧空间):
vector 内部存储了 3 个 string 对象。每个 string 对象内部都有一个指针(比如 char* _str)指向堆上的字符数据(如 "1111")。
- 执行 memcpy 拷贝:
memcpy 把旧空间里 string 对象的字节直接复制到新空间。这意味着:新空间里 string 对象的 _str 指针指向的地址,和旧空间里一模一样。
- 释放旧空间(核心死穴):
扩容逻辑中会调用
delete[] _start。这会触发旧空间里每个 string 对象的析构函数。
- 旧空间的
string 析构了,执行了 delete[] _str。
- 结果:新空间里的
string 对象现在全是指向已释放内存的野指针!
- 再次访问或析构:
当
main 函数结束,新空间的 vector 析构时,会再次对那些已经被释放过的地址调用 delete[]。
- 结论:触发 Double Free(两次释放同一块内存),程序直接挂掉。
因此,对于包含指针成员的类,严禁直接使用 memcpy 进行拷贝,应依赖拷贝构造函数或重载赋值运算符来实现深拷贝。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online