C++ 中 std::vector 原理详解:从内存模型到 resize/reserve 的深度辨析

C++ 中 std::vector 原理详解:从内存模型到 resize/reserve 的深度辨析


一、引言:最常用的容器,最容易被忽视的细节


std::vector是 C++ 中使用最频繁的标准库容器,它提供了动态数组的功能——可以根据需要自动增长和收缩。但许多开发者在使用vector时,对resizereservesizecapacity等概念的理解停留在表面,导致写出低效甚至错误的代码。


理解vector的底层内存管理机制,特别是sizecapacity的区别、resizereserve的适用场景,是写出高效 C++ 代码的基本功。


二、核心概念速览


| 概念 | 说明 |

|------|------|

|size()| 容器中实际存储的元素个数 |

|capacity()| 容器已分配内存能容纳的元素个数(不重新分配的前提下) |

|resize(n)| 改变sizen,可能影响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 内存布局图


已分配内存(capacity=8)未使用空间(capacity-size=3)已使用空间(size=5)

[0]: A

[1]: B

[2]: C

[3]: D

[4]: E

[5]: 未初始化

[6]: 未初始化

[7]: 未初始化

start

finish

end_of_storage

U7之后


四、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_backpop_backresizeclear等 | 扩容(自动)、reserveshrink_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 扩容过程


新内存旧内存vector 内部代码新内存旧内存vector 内部代码alt[size < capacity][size == capacity (需要扩容)]push_back(newElement)检查 size == capacity?在 finish 处构造新元素finish++分配新内存(通常是旧容量的 2 倍)将旧元素移动/拷贝到新内存释放旧内存更新 start/finish/end_of_storage 指针在新位置构造元素


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 完整对比


改变实际元素数量预分配内存以提高性能释放未使用的容量

需要操作 vector 大小/容量

目的是什么?

使用 resize

使用 reserve

使用 shrink_to_fit

• 改变 size
• 可能改变 capacity
• 会构造/析构元素
• vec[0..n-1] 可安全访问

• 不改变 size
• 只增加 capacity
• 不构造元素
• 用于避免重复分配

• 请求减少 capacity 至 size
• 不保证生效
• 可能导致重新分配


| 行为 | 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的核心设计围绕三个指针(startfinishend_of_storage)展开,理解它们的关系就理解了整个容器的工作方式:


  1. size vs capacitysize是容器中实际存在的元素数量,capacity是已分配内存可容纳的元素数量。size <= capacity始终成立。[0, size)范围的元素是安全可访问的,[size, capacity)范围的内存已分配但元素未构造,不应访问。


  1. resize vs reserve
  • resize(n)改变size,会构造或析构元素,让容器真正拥有n个元素。当需要实际使用这些位置时用resize
  • reserve(n)仅预分配内存,不改变size,不构造元素。当你知道即将添加大量元素时用reserve避免多次内存重新分配。


  1. 扩容机制:当size == capacity时,push_back会触发扩容——分配更大的内存(通常 1.5 或 2 倍),将旧元素移动/拷贝到新内存,释放旧内存。这是push_back平摊 O(1) 但单次可能 O(n) 的根本原因。


  1. 性能最佳实践:如果预先知道元素数量,使用reserve一次性分配,避免重复扩容的开销(包括内存分配、元素移动、旧内存释放)。这是优化vector性能最简单有效的手段。