C++ 中 std::vector 原理详解:从内存模型到 resize/reserve 的深度辨析
一、引言:最常用的容器,最容易被忽视的细节
std::vector是 C++ 中使用最频繁的标准库容器,它提供了动态数组的功能——可以根据需要自动增长和收缩。但许多开发者在使用vector时,对resize、reserve、size、capacity等概念的理解停留在表面,导致写出低效甚至错误的代码。
理解vector的底层内存管理机制,特别是size与capacity的区别、resize与reserve的适用场景,是写出高效 C++ 代码的基本功。
二、核心概念速览
| 概念 | 说明 |
|------|------|
|size()| 容器中实际存储的元素个数 |
|capacity()| 容器已分配内存能容纳的元素个数(不重新分配的前提下) |
|resize(n)| 改变size为n,可能影响capacity,会构造/析构元素 |
|reserve(n)| 保证capacity至少为n,不改变size,不构造元素 |
|shrink_to_fit()| 请求将capacity减小到与size匹配(不保证) |
| 扩容策略 | 当size == capacity时,新分配通常是原来容量的 1.5 倍或 2 倍 |
| 内存连续性 |vector保证元素在内存中连续存储,可通过data()获取原始指针 |
三、vector 的内存模型
3.1 内部结构
std::vector通常由三个指针管理(简化实现):
template<typename T> class vector { T* start; // 指向已分配内存的起始位置 T* finish; // 指向最后一个实际元素的下一个位置 T* end_of_storage; // 指向已分配内存的末尾 };这三个指针的关系决定了size()和capacity():
size() = finish - start; // 实际元素数量 capacity() = end_of_storage - start; // 已分配空间可容纳的元素数量 empty() = (start == finish); // 是否为空3.2 内存布局图
四、size 与 capacity 的区别
4.1 基本概念
#include <iostream> #include <vector> int main() { std::vector<int> vec; std::cout << "Initial:" << std::endl; std::cout << " size: " << vec.size() << std::endl; // 0 std::cout << " capacity: " << vec.capacity() << std::endl; // 0 // 添加元素 for (int i = 0; i < 10; ++i) { vec.push_back(i); std::cout << "After push_back(" << i << "): " << "size=" << vec.size() << ", capacity=" << vec.capacity() << std::endl; } }可能的输出(依赖编译器实现):
Initial: size: 0 capacity: 0 After push_back(0): size=1, capacity=1 After push_back(1): size=2, capacity=2 After push_back(2): size=3, capacity=4 After push_back(3): size=4, capacity=4 After push_back(4): size=5, capacity=8 After push_back(5): size=6, capacity=8 After push_back(6): size=7, capacity=8 After push_back(7): size=8, capacity=8 After push_back(8): size=9, capacity=16 After push_back(9): size=10, capacity=16关键观察:
size每次push_back后精确递增capacity在某些push_back时发生跳跃(扩容),新容量通常是旧容量的 2 倍(GCC)或 1.5 倍(MSVC)
4.2 size vs capacity 核心区别
| 维度 | size | capacity |
|------|------|----------|
| 含义 | 当前实际元素数量 | 已分配内存可容纳的元素数量 |
| 访问安全范围 |[0, size)可安全访问 |[0, capacity)内存已分配,但[size, capacity)元素未构造 |
| 改变方式 |push_back、pop_back、resize、clear等 | 扩容(自动)、reserve、shrink_to_fit|
| 关系 |size <= capacity始终成立 |capacity >= size始终成立 |
五、resize 与 reserve 的区别
5.1 resize:改变实际元素数量
#include <iostream> #include <vector> int main() { std::vector<int> vec = {1, 2, 3}; std::cout << "Before resize:" << std::endl; std::cout << " size: " << vec.size() << std::endl; // 3 std::cout << " capacity: " << vec.capacity() << std::endl; // 3 // 输出: 1 2 3 // resize 增大 size vec.resize(5); // 新元素默认初始化为 0 std::cout << "\nAfter resize(5):" << std::endl; std::cout << " size: " << vec.size() << std::endl; // 5 std::cout << " capacity: " << vec.capacity() << std::endl; // 可能 >5 // 输出: 1 2 3 0 0 // resize 减小 size (不改变 capacity) vec.resize(2); std::cout << "\nAfter resize(2):" << std::endl; std::cout << " size: " << vec.size() << std::endl; // 2 std::cout << " capacity: " << vec.capacity() << std::endl; // 不变 // 输出: 1 2 (后三个元素被析构) }resize 的行为:
resize(n)将size设置为n- 如果
n > size:在末尾添加n - size个元素(默认构造或使用提供的值) - 如果
n < size:销毁末尾多余的size - n个元素 - 如果
n > capacity:先扩容,再构造新元素 - 会调用构造函数/析构函数
5.2 reserve:预分配内存,不改变元素数量
#include <iostream> #include <vector> #include <chrono> int main() { std::vector<int> vec; std::cout << "Before reserve:" << std::endl; std::cout << " size: " << vec.size() << std::endl; // 0 std::cout << " capacity: " << vec.capacity() << std::endl; // 0 // reserve 预分配内存 vec.reserve(100); std::cout << "\nAfter reserve(100):" << std::endl; std::cout << " size: " << vec.size() << std::endl; // 0 (未改变!) std::cout << " capacity: " << vec.capacity() << std::endl; // 100 // 添加元素时不会再重新分配 for (int i = 0; i < 100; ++i) { vec.push_back(i); // 不会触发任何内存重新分配 } // reserve 减小 capacity 时(如果 n < capacity): // 通常编译器实现会忽略这种请求 vec.reserve(10); std::cout << "\nAfter reserve(10):" << std::endl; std::cout << " size: " << vec.size() << std::endl; // 100 std::cout << " capacity: " << vec.capacity() << std::endl; // 通常 >= 100 }reserve 的行为:
- 仅改变
capacity,不改变size - 不构造任何元素
- 如果
n <= capacity:通常什么都不做(不会主动缩容) - 如果
n > capacity:分配新内存,将现有元素移动到新内存,释放旧内存 - 不会调用构造函数/析构函数来影响元素
5.3 性能对比:预分配 vs 动态扩容
#include <iostream> #include <vector> #include <chrono> int main() { const int N = 10000000; // 没有 reserve:多次重新分配 { auto start = std::chrono::high_resolution_clock::now(); std::vector<int> vec; for (int i = 0; i < N; ++i) { vec.push_back(i); // 会触发约 24 次内存重新分配 } auto end = std::chrono::high_resolution_clock::now(); auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); std::cout << "Without reserve: " << duration.count() << "ms" << std::endl; } // 使用 reserve:一次分配 { auto start = std::chrono::high_resolution_clock::now(); std::vector<int> vec; vec.reserve(N); // 一次性预分配 for (int i = 0; i < N; ++i) { vec.push_back(i); // 不需要重新分配 } auto end = std::chrono::high_resolution_clock::now(); auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); std::cout << "With reserve: " << duration.count() << "ms" << std::endl; } }六、vector 扩容原理
6.1 扩容过程
6.2 扩容的代价
// 扩容涉及三个步骤: // 1. 分配更大的内存块 // 2. 将现有元素移动(或拷贝)到新内存 // 3. 释放旧内存 // 这使得 push_back 在扩容时是 O(n),而不是 O(1) // 但平摊复杂度仍然是 O(1)6.3 扩容因子
| 编译器 | 扩容因子 | 策略 |
|--------|:---:|------|
| GCC (libstdc++) | 2 | 每次容量翻倍 |
| Clang (libc++) | 2 | 每次容量翻倍 |
| MSVC | 1.5 | 每次容量变为原来的 1.5 倍 |
七、resize vs reserve 完整对比
| 行为 | resize(n) | reserve(n) |
|------|-----------|------------|
| 改变 size | 是,设置为 n | 否 |
| 改变 capacity | 如果 n > capacity 则扩容 | 如果 n > capacity 则扩容 |
| 构造新元素 | 是(当 n > size 时) | 否 |
| 销毁多余元素 | 是(当 n < size 时) | 否 |
| 访问 vec[i] (i<n) | 安全 | 不安全!元素未构造 |
| 主要用途 | 需要实际改变元素数量 | 优化性能,避免重复分配 |
| 时间复杂度 | O(\|n - size\|) + 可能的扩容 | O(size) 如果需要扩容 |
八、实际使用场景
8.1 使用 reserve 优化性能
// 场景:从文件读取数据到 vector std::vector<std::string> readLines(const std::string& filename) { std::ifstream file(filename); std::vector<std::string> lines; std::string line; // 不好的做法:不知道文件大小,不断触发扩容 // while (std::getline(file, line)) { lines.push_back(line); } // 更好的做法:先预估大小,预分配 lines.reserve(10000); // 预估 10000 行 while (std::getline(file, line)) { lines.push_back(line); // 不会频繁重新分配 } return lines; }8.2 使用 resize 初始化元素
// 场景:需要一个填充了默认值的数组 std::vector<int> counters(100); // 构造时指定 size = 100, 全部初始化为 0 std::vector<int> counters2; counters2.resize(100, -1); // 同上,全部初始化为 -1 // 场景:需要访问索引 i 的安全方式 void processIndex(std::vector<int>& vec, int idx) { if (idx >= vec.size()) { vec.resize(idx + 1); // 保证索引存在 } vec[idx] = 42; }8.3 使用 swap 技巧缩容(C++11 前)
// C++11 之前没有 shrink_to_fit,使用 swap 技巧 std::vector<int> vec; // ... 大量操作后 size=100, capacity=10000 // 收缩容量 std::vector<int>(vec).swap(vec); // 创建一个临时 vector(vec 的拷贝,capacity = size) // 然后与原 vector 交换内部指针 // 原 vector 的旧内存被临时 vector 带走并释放 // C++11 起直接使用: vec.shrink_to_fit();8.4 常见错误:混淆 reserve 后的访问
// 错误示例 std::vector<int> vec; vec.reserve(10); vec[0] = 42; // 未定义行为!虽然 capacity >= 10,但 size 仍然是 0 // vec[0] 的元素还未构造 // 正确做法 vec.resize(10); // 或者 push_back vec[0] = 42; // 现在安全了九、总结
std::vector的核心设计围绕三个指针(start、finish、end_of_storage)展开,理解它们的关系就理解了整个容器的工作方式:
- size vs capacity:
size是容器中实际存在的元素数量,capacity是已分配内存可容纳的元素数量。size <= capacity始终成立。[0, size)范围的元素是安全可访问的,[size, capacity)范围的内存已分配但元素未构造,不应访问。
- resize vs reserve:
resize(n)改变size,会构造或析构元素,让容器真正拥有n个元素。当需要实际使用这些位置时用resize。reserve(n)仅预分配内存,不改变size,不构造元素。当你知道即将添加大量元素时用reserve避免多次内存重新分配。
- 扩容机制:当
size == capacity时,push_back会触发扩容——分配更大的内存(通常 1.5 或 2 倍),将旧元素移动/拷贝到新内存,释放旧内存。这是push_back平摊 O(1) 但单次可能 O(n) 的根本原因。
- 性能最佳实践:如果预先知道元素数量,使用
reserve一次性分配,避免重复扩容的开销(包括内存分配、元素移动、旧内存释放)。这是优化vector性能最简单有效的手段。