C++基础——vector容器

详解C++ STL中vector容器的特性、用法、性能优化及最佳实践

C++ vector容器详解

vector是C++标准模板库(STL)中最常用的序列容器,它实现了动态数组的功能,能够自动管理内存并在运行时动态调整大小。作为C++程序员的必备工具,掌握vector的使用技巧对提高代码质量和性能至关重要。

基本特性

vector容器具有以下关键特性:

  • 动态数组:底层实现为连续的动态分配数组
  • 自动内存管理:自动处理内存分配和释放
  • 随机访问:支持常数时间复杂度的随机访问
  • 动态扩容:容量不足时自动扩展
  • 类型安全:基于模板实现,可存储任意类型

与原生数组相比,vector提供了更安全、更灵活的接口,同时在性能上几乎没有损失。

头文件与基本用法

头文件引入

使用vector前必须包含相应头文件:

1#include <vector>  // vector的头文件

创建与初始化

vector提供多种初始化方式,适应不同场景:

 1// 默认构造函数
 2std::vector<int> vec1;                    // 空vector
 3
 4// 指定大小
 5std::vector<int> vec2(5);                 // 包含5个值为0的元素
 6
 7// 指定大小和初值
 8std::vector<std::string> vec3(3, "Hello"); // 包含3个"Hello"字符串
 9
10// 使用初始化列表(C++11)
11std::vector<double> vec4{1.1, 2.2, 3.3};   // 包含3个指定的double值
12
13// 从其他容器构造
14int arr[] = {1, 2, 3, 4, 5};
15std::vector<int> vec5(arr, arr + 5);      // 从数组构造
16
17// 从其他vector拷贝构造
18std::vector<int> vec6(vec5);              // 拷贝vec5的内容
19
20// 使用迭代器范围初始化
21std::vector<int> vec7(vec5.begin() + 1, vec5.end() - 1); // 只包含vec5的部分元素

常用成员函数

容量管理

函数 说明 复杂度
size() 返回当前元素数量 O(1)
capacity() 返回当前分配的存储容量 O(1)
empty() 检查容器是否为空 O(1)
reserve(n) 预分配至少能容纳n个元素的内存空间 O(n)
shrink_to_fit() 减少容量以适应当前大小(C++11) O(n)
max_size() 返回可容纳的最大元素数 O(1)
resize(n) 调整容器大小为n个元素 O(n)

元素访问

函数 说明 安全性
operator[] 通过下标访问元素(不检查边界) 不安全
at(n) 通过下标访问元素(会检查边界,越界抛出异常) 安全
front() 访问第一个元素 需预先检查非空
back() 访问最后一个元素 需预先检查非空
data() 返回指向底层数组的指针(C++11) 需预先检查非空

修改操作

函数 说明 复杂度
push_back(val) 在末尾添加元素 均摊O(1)
emplace_back(args) 在末尾原地构造元素(C++11) 均摊O(1)
pop_back() 移除末尾元素 O(1)
insert(pos, val) 在指定位置插入元素 O(n)
emplace(pos, args) 在指定位置原地构造元素(C++11) O(n)
erase(pos) 移除指定位置的元素 O(n)
clear() 清空所有元素 O(n)
assign(n, val) 替换内容为n个值为val的元素 O(n)
swap(other) 交换两个vector的内容 O(1)

实际应用示例

基本操作流程

以下示例展示了vector的基本操作流程:

 1#include <iostream>
 2#include <vector>
 3#include <string>
 4
 5int main() {
 6    // 创建并初始化vector
 7    std::vector<int> numbers = {10, 20, 30, 40, 50};
 8    
 9    // 添加元素
10    numbers.push_back(60);        // 在末尾添加元素
11    numbers.insert(numbers.begin() + 2, 25);  // 在第3个位置插入25
12    
13    // 使用emplace_back直接构造元素(C++11)
14    std::vector<std::pair<std::string, int>> records;
15    records.emplace_back("Alice", 25);  // 无需创建临时对象
16    
17    // 访问元素
18    std::cout << "第一个元素: " << numbers.front() << std::endl;
19    std::cout << "最后一个元素: " << numbers.back() << std::endl;
20    std::cout << "索引为2的元素: " << numbers[2] << std::endl;
21    
22    try {
23        // 安全访问 - 会检查边界
24        std::cout << "安全访问: " << numbers.at(10) << std::endl;
25    } catch (const std::out_of_range& e) {
26        std::cout << "边界检查异常: " << e.what() << std::endl;
27    }
28    
29    // 遍历vector (C++11范围for循环)
30    std::cout << "所有元素: ";
31    for (const auto& num : numbers) {
32        std::cout << num << " ";
33    }
34    std::cout << std::endl;
35    
36    // 使用迭代器遍历并修改
37    std::cout << "修改后的元素: ";
38    for (auto it = numbers.begin(); it != numbers.end(); ++it) {
39        *it *= 2;  // 将每个元素乘以2
40        std::cout << *it << " ";
41    }
42    std::cout << std::endl;
43    
44    // 删除元素
45    numbers.pop_back();  // 删除最后一个元素
46    numbers.erase(numbers.begin());  // 删除第一个元素
47    
48    // 使用迭代器删除特定元素
49    auto it = std::find(numbers.begin(), numbers.end(), 50);
50    if (it != numbers.end()) {
51        numbers.erase(it);  // 删除值为50的元素
52    }
53    
54    // 容量信息
55    std::cout << "元素个数: " << numbers.size() 
56              << ", 容量: " << numbers.capacity() << std::endl;
57              
58    // 释放多余容量
59    numbers.shrink_to_fit();
60    std::cout << "收缩后容量: " << numbers.capacity() << std::endl;
61    
62    // 清空vector
63    numbers.clear();
64    std::cout << "清空后大小: " << numbers.size() << std::endl;
65    
66    return 0;
67}

自定义类型

vector可以存储任何类型的元素,包括自定义类型:

 1#include <iostream>
 2#include <vector>
 3#include <string>
 4
 5class User {
 6private:
 7    std::string name;
 8    int age;
 9
10public:
11    User(const std::string& n, int a) : name(n), age(a) {
12        std::cout << "构造: " << name << std::endl;
13    }
14    
15    // 移动构造函数(C++11)
16    User(User&& other) noexcept : name(std::move(other.name)), age(other.age) {
17        std::cout << "移动构造: " << name << std::endl;
18    }
19    
20    // 拷贝构造函数
21    User(const User& other) : name(other.name), age(other.age) {
22        std::cout << "拷贝构造: " << name << std::endl;
23    }
24    
25    void display() const {
26        std::cout << name << ", " << age << "岁" << std::endl;
27    }
28    
29    ~User() {
30        std::cout << "析构: " << name << std::endl;
31    }
32};
33
34int main() {
35    std::vector<User> users;
36    
37    // 使用push_back添加元素(可能导致拷贝)
38    User alice("Alice", 25);
39    users.push_back(alice);  // 调用拷贝构造函数
40    
41    // 使用emplace_back直接构造(避免不必要的拷贝)
42    users.emplace_back("Bob", 30);  // 直接构造,无需临时对象
43    
44    // 使用push_back配合移动语义(C++11)
45    User charlie("Charlie", 35);
46    users.push_back(std::move(charlie));  // 调用移动构造函数
47    
48    // 显示所有用户
49    for (const auto& user : users) {
50        user.display();
51    }
52    
53    return 0;
54}

内存管理与性能优化

动态增长机制

vector的内存分配策略是影响性能的关键因素:

 1#include <iostream>
 2#include <vector>
 3
 4int main() {
 5    std::vector<int> vec;
 6    size_t prev_capacity = 0;
 7    
 8    // 观察capacity的变化
 9    std::cout << "大小\t容量\t变化倍数" << std::endl;
10    for (int i = 0; i < 100; ++i) {
11        vec.push_back(i);
12        
13        if (vec.capacity() != prev_capacity) {
14            double growth = prev_capacity > 0 
15                ? static_cast<double>(vec.capacity()) / prev_capacity 
16                : 0;
17                
18            std::cout << vec.size() << "\t" 
19                      << vec.capacity() << "\t" 
20                      << growth << std::endl;
21                      
22            prev_capacity = vec.capacity();
23        }
24    }
25    
26    return 0;
27}

提高性能的技巧

  1. 预分配空间:避免频繁扩容

    1std::vector<int> vec;
    2vec.reserve(1000);  // 预留1000个元素的空间
    
  2. 使用emplace_back代替push_back:减少不必要的拷贝

    1std::vector<std::pair<int, std::string>> data;
    2data.emplace_back(1, "one");  // 直接构造
    3data.push_back({2, "two"});   // 创建临时对象再拷贝或移动
    
  3. 移动语义与右值引用:减少拷贝开销

    1std::string str = "Hello, World!";
    2std::vector<std::string> vec;
    3vec.push_back(std::move(str));  // 移动而非拷贝
    
  4. 使用shrink_to_fit回收多余内存:在内存敏感场景中使用

    1vec.shrink_to_fit();  // 收缩capacity到size
    
  5. 批量操作:减少单元操作次数

    1// 一次性插入多个元素
    2std::vector<int> source = {1, 2, 3, 4, 5};
    3std::vector<int> dest;
    4dest.insert(dest.end(), source.begin(), source.end());
    

迭代器与陷阱

迭代器失效

迭代器失效是使用vector时常见的错误源:

 1std::vector<int> vec = {1, 2, 3, 4, 5};
 2
 3// 错误示例:迭代过程中修改容器
 4for (auto it = vec.begin(); it != vec.end(); ++it) {
 5    if (*it == 3) {
 6        vec.push_back(6);  // 可能导致迭代器失效
 7    }
 8}
 9
10// 正确示例:先收集需要修改的信息,然后再修改
11std::vector<int> toAdd;
12for (auto it = vec.begin(); it != vec.end(); ++it) {
13    if (*it == 3) {
14        toAdd.push_back(6);
15    }
16}
17for (int val : toAdd) {
18    vec.push_back(val);  // 安全地修改
19}

常见陷阱

  1. 迭代器失效的情况

    • 添加元素导致重新分配内存
    • 删除元素后继续使用被删除元素的迭代器
    • 对空容器使用front()、back()等函数
  2. 边界检查

    1// 危险:不检查边界
    2int val = vec[100];  // 越界访问不会抛出异常
    3
    4// 安全:检查边界
    5try {
    6    int val = vec.at(100);  // 越界会抛出std::out_of_range异常
    7} catch (const std::out_of_range& e) {
    8    std::cerr << "越界访问: " << e.what() << std::endl;
    9}
    
  3. 引用失效

    1std::vector<int> vec = {1, 2, 3};
    2int& ref = vec[0];  // 获取第一个元素的引用
    3vec.push_back(4);   // 可能导致重新分配内存
    4ref = 10;           // 危险:ref可能已经失效
    

高级应用

二维vector与多维容器

创建和操作多维数据结构:

 1// 创建5x4的二维vector,初始值为0
 2std::vector<std::vector<int>> matrix(5, std::vector<int>(4, 0));
 3
 4// 访问和修改元素
 5matrix[2][3] = 42;
 6
 7// 打印二维矩阵
 8for (const auto& row : matrix) {
 9    for (int val : row) {
10        std::cout << val << " ";
11    }
12    std::cout << std::endl;
13}
14
15// 不规则的二维vector
16std::vector<std::vector<int>> irregular;
17irregular.push_back({1, 2, 3});
18irregular.push_back({4, 5});
19irregular.push_back({6, 7, 8, 9});

配合算法库

 1#include <algorithm>
 2#include <numeric>
 3#include <vector>
 4#include <iostream>
 5#include <functional>  // C++11 函数对象
 6
 7int main() {
 8    std::vector<int> data = {5, 2, 8, 1, 9, 3, 7, 4, 6};
 9    
10    // 排序
11    std::sort(data.begin(), data.end());  // 升序排序
12    
13    // 使用lambda表达式(C++11)降序排序
14    std::sort(data.begin(), data.end(), [](int a, int b) {
15        return a > b;
16    });
17    
18    // 查找
19    auto it = std::find(data.begin(), data.end(), 7);
20    if (it != data.end()) {
21        std::cout << "找到7,位置: " << (it - data.begin()) << std::endl;
22    }
23    
24    // 计算总和
25    int sum = std::accumulate(data.begin(), data.end(), 0);
26    std::cout << "总和: " << sum << std::endl;
27    
28    // 查找最大/最小值
29    auto [min_it, max_it] = std::minmax_element(data.begin(), data.end());
30    std::cout << "最小值: " << *min_it << ", 最大值: " << *max_it << std::endl;
31    
32    // 转换元素
33    std::vector<int> squared(data.size());
34    std::transform(data.begin(), data.end(), squared.begin(),
35                   [](int x) { return x * x; });
36                   
37    // 删除满足条件的元素(C++20之前)
38    auto new_end = std::remove_if(data.begin(), data.end(),
39                                  [](int x) { return x % 2 == 0; });
40    data.erase(new_end, data.end());  // 删除偶数
41    
42    return 0;
43}

自定义分配器

高级用例可以使用自定义分配器:

 1#include <vector>
 2#include <memory>
 3#include <iostream>
 4
 5// 带跟踪功能的简单分配器
 6template <typename T>
 7class TrackingAllocator {
 8public:
 9    using value_type = T;
10    
11    TrackingAllocator() noexcept = default;
12    
13    template <typename U>
14    TrackingAllocator(const TrackingAllocator<U>&) noexcept {}
15    
16    T* allocate(std::size_t n) {
17        auto ptr = std::allocator<T>().allocate(n);
18        std::cout << "分配了 " << n << " 个T对象,地址: " << ptr << std::endl;
19        return ptr;
20    }
21    
22    void deallocate(T* p, std::size_t n) noexcept {
23        std::cout << "释放了 " << n << " 个T对象,地址: " << p << std::endl;
24        std::allocator<T>().deallocate(p, n);
25    }
26};
27
28int main() {
29    // 使用自定义分配器的vector
30    std::vector<int, TrackingAllocator<int>> vec;
31    vec.reserve(10);
32    
33    for (int i = 0; i < 15; ++i) {
34        vec.push_back(i);
35    }
36    
37    return 0;
38}

容器比较

vector与其他STL容器

以下表格比较了常见STL容器的特性:

特性 vector array list deque forward_list
内存布局 连续 连续 不连续 分段连续 不连续
随机访问 O(1) O(1) O(n) O(1) 不支持
头部插入 O(n) 不支持 O(1) O(1) O(1)
尾部插入 均摊O(1) 不支持 O(1) O(1) 不支持
中间插入 O(n) 不支持 O(1) O(n) O(1)
内存开销 极低
迭代器稳定性
适用场景 频繁读取、后部修改 固定大小集合 频繁插入删除 两端操作 单向链接,内存敏感

选择适当的容器

  • 选择vector的场景:

    • 需要随机访问元素
    • 主要在尾部添加/删除元素
    • 需要紧凑的内存布局
    • 元素数量变化不频繁
  • 考虑其他容器的场景:

    • 需要频繁在任意位置插入/删除元素:list
    • 需要在两端频繁添加/删除元素:deque
    • 元素数量固定:array或原生数组
    • 超大数据集、元素排序很重要:set/map

最佳实践与总结

有效使用vector的关键点

  1. 合理预分配容量:对于已知大小的集合,使用reserve()避免频繁重新分配
  2. 利用移动语义:使用C++11的移动语义减少不必要的拷贝
  3. 使用emplace系列函数:减少临时对象的创建和销毁
  4. 注意迭代器失效:在修改容器后更新迭代器
  5. 合理选择访问方式:需要安全性时使用at(),需要性能时使用operator[]
  6. 避免频繁的插入/删除操作:尤其是在容器前部和中部
  7. 使用标准算法:优先使用STL算法而不是自己编写循环
  8. 在性能关键场景进行基准测试:不同编译器和平台上vector的性能特性可能不同

总结

vector是C++中最通用、最强大的容器之一,它提供了动态数组的所有功能,同时保持了极高的性能和易用性。掌握vector的使用技巧和注意事项,可以显著提高C++程序的质量和性能。

关键记忆点:

  • 连续内存布局,支持随机访问
  • 在尾部添加元素通常很快(均摊O(1))
  • 在中间或头部插入/删除元素较慢(O(n))
  • 预分配容量可以提高性能
  • 注意迭代器失效问题
  • 使用C++11/14/17/20的新特性可以进一步提高效率

在大多数需要动态数组功能的场景中,vector应该是你的首选容器。

使用 Hugo 构建, 主题 StackJimmy 设计
本博客已稳定运行:, 共发表了10篇文章