目 录CONTENT

文章目录

【STL】std::vector 详解

TalentQ
2026-05-08 / 0 评论 / 0 点赞 / 1 阅读 / 0 字

0 快问快答

学习的首要原则是避免感到枯燥。小问题敲击你的大脑神经。

Q1:std::vector 底层数据结构是什么?

A:连续内存的动态数组。支持随机访问,大小可动态变化。

Q2:size() 和 capacity() 的区别?

A:size() 是实际元素个数;capacity() 是已分配内存能容纳的元素总数(capacity >= size)。

Q3:push_back 的时间复杂度?可能发生什么操作?

A:均摊 O(1)。当 size == capacity 时,会重新分配内存(通常以 1.5~2 倍增长),拷贝/移动所有旧元素到新空间。

Q4:reserve(n) 和 resize(n) 区别?

A:reserve 只改变容量,不构造元素;resize 改变大小,若 n > size 则添加默认构造元素。

Q5:如何主动释放 vector 占用的多余内存?

A:shrink_to_fit()(仅请求,不保证)或通过交换技巧:vector<T>().swap(vec);

Q6:vector<bool> 有什么特别?

A:模板特化,采用位压缩存储,不满足标准容器要求(如无法取 bool&)。一般建议用 deque<bool> 或 vector<char> 替代。

Q7:哪些操作会使迭代器失效?

A:

扩容(push_back/insert 导致重分配):所有迭代器失效。

不扩容时:insert/erase 使插入/删除点之后的迭代器失效。

reserve/shrink_to_fit 可能失效。

swap 不影响迭代器(只要不交换容器本身)。

Q8:emplace_back 比 push_back 好在哪?

A:直接传递构造参数在容器内构造对象,避免临时对象拷贝/移动。对不可拷贝类型尤其有用。

Q9:vector 适合做队列或频繁头插/删吗?

A:不适合,头插/删是 O(n)。用 deque 或 list。

Q10:vector 与 array 的主要区别?

A:array 是固定大小栈内存;vector 动态堆内存。

Q11:vector 与 list 何时选谁?

A:需要随机访问、内存连续 → vector。频繁中间插入/删除、无需随机访问 → list。

Q12:vector 是否保证 C 风格的连续内存?

A:C++11 起保证 &v[0] 或 v.data() 返回连续数组,适用于 C 接口。

1 概述

std::vector 是 C++ 标准模板库(STL)中最常用的序列容器之一。它封装了动态数组,能够自动管理内存扩容,同时提供与原生数组接近的访问性能。vector 将元素存储在连续的内存块中,因此支持常数时间的随机访问,并且在末尾添加或删除元素具有较高的效率(均摊常数时间)。

要使用 std::vector,需要包含头文件:

#include <vector>

核心特点

  • 动态大小:运行时自动调整容量

  • 连续存储:支持指针偏移运算,兼容 C 风格数组接口

  • 类型安全:模板化,存储任意类型元素

  • 内存自动管理:析构时自动释放内存

时间复杂度

  • 随机访问:O(1)

  • 尾部插入/删除:均摊 O(1)

  • 中间插入/删除:O(n)

  • 查找:O(n)

2 构造与赋值

2.1 构造函数

vector 提供多种重载的构造函数,方便初始化。

构造函数

说明

vector()

默认构造,空容器

vector(size_type n)

构造包含 n 个值初始化元素的容器

vector(size_type n, const T& value)

构造包含 n 个 value 副本的容器

vector(InputIt first, InputIt last)

区间构造 [first, last) 的内容

vector(const vector& other)

拷贝构造

vector(vector&& other)

移动构造(C++11)

vector(std::initializer_list<T> init)

初始化列表构造(C++11)

示例

#include <vector>
#include <iostream>

int main() {
    std::vector<int> v1;                     // 空容器
    std::vector<int> v2(5);                  // {0,0,0,0,0}
    std::vector<int> v3(5, 10);              // {10,10,10,10,10}
    std::vector<int> v4(v3.begin(), v3.end());// 拷贝区间
    std::vector<int> v5(v4);                 // 拷贝构造
    std::vector<int> v6(std::move(v5));      // 移动构造后 v5 为空
    std::vector<int> v7 = {1,2,3,4,5};       // 初始化列表

    for (int x : v7) std::cout << x << ' ';  // 1 2 3 4 5
    std::cout << "\nSize of v5: " << v5.size() << '\n'; // 0
    return 0;
}

2.2 赋值操作

  • operator=:拷贝、移动或初始化列表赋值

  • assign():重新设置容器内容(可指定区间或重复值)

std::vector<int> v = {1,2,3};
v.assign(4, 100);           // v = {100,100,100,100}
v.assign({5,6,7});          // v = {5,6,7}

std::vector<int> w;
w = v;                      // 拷贝赋值
w = std::move(v);           // 移动赋值
w = {8,9};                  // 初始化列表赋值

3 元素访问

vector 提供多种方式访问元素,需注意边界检查。

成员函数

行为

at(size_type i)

带边界检查的访问,越界抛出 std::out_of_range

operator[](size_type i)

无边界检查,越界为未定义行为

front()

首元素引用

back()

尾元素引用

data()

返回底层数组的指针(C++11)

std::vector<char> vec = {'a','b','c'};
std::cout << vec[0];            // 'a'
std::cout << vec.at(1);         // 'b'
std::cout << vec.front();       // 'a'
std::cout << vec.back();        // 'c'

char* ptr = vec.data();         // 指向 'a'
ptr[1] = 'x';                   // vec 变为 {'a','x','c'}

⚠️ 注意:对于空容器调用 front()/back() 是未定义行为;operator[] 需确保索引有效。

4 迭代器

vector 提供的迭代器属于随机访问迭代器,支持 +-[]< 等运算。

成员函数

说明

begin() / end()

普通迭代器,指向首/尾后位置

cbegin() / cend()

常量迭代器(C++11)

rbegin() / rend()

反向迭代器

crbegin() / crend()

常量反向迭代器(C++11)

std::vector<int> v = {10,20,30};
for (auto it = v.begin(); it != v.end(); ++it) {
    *it += 5;
}
// v 变为 {15,25,35}

for (auto it = v.cbegin(); it != v.cend(); ++it) {
    // *it = 0;  错误:常量迭代器不可修改
}

for (auto rit = v.rbegin(); rit != v.rend(); ++rit) {
    std::cout << *rit << ' ';   // 35 25 15
}

由于 vector 的内存连续,可以通过 &v[0] + i 获得迭代器效果,但推荐使用迭代器以保持泛型兼容性。

5 容量管理

理解 sizecapacity 的区别是高效使用 vector 的关键。

  • size():当前元素个数

  • capacity():当前已分配内存所能容纳的元素个数(capacity() >= size()

  • empty():检查是否为空

  • max_size():理论上可容纳的最大元素数(通常很大)

5.1 扩容机制

当插入新元素导致 size == capacity 时,vector 会分配一块更大的新内存(通常为原容量的 1.5 倍或 2 倍,取决于实现),将原有元素移动或复制过去,最后释放旧内存。这个操作称为 扩容(reallocation),开销较大。

为了减少扩容次数,可以使用 reserve() 预分配容量。

std::vector<int> v;
std::cout << "Size: " << v.size() << ", Cap: " << v.capacity() << '\n'; // 0,0

v.push_back(1);
std::cout << v.capacity(); // 常见实现结果为 1

v.push_back(2);            // 触发扩容,capacity 变为 2 
v.push_back(3);            // 触发扩容,capacity 变为 4 
// 继续插入...

5.2 预留与缩容

成员函数

作用

reserve(size_type n)

预分配至少能容纳 n 个元素的内存,若 n <= capacity() 则无效果

shrink_to_fit()

请求移除多余容量(C++11),不保证一定缩容

std::vector<int> v;
v.reserve(100);            // capacity >= 100
for (int i = 0; i < 100; ++i) v.push_back(i); // 无扩容发生

v.clear();                 // size = 0, capacity 不变
v.shrink_to_fit();         // 实现可能释放内存,capacity 可能变为 0

在已知元素个数的场景下,务必使用 reserve() 避免多次扩容,显著提升性能。

6 修改操作

6.1 尾部操作

  • push_back(const T& value) / push_back(T&& value)(C++11)

  • pop_back() – 删除末尾元素(不返回该元素)

  • emplace_back(Args&&... args)(C++11)– 就地构造元素,避免拷贝/移动

struct Point { int x, y; Point(int a, int b) : x(a), y(b) {} };

std::vector<Point> points;
points.emplace_back(3, 4);   // 直接调用 Point(3,4),无临时对象
points.push_back(Point(1,2));// 先构造临时,再移动(或拷贝)到容器

对于复杂类型,emplace_back 通常比 push_back 更高效。

6.2 任意位置插入与删除

  • insert(iterator pos, const T& value) / (重载版本)

  • erase(iterator pos) / erase(iterator first, iterator last)

  • clear() – 清空所有元素(size = 0)

  • emplace(iterator pos, Args&&... args) – 就地构造于指定位置前

std::vector<int> v = {1,2,3,4,5};
v.insert(v.begin() + 2, 99);      // {1,2,99,3,4,5}
v.erase(v.begin() + 1, v.begin() + 3); // 删除索引 1~2 → {1,4,5}
v.emplace(v.begin(), -1);         // {-1,1,4,5}

⚠️ 注意:中间插入/删除会导致该位置之后的所有元素移动,时间复杂度 O(n)。

6.3 其他修改

  • resize(size_type n) / resize(size_type n, const T& value)

    • n < size():截断尾部

    • n > size():尾部添加值初始化(或 value 副本)的元素

  • swap(vector& other) – 交换两个容器的内容(常数时间,仅交换内部指针)

std::vector<int> a = {1,2,3};
std::vector<int> b = {4,5};
a.swap(b);              // a = {4,5}, b = {1,2,3}

7. 迭代器失效问题

vector 的某些修改操作可能导致已有迭代器、指针或引用失效。理解失效规则是避免悬垂引用的关键。

操作

失效情况

reserve / resize(导致扩容)

所有迭代器、指针、引用失效

push_back / emplace_back(未扩容)

end() 失效,其他迭代器有效

push_back / emplace_back(扩容)

全部失效

insert(未扩容,插入位置之前)

insert 位置及之后的迭代器失效

insert(扩容)

全部失效

erase

被删除点及之后的迭代器失效

pop_back

指向被删元素的迭代器失效,end() 失效

clear

所有迭代器失效

shrink_to_fit

如果重新分配,所有迭代器失效

示例常见错误

std::vector<int> v = {1,2,3};
for (auto it = v.begin(); it != v.end(); ++it) {
    if (*it == 2) v.insert(it, 99);   // 危险!insert 会使 it 失效
}

正确做法:利用插入返回的新迭代器或者改变遍历逻辑。

std::vector<int> v = {1,2,3};
for (auto it = v.begin(); it != v.end(); ) {
    if (*it == 2) it = v.insert(it, 99) + 1; // insert 返回指向 99 的迭代器,+1 后指向原来的 2
    else ++it;
}

8 vector<bool> 特化

std::vector<bool>vector 的一个模板特化,为了节省空间,它用位(bit)存储布尔值,而不是字节。这导致一些与普通 vector 不同的行为:

  • 无法直接取 bool& 引用(因为位不可寻址),operator[] 返回一个代理类 std::vector<bool>::reference

  • 迭代器、data() 成员不可用(没有连续的内存数组)

  • 性能可能不及普通 vector<char>

  • 在多线程环境下需特别注意

如果需要 bool 的动态数组且不在乎空间占用,推荐使用 std::vector<char> 以获得标准容器行为

std::vector<bool> flags(10, true);
flags[0] = false;           // 正常工作,但引用是代理
// bool* p = flags.data();  // 错误:没有 data()

9 性能分析与使用建议

9.1 扩容策略

大多数标准库实现采用 1.5 倍2 倍 扩容因子。例如 GCC 使用 2 倍,而 Clang 使用 1.5 倍(更利于重用已释放的内存碎片)。假设初始 capacity=0,经过 k 次扩容后的容量约为 initial * factor^k

测试代码(GCC):

std::vector<int> v;
for (int i = 0; i < 100; ++i) {
    v.push_back(i);
    std::cout << "size=" << v.size() << " cap=" << v.capacity() << '\n';
}

输出典型序列:0→1→2→4→8→16→32→64→128...

9.2 高效使用指南

  • 预分配内存:已知元素数量时,使用 reserve(n)

  • 避免频繁的中间插入:优先考虑 dequelist

  • 移动语义:在存储大型对象时,使用 emplace_backpush_back(T&&)

  • 使用 shrink_to_fit:在长期不再增长后释放多余内存(但注意它可能触发拷贝)

  • 使用 swap 技巧清空内存:旧标准中常用 vector<T>().swap(v); 来强制释放内存,现在 shrink_to_fit 更清晰

// 强制释放内存(C++11 以前风格)
std::vector<int>().swap(v);
// C++11 及以后
v.clear();
v.shrink_to_fit();

9.3 与其他序列容器的对比

容器

随机访问

中间插入/删除

前端操作

内存使用

vector

O(1)

O(n)

O(n)

连续,有预留容量

deque

O(1)

O(n) (稍快于 vector)

O(1)

分段连续,开销略大

list

O(n)

O(1) (找到位置后)

O(1)

每个元素额外指针开销

array

O(1)

不可

不可

连续,无动态分配

选择建议

  • 默认使用 vector,除非有明确的特殊需求

  • 大量头部插入/删除 → deque

  • 频繁中间插入且不关心随机访问 → list / forward_list

  • 编译期固定大小 → std::array

10 异常安全性

vector 操作提供基本的异常安全保证(某些操作提供强保证):

  • push_back / emplace_back:如果元素构造或内存分配抛出异常,容器保持原状(强保证)

  • insert / emplace:通常提供基本保证,也可能强保证(取决于元素类型和位置)

  • pop_back / clear:不抛出异常(除非元素析构抛出,但不应发生)

当元素类型是不可复制的移动类型时,vector 会在扩容时使用移动而非复制以提升性能。但为了强异常安全,标准库要求移动操作声明为 noexcept,否则将回退到复制。

class Noisy {
public:
    Noisy(Noisy&&) noexcept { ... }  // 帮助 vector 高效扩容
};


0

评论区