vector学习总结

vector 学习总结

std::vector 是 C++ 标准库中最常用的顺序容器,本文从零到熟练梳理它的所有核心用法。

1. vector 是什么

  • 存储连续的元素序列,支持随机访问(按下标 O(1) 访问)。
  • 底层是动态数组:在堆上分配一块连续内存,元素紧密排列。
  • 自动扩容:元素增多时自动重新分配更大内存(O(n) 均摊插入)。
  • 属于"序列容器",头文件 <vector>。
#include <vector>

vector<int> v; // 空 vector,int 类型

对比记忆:vector 是"动态数组",内存连续、访问快、尾部增删快;list 是"双向链表",任意位置增删快但不能按下标访问;map 是"红黑树",按 key 有序存储键值对。

2. 定义与初始化

#include <vector>
#include <string>
using namespace std;

// 1. 空 vector
vector<int> v1;

// 2. 指定大小,元素值初始化为 0
vector<int> v2(5);          // 5 个元素,都是 0
vector<int> v3(5, 42);      // 5 个元素,都是 42

// 3. 初始化列表
vector<int> v4{1, 2, 3, 4, 5};

// 4. 从其他容器构造
string s = "hello";
vector<char> v5(s.begin(), s.end()); // {'h','e','l','l','o'}

// 5. 拷贝构造
vector<int> v6(v4);

// 6. 移动构造(C++11)
vector<int> v7(std::move(v4)); // v4 被掏空

// 7. resize / reserve
vector<int> v8;
v8.resize(10);   // size 变为 10,新元素默认初始化
v8.reserve(100); // capacity 变为 100,size 不变

关键概念:

  • size:当前元素个数
  • capacity:已分配内存能容纳的最大元素个数
  • reserve() 只改 capacity,不改 size;resize() 两者都改

3. 核心操作一览

操作 写法 说明 复杂度
尾部插入 v.push_back(x) 在末尾添加元素 O(1) 均摊
尾部插入 v.emplace_back(args...) C++11,原地构造,避免拷贝 O(1) 均摊
中间插入 v.insert(it, x) 在迭代器 it 前插入,后续元素后移 O(n)
尾部删除 v.pop_back() 删除最后一个元素 O(1)
中间删除 v.erase(it) 删除迭代器 it 处元素,返回下一个迭代器 O(n)
范围删除 v.erase(first, last) 删除 [first, last) 范围内元素 O(n)
按下标访问 v[i] 不检查越界 O(1)
按下标访问 v.at(i) 越界抛 out_of_range 异常 O(1)
首元素 v.front() 返回第一个元素的引用 O(1)
尾元素 v.back() 返回最后一个元素的引用 O(1)
指针 v.data() 返回底层数组指针 O(1)
大小 v.size() 当前元素个数 O(1)
容量 v.capacity() 当前分配的内存容量 O(1)
预留空间 v.reserve(n) 至少分配 n 个元素的空间 O(n) 最坏
调整大小 v.resize(n) 改变 size,多删少补 O(n) 最坏
收缩空间 v.shrink_to_fit() 使 capacity == size(非强制) O(n)
清空 v.clear() 删除所有元素,size=0,capacity 不变 O(n)
判空 v.empty() size == 0 时返回 true O(1)
交换 v.swap(other) 与另一个 vector 交换内容 O(1) O(1)

4. 增删查改实战

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    vector<int> v;

    // ===== 插入 =====
    v.push_back(10);
    v.push_back(20);
    v.push_back(30);
    v.emplace_back(40); // 原地构造

    // 在中间插入
    auto it = v.begin() + 2;
    v.insert(it, 25); // v: {10, 20, 25, 30, 40}

    // 插入多个相同元素
    v.insert(v.begin(), 3, 0); // v: {0, 0, 0, 10, 20, 25, 30, 40}

    // ===== 访问 =====
    cout << v[0] << endl;     // 0
    cout << v.at(4) << endl;  // 20
    cout << v.front() << endl; // 0
    cout << v.back() << endl;  // 40

    // ===== 查找(vector 没有内置 find,用 std::find) =====
    auto pos = find(v.begin(), v.end(), 25);
    if (pos != v.end()) {
        cout << "Found at index: " << (pos - v.begin()) << endl; // 6
    }

    // ===== 遍历 =====
    // 1. 下标遍历
    for (int i = 0; i < v.size(); i++) {
        cout << v[i] << " ";
    }
    cout << endl;

    // 2. 迭代器遍历
    for (auto it = v.begin(); it != v.end(); ++it) {
        cout << *it << " ";
    }
    cout << endl;

    // 3. 范围 for(C++11)
    for (auto& x : v) {
        cout << x << " ";
    }
    cout << endl;

    // ===== 删除 =====
    v.erase(v.begin());       // 删除首元素
    v.pop_back();             // 删除尾元素
    v.erase(v.begin(), v.begin() + 2); // 删除前两个
    v.clear();                // 清空所有元素
    cout << v.size() << endl; // 0

    return 0;
}

5. reserve() vs resize()(重点)

这是面试高频考点,两者本质不同:

vector<int> v;
v.reserve(10);
// size = 0, capacity = 10, 没有任何元素
// v[0] 是未定义行为!

v.resize(10);
// size = 10, capacity >= 10, 10 个值初始化为 0 的元素
cout << v[0]; // 合法,输出 0
reserve(n) resize(n)
改变 size 不改 改
改变 capacity 改 可能改
元素个数 不变 多删少补
用途 预分配内存,避免频繁扩容 改变元素个数

坑:reserve 后直接用下标访问未初始化的内存 → 未定义行为!

vector<int> v;
v.reserve(100);
for (int i = 0; i < 100; i++) {
    v[i] = i; // 未定义行为!size 还是 0
}
// 正确做法:
for (int i = 0; i < 100; i++) {
    v.push_back(i); // 每次 push_back 才会真正构造元素
}
// 或者先 resize
v.resize(100);
for (int i = 0; i < 100; i++) {
    v[i] = i; // 合法,元素已构造
}

6. 扩容机制:size 与 capacity 的关系

vector 扩容时会分配一块更大的内存,把旧元素拷贝过去,再释放旧内存:

size:     0  1  2  3  4  5  6  7  8 ...
capacity: 0  1  2  4  4  8  8  8  8 16 ...
  • 每次 push_back 导致 size > capacity 时触发扩容。
  • 扩容倍数由实现决定,通常是 2 倍(GCC)或 1.5 倍(MSVC)。
  • 扩容代价大:O(n) 拷贝 + 内存分配,所以批量插入前用 reserve() 预分配。
vector<int> v;
v.reserve(1000); // 预分配,后续 1000 次 push_back 不触发扩容
for (int i = 0; i < 1000; i++) {
    v.push_back(i); // 全部 O(1) 均摊,无重新分配
}

7. 迭代器的特点

  • vector 的迭代器是随机访问迭代器:支持 ++、--、it + n、it - n、it1 - it2、下标比较。
  • 这是最强大的迭代器类型,可以配合任何 STL 算法。

迭代器失效规则(重点):

操作 失效情况
push_back / emplace_back 可能导致全部迭代器失效(扩容时)
pop_back 尾迭代器失效
insert / erase 插入点及之后的所有迭代器失效
reserve / resize / shrink_to_fit 全部迭代器失效
clear 全部迭代器失效
// 危险:边遍历边 insert 可能死循环
for (auto it = v.begin(); it != v.end(); ++it) {
    if (*it == 5) {
        v.insert(it, 10); // it 失效!后续 ++it 未定义行为
    }
}

// 正确做法:insert 返回新的有效迭代器
for (auto it = v.begin(); it != v.end(); ) {
    if (*it == 5) {
        it = v.insert(it, 10); // 用返回值更新
        ++it;                   // 跳过新插入的元素
        ++it;                   // 跳过原元素
    } else {
        ++it;
    }
}

// erase 的正确用法:erase 返回下一个有效迭代器
for (auto it = v.begin(); it != v.end(); ) {
    if (*it == 5) {
        it = v.erase(it); // 返回下一个
    } else {
        ++it;
    }
}

8. vector 与 array、deque、list 的对比

vector array deque list
底层 动态数组 静态数组 分段数组 双向链表
内存连续 是 是 否(分段连续) 否
随机访问 O(1) O(1) O(1) 不支持
尾部增删 O(1) 均摊 N/A(固定大小) O(1) O(1)
头部增删 O(n) N/A O(1) O(1)
中间增删 O(n) N/A O(n) O(1)(已知位置)
扩容 自动 不可 两端增长 不需要
内存碎片 小 无 中等 大(每节点独立分配)

选择建议:

  • 默认选择 vector:大多数场景 vector 都是最快的(缓存友好)。
  • 频繁头部增删 → deque。
  • 频繁中间增删且不需要随机访问 → list。
  • 固定大小 → array(栈上分配,零开销)。

9. 用 vector 模拟栈和队列

#include <vector>
#include <stack>
#include <queue>
using namespace std;

// 用 vector 模拟栈(最常用)
vector<int> stk;
stk.push_back(1);    // 入栈
stk.push_back(2);
stk.pop_back();       // 出栈
int top = stk.back(); // 栈顶

// 用 vector 模拟队列(不推荐,O(n) dequeue)
vector<int> q;
q.push_back(1);           // 入队
q.erase(q.begin());       // 出队 O(n),慢
// 建议用 deque 或 queue 代替

10. 常见坑汇总

  1. reserve 后用下标访问:size 仍是 0,元素未构造,未定义行为。
  2. push_back 导致迭代器失效:扩容时所有迭代器失效,不能在循环中依赖旧迭代器。
  3. insert / erase 后使用旧迭代器:插入/删除点及之后的迭代器全部失效。
  4. 在循环中 push_back 可能无限扩容:循环前用 reserve 预分配。
  5. vector<bool> 不是真正的 vector:它是位压缩的特殊实现,operator[] 返回代理对象而非 bool&。如果需要位集合,用 bitset。
  6. pop_back 不返回被删元素:需要先 back() 再 pop_back()。
  7. clear 不释放内存:size 变 0 但 capacity 不变。想释放内存用 shrink_to_fit() 或 swap 技巧:
vector<int> v(1000000);
v.clear(); // size=0, capacity 还是 1000000,内存未释放

// 释放内存的技巧
vector<int>().swap(v); // 临时空 vector 和 v 交换,v 的内存被释放
// 或 C++20
v.shrink_to_fit();
  1. insert 在尾部插入和 push_back 效果一样但更慢:尾部插入优先用 push_back / emplace_back。
  2. 误以为 vector 是线程安全的:多线程读写同一个 vector 需要加锁。

11. emplace_back vs push_back(C++11)

vector<pair<int, string>> v;

// push_back:先构造临时对象,再移动/拷贝到 vector
v.push_back({1, "hello"}); // 构造 pair → 移动到 vector

// emplace_back:直接在 vector 的内存上构造,零拷贝
v.emplace_back(1, "hello"); // 直接用 (1, "hello") 构造 pair

经验法则:能用 emplace_back 就用 emplace_back,性能更好。

struct Expensive {
    string data;
    Expensive(const string& s) : data(s) {
        cout << "构造" << endl;
    }
    Expensive(const Expensive& other) : data(other.data) {
        cout << "拷贝" << endl;
    }
    Expensive(Expensive&& other) : data(std::move(other.data)) {
        cout << "移动" << endl;
    }
};

vector<Expensive> v;
v.push_back(Expensive("test")); // 构造 + 移动
v.emplace_back("test");          // 只构造,无拷贝无移动

12. 实战:动态数组管理(vector 最经典的场景)

#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
using namespace std;

int main() {
    // ===== 场景:成绩管理 =====
    vector<int> scores;

    // 批量录入
    scores.reserve(100); // 预分配
    for (int i = 0; i < 10; i++) {
        scores.push_back(60 + rand() % 41); // 60~100 随机分数
    }

    // 排序
    sort(scores.begin(), scores.end());

    // 统计
    double avg = accumulate(scores.begin(), scores.end(), 0.0) / scores.size();
    cout << "最高分: " << scores.back() << endl;
    cout << "最低分: " << scores.front() << endl;
    cout << "平均分: " << avg << endl;

    // 去重(先排序)
    scores.erase(unique(scores.begin(), scores.end()), scores.end());

    // 删除不及格的成绩
    scores.erase(
        remove_if(scores.begin(), scores.end(), [](int s) { return s < 60; }),
        scores.end()
    );

    // 遍历输出
    for (auto [i, s] : enumerate(scores)) { // C++23 enumerate
        cout << s << " ";
    }
    cout << endl;

    return 0;
}

13. 实战场景

给定一个需求:需要存储一组动态增长的数据,支持按下标快速访问,同时需要频繁在尾部增删。

方法一:使用std::vector建模,内存连续、缓存友好、按下标O(1)访问、尾部增删O(1)均摊。
方法二:使用std::deque建模,支持头部O(1)增删、尾部O(1)增删,但内存分段、缓存不如vector。
方法三:使用std::list建模,任意位置O(1)增删,但不支持随机访问、内存开销大。

总结:
1、默认选择 vector,连续内存缓存命中率最高,大多数场景最快
2、频繁头部增删选择 deque
3、频繁中间增删且不需要随机访问选择 list
4、固定大小选择 array(栈上分配,零开销)
5、批量插入前用 reserve 预分配内存,避免频繁扩容的拷贝开销
6、vector 的迭代器是随机访问迭代器,支持 it+n 这种跳跃操作,而 list 和 map 的迭代器只支持 ++/--
另外 vector 使用连续存储,遍历速度远快于 list(链表跳内存)和 map(红黑树跳内存)

posted @ 2026-09-08 23:48  LemHou  阅读(12)  评论(0)    收藏  举报