C++ Vector 详解:从入门到精通 – wiki词典


C++ Vector 详解:从入门到精通

在 C++ 标准库中,std::vector 是一个极其强大且常用的容器,它提供了动态数组的功能。与 C 风格的静态数组不同,std::vector 可以根据需要自动调整大小,这使得它在处理不确定数量的数据时非常灵活和高效。本文将带你从 std::vector 的基础用法,逐步深入到其内部机制、性能考量以及高级应用。

一、std::vector 简介

std::vector 是一个序列容器,它封装了动态大小的数组。它在内存中是连续存储的,这意味着可以通过指针算术或索引快速访问其元素,这与 C 风格数组具有相同的性能优势。然而,vector 的关键优势在于它能够自动处理内存分配和释放,使得程序员可以专注于业务逻辑而不是底层内存管理。

主要特点:

  • 动态大小: 可以在运行时改变其大小。
  • 连续存储: 元素存储在连续的内存块中,支持高效的随机访问。
  • 自动管理内存: 负责元素的构造、析构以及内存的分配与回收。
  • 效率: 在末尾添加或删除元素通常是常数时间复杂度(O(1)),但在中间或开头插入/删除元素则可能需要线性时间复杂度(O(n)),因为它涉及到元素的移动。

二、基本用法

1. 声明与初始化

std::vector 是一个模板类,你需要指定它存储的元素类型。

“`cpp

include

include

include

int main() {
// 声明一个空的 int 类型 vector
std::vector vec1;

// 声明并初始化一个包含 5 个默认构造元素的 vector (对于 int 来说是 0)
std::vector<int> vec2(5); // vec2: {0, 0, 0, 0, 0}

// 声明并初始化一个包含 3 个指定值的 vector
std::vector<int> vec3(3, 10); // vec3: {10, 10, 10}

// 使用初始化列表初始化
std::vector<int> vec4 = {1, 2, 3, 4, 5}; // vec4: {1, 2, 3, 4, 5}

// 从另一个 vector 拷贝构造
std::vector<int> vec5(vec4); // vec5: {1, 2, 3, 4, 5}

// 从迭代器范围初始化
std::vector<int> vec6(vec4.begin() + 1, vec4.end() - 1); // vec6: {2, 3, 4}

// 声明一个字符串类型的 vector
std::vector<std::string> names = {"Alice", "Bob", "Charlie"};

return 0;

}
“`

2. 添加元素

  • push_back(value): 在 vector 末尾添加一个元素的副本。
  • emplace_back(args...): 在 vector 末尾就地构造一个元素(C++11 及以后),避免不必要的拷贝或移动。

“`cpp
std::vector numbers;
numbers.push_back(10); // numbers: {10}
numbers.push_back(20); // numbers: {10, 20}
numbers.emplace_back(30); // numbers: {10, 20, 30}

struct MyClass {
int x;
std::string s;
MyClass(int val, const std::string& str) : x(val), s(str) {}
};

std::vector objects;
// 使用 push_back 需要先构造临时对象
objects.push_back(MyClass(1, “Hello”));
// 使用 emplace_back 直接在 vector 内部构造
objects.emplace_back(2, “World”);
“`

3. 访问元素

  • operator[]: 提供对元素的引用,不执行边界检查。
  • at(index): 提供对元素的引用,执行边界检查,越界时抛出 std::out_of_range 异常。
  • front(): 返回第一个元素的引用。
  • back(): 返回最后一个元素的引用。
  • data(): 返回指向 vector 内部数组的指针(C++11 及以后),可用于与 C 风格 API 交互。

“`cpp
std::vector data = {10, 20, 30, 40, 50};

std::cout << data[0] << std::endl; // 输出 10
std::cout << data.at(2) << std::endl; // 输出 30

try {
std::cout << data.at(10) << std::endl; // 抛出 std::out_of_range 异常
} catch (const std::out_of_range& e) {
std::cerr << “Error: ” << e.what() << std::endl;
}

std::cout << “First element: ” << data.front() << std::endl; // 输出 10
std::cout << “Last element: ” << data.back() << std::endl; // 输出 50

int* raw_ptr = data.data();
for (size_t i = 0; i < data.size(); ++i) {
std::cout << raw_ptr[i] << ” “;
}
std::cout << std::endl; // 输出 10 20 30 40 50
“`

4. 迭代器

std::vector 支持随机访问迭代器,可以像指针一样使用。

  • begin() / cbegin(): 返回指向第一个元素的迭代器(常量迭代器)。
  • end() / cend(): 返回指向最后一个元素“之后”位置的迭代器(常量迭代器)。
  • rbegin() / crbegin(): 返回指向最后一个元素的逆向迭代器(常量逆向迭代器)。
  • rend() / crend(): 返回指向第一个元素“之前”位置的逆向迭代器(常量逆向迭代器)。

“`cpp
std::vector nums = {1, 2, 3, 4, 5};

// 使用范围 for 循环 (C++11 及以后)
for (int n : nums) {
std::cout << n << ” “;
}
std::cout << std::endl; // 输出 1 2 3 4 5

// 使用迭代器
for (std::vector::iterator it = nums.begin(); it != nums.end(); ++it) {
it = 2; // 修改元素
std::cout << *it << ” “;
}
std::cout << std::endl; // 输出 2 4 6 8 10

// 逆向遍历
for (auto it = nums.rbegin(); it != nums.rend(); ++it) {
std::cout << *it << ” “;
}
std::cout << std::endl; // 输出 10 8 6 4 2
“`

5. 大小与容量

理解 size()capacity() 的区别至关重要。

  • size(): 返回 vector 中实际元素的数量。
  • capacity(): 返回 vector 在重新分配内存之前可以容纳的元素总数。
  • max_size(): 返回 vector 在给定系统或库实现下能够容纳的最大元素数量。
  • empty(): 检查 vector 是否为空(size() == 0)。

“`cpp
std::vector v;
std::cout << “Initial size: ” << v.size() << “, capacity: ” << v.capacity() << std::endl;

for (int i = 0; i < 10; ++i) {
v.push_back(i);
std::cout << “After push_back ” << i << “: size: ” << v.size() << “, capacity: ” << v.capacity() << std::endl;
}
// 可以观察到 capacity 在 size 达到某个值时会成倍增长,这是为了摊销重新分配的成本。
“`

三、修改 vector

1. 移除元素

  • pop_back(): 移除最后一个元素。
  • clear(): 移除所有元素,size() 变为 0,但 capacity() 通常不变。
  • erase(iterator position): 移除 position 处的元素。
  • erase(iterator first, iterator last): 移除 [first, last) 范围内的元素。

cpp
std::vector<int> data = {1, 2, 3, 4, 5};
data.pop_back(); // data: {1, 2, 3, 4}
data.erase(data.begin() + 1); // 移除索引为 1 的元素 (2), data: {1, 3, 4}
data.erase(data.begin(), data.begin() + 2); // 移除前两个元素 (1, 3), data: {4}
data.clear(); // data: {}

2. 插入元素

  • insert(iterator position, value): 在 position 前插入一个元素的副本。
  • insert(iterator position, count, value): 在 position 前插入 countvalue
  • insert(iterator position, InputIt first, InputIt last): 在 position 前插入 [first, last) 范围的元素。
  • emplace(iterator position, args...): 在 position 前就地构造一个元素(C++11 及以后)。

“`cpp
std::vector data = {1, 2, 5};
data.insert(data.begin() + 2, 3); // data: {1, 2, 3, 5}
data.insert(data.begin() + 3, 4); // data: {1, 2, 3, 4, 5}

std::vector more_data = {10, 20};
data.insert(data.end(), more_data.begin(), more_data.end()); // data: {1, 2, 3, 4, 5, 10, 20}

data.emplace(data.begin(), 0); // data: {0, 1, 2, 3, 4, 5, 10, 20}
“`

注意:inserterase 操作,特别是当它们发生在 vector 中间时,可能会导致大量元素的移动,从而降低性能。

3. 调整大小与容量

  • resize(count): 将 vectorsize 调整为 count。如果 count 大于当前 size,新元素会被默认构造;如果 count 小于当前 size,多余元素会被销毁。
  • resize(count, value): 与上一个版本类似,但新元素会被初始化为 value
  • reserve(capacity): 确保 vectorcapacity 至少为 capacity。如果 capacity 小于当前 capacity,则此调用无效。它不会改变 size()
  • shrink_to_fit(): 请求 vector 将其 capacity 减少到 size()。这只是一个请求,标准库不保证一定会执行(C++11 及以后)。

“`cpp
std::vector numbers = {1, 2, 3};
std::cout << “Size: ” << numbers.size() << “, Capacity: ” << numbers.capacity() << std::endl;

numbers.resize(5); // numbers: {1, 2, 3, 0, 0}
std::cout << “Size: ” << numbers.size() << “, Capacity: ” << numbers.capacity() << std::endl;

numbers.resize(2); // numbers: {1, 2}
std::cout << “Size: ” << numbers.size() << “, Capacity: ” << numbers.capacity() << std::endl;

numbers.reserve(10); // 确保 capacity 至少为 10
std::cout << “Size: ” << numbers.size() << “, Capacity: ” << numbers.capacity() << std::endl; // Size 仍然是 2
“`

四、std::vector 内部机制与性能考量

1. 容量增长策略

push_backemplace_back 导致 vectorsize() 超过其 capacity() 时,vector 会进行内存重新分配。这通常涉及以下步骤:

  1. 分配一块新的更大的内存块(通常是当前容量的 1.5 倍或 2 倍)。
  2. 将旧内存块中的所有元素移动(如果元素类型支持移动语义)或拷贝到新内存块。
  3. 销毁旧内存块中的所有元素。
  4. 释放旧内存块。

这个过程是昂贵的,尤其是对于包含复杂对象的 vector。为了摊销这种成本,vector 采用了一种策略,即在需要时分配比当前所需更多的内存。这就是 capacity() 概念的由来。

性能提示:
如果你事先知道 vector 将要存储的元素大致数量,使用 reserve() 预先分配内存可以显著提高性能,避免多次重新分配。

cpp
std::vector<int> large_vec;
large_vec.reserve(10000); // 预分配足够的内存
for (int i = 0; i < 10000; ++i) {
large_vec.push_back(i); // 避免了多次重新分配
}

2. 内存局部性

vector 的元素在内存中是连续存储的。这对于 CPU 缓存来说是非常有利的,因为当访问一个元素时,其附近的元素很可能也被加载到缓存中,从而提高后续访问的速度(缓存局部性)。这也是 vector 在许多场景下比 std::liststd::deque 更快的原因。

3. 迭代器失效

vector 发生重新分配时,所有指向 vector 内部元素的迭代器、指针和引用都会失效。这是 vector 使用时一个常见的陷阱。

可能导致迭代器失效的操作:

  • push_back() / emplace_back():如果导致容量增长,所有迭代器失效。
  • insert():在其插入点之后的元素迭代器失效;如果导致容量增长,所有迭代器失效。
  • erase():在擦除点及其之后的元素迭代器失效。
  • clear():所有迭代器失效。
  • resize() / reserve():如果导致容量增长,所有迭代器失效。

解决方案:

  • 在循环中进行插入或删除操作时,应格外小心。erase()insert() 会返回一个指向新位置的有效迭代器,你应该使用它来更新你的迭代器。
  • 优先使用基于索引的循环(for (size_t i = 0; i < vec.size(); ++i)),因为索引不会失效。
  • 如果不可避免地需要进行大量中间插入/删除,考虑使用 std::liststd::deque

cpp
std::vector<int> numbers = {1, 2, 3, 4, 5};
for (auto it = numbers.begin(); it != numbers.end(); /* no increment here */) {
if (*it % 2 == 0) {
it = numbers.erase(it); // erase 返回指向下一个元素的迭代器
} else {
++it;
}
}
// numbers: {1, 3, 5}

五、高级应用与最佳实践

1. 移动语义 (C++11)

vector 的元素类型如果支持移动语义(即有移动构造函数和移动赋值运算符),那么在 vector 重新分配时,元素的移动将比拷贝更高效。对于自定义类型,提供移动语义可以显著提升 vector 的性能。

emplace_backemplace 在大多数情况下优于 push_backinsert,因为它们可以直接在 vector 内部构造元素,避免了额外的拷贝或移动操作。

2. 使用 <algorithm>

std::vector 是 STL 算法的理想容器,因为它的连续存储使其与算法的工作方式非常匹配。

“`cpp

include // for std::sort, std::find, etc.

include // for std::accumulate

std::vector values = {5, 2, 8, 1, 9, 4};

// 排序
std::sort(values.begin(), values.end()); // values: {1, 2, 4, 5, 8, 9}

// 查找
auto it = std::find(values.begin(), values.end(), 8);
if (it != values.end()) {
std::cout << “Found 8 at index: ” << std::distance(values.begin(), it) << std::endl;
}

// 求和
long long sum = std::accumulate(values.begin(), values.end(), 0LL);
std::cout << “Sum: ” << sum << std::endl; // 输出 Sum: 29

// 拷贝到另一个 vector
std::vector copied_values;
copied_values.resize(values.size()); // 预留空间
std::copy(values.begin(), values.end(), copied_values.begin());
“`

3. 与 C 风格数组的转换

vectordata() 成员函数返回指向底层数组的指针,这使得 vector 可以方便地与需要 C 风格数组的函数交互。

“`cpp
void process_c_array(int* arr, size_t size) {
for (size_t i = 0; i < size; ++i) {
arr[i]++;
}
}

std::vector my_vec = {1, 2, 3};
process_c_array(my_vec.data(), my_vec.size()); // my_vec: {2, 3, 4}
``
**注意:** 确保传递给 C 风格函数的指针不会超出
vector的当前size范围,并且在函数执行期间vector` 不会发生重新分配,否则指针可能失效。

4. 交换 vector

std::vector::swap() 函数可以高效地交换两个 vector 的内容,通常是常数时间复杂度,因为它只交换内部的指针和大小/容量信息,而不是实际的元素。

“`cpp
std::vector vec_a = {1, 2, 3};
std::vector vec_b = {4, 5, 6, 7};

vec_a.swap(vec_b);
// vec_a: {4, 5, 6, 7}
// vec_b: {1, 2, 3}
这对于清空一个 `vector` 并释放其内存非常有用:cpp
std::vector big_vec(1000000, 42);
// … 使用 big_vec …
std::vector().swap(big_vec); // 将 big_vec 交换为一个空的临时 vector,并释放 big_vec 的内存
// big_vec 现在是空的,且 capacity 也可能变为 0
``
或者从 C++11 开始,可以使用
big_vec.shrink_to_fit()来尝试减少容量,但swap` 技巧更具确定性。

六、总结

std::vector 是 C++ 中最基本也是最重要的容器之一。理解其动态大小、连续存储、内存管理机制以及迭代器失效规则,对于编写高效、健壮的 C++ 代码至关重要。通过合理地使用 reserve 预分配内存、emplace_back 就地构造元素、以及熟悉其与 STL 算法的结合,你可以充分发挥 vector 的强大功能,使其成为你处理数据时的首选工具。

希望这篇详尽的文章能帮助你从入门到精通 std::vector

滚动至顶部