【经典书籍】C++ Primer 第9章顺序容器精华讲解
《C++ Primer》第9章的标题通常是 “顺序容器(Sequential Containers)”,这是 C++ 标准库中非常核心的一部分内容。本章主要介绍了标准库提供的用于存储有序数据集合的容器类型,比如 vector
、list
、deque
等,并深入讲解了它们的特性、用法、选择策略以及相关的操作。
下面是对第9章的详细讲解与内容梳理。
🧩 第9章 顺序容器(Sequential Containers)—— 内容概览
一、什么是顺序容器?
顺序容器(Sequential Containers) 是一种用来存储一组对象的容器,这些对象在容器中按照它们被添加进去的顺序进行存储,也就是说,元素的逻辑顺序与它们在容器中的物理顺序是一致的。
顺序容器不提供基于键(key)的快速查找(那是关联容器的功能,比如 map
和 set
),而是更关注元素之间的顺序关系和高效的插入/删除操作。
C++ 标准库提供了几种常用的顺序容器:
容器 | 头文件 | 特点简述 |
---|---|---|
vector | <vector> | 动态数组,支持快速随机访问,尾部插入/删除高效,中间或头部插入/删除相对低效 |
deque | <deque> | 双端队列,支持首尾高效插入/删除,也支持随机访问 |
list | <list> | 双向链表,任意位置插入/删除都很快,但不支持随机访问 |
forward_list (C++11) | <forward_list> | 单向链表,更节省空间,只支持单向遍历,插入/删除快,但功能更有限 |
array | <array> | 固定大小的数组,不是动态的,但比 C 风格数组更安全、更易用(C++11 引入) |
注意:
array
虽然是容器,但它大小固定,不属于“动态”顺序容器,更多是替代原生数组的一种更安全的方案。
二、顺序容器概述与选择
1. 容器的共性操作
所有顺序容器都支持一些公共接口,比如:
-
添加元素:
push_back()
、push_front()
、insert()
-
删除元素:
pop_back()
、pop_front()
、erase()
、clear()
-
访问元素:
[]
、at()
、front()
、back()
-
获取大小:
size()
-
判断是否为空:
empty()
-
迭代器支持:
begin()
、end()
等,用于遍历
但不同容器对某些操作的效率支持不同,有些操作甚至可能不被支持(如 list
不支持 []
随机访问)。
2. 如何选择合适的顺序容器?
应用场景 | 推荐容器 | 原因 |
---|---|---|
需要随机访问,主要在尾部增删 | vector | 随机访问最快,尾部操作高效 |
需要随机访问,同时在头尾高效增删 | deque | 头尾插入/删除都很快,也支持随机访问 |
需要频繁在任意位置插入/删除 | list 或 forward_list | 插入/删除效率高,不涉及元素移动 |
内存紧凑、只需单向遍历且插入/删除多在某位置 | forward_list | 比 list 更省空间,但功能有限 |
元素数量固定,追求性能与安全 | array | 替代 C 数组,更安全,但不可动态调整大小 |
三、容器详解
1. vector
特点:
-
动态数组,内存通常连续分配
-
支持快速的随机访问(通过下标或迭代器)
-
尾部插入 / 删除效率高(
push_back
,pop_back
) -
中间或头部插入 / 删除效率较低,因为需要移动后续元素
常用操作:
std::vector<int> vec;
vec.push_back(10); // 尾部插入
vec.pop_back(); // 尾部删除
vec.size(); // 元素个数
vec.empty(); // 是否为空
vec[0]; // 访问第一个元素(不检查边界)
vec.at(0); // 访问第一个元素(会检查边界,越界抛异常)
vec.insert(vec.begin() + 1, 20); // 在指定位置插入
vec.erase(vec.begin()); // 删除第一个元素
2. deque
特点:
-
双端队列,支持头部和尾部高效的插入/删除
-
也支持随机访问(类似 vector)
-
内部通常由多个固定大小的块组成,不必连续存储
适用场景: 需要像 vector 一样随机访问,但同时又想在头部快速插入/删除
常用操作:
std::deque<int> dq;
dq.push_back(1); // 尾插
dq.push_front(2); // 头插
dq.pop_back(); // 尾删
dq.pop_front(); // 头删
3. list
特点:
-
双向链表,每个元素单独分配,元素之间通过指针连接
-
不支持随机访问(没有
operator[]
) -
任意位置的插入和删除都非常高效(只需修改指针)
-
相对于 vector 和 deque,占用内存稍多,缓存局部性较差
常用操作:
std::list<int> lst;
lst.push_back(10);
lst.push_front(20);
lst.pop_back();
lst.pop_front();
lst.insert(++lst.begin(), 15); // 在第二个位置前插入15
lst.erase(--lst.end()); // 删除最后一个元素
4. forward_list (C++11)
特点:
-
单向链表,只能从头到尾单向遍历
-
比
list
更节省内存,但功能受限 -
只支持前向插入和删除,不支持反向操作
适用场景: 对内存要求苛刻,只需要单向遍历和简单插入/删除
5. array (C++11)
特点:
-
固定大小的数组,编译期确定大小
-
比 C 风格数组更安全(比如支持赋值、有边界方法等)
-
不支持动态扩容,没有 push_back 等动态操作
常用操作:
std::array<int, 5> arr = {1, 2, 3, 4, 5};
arr[0] = 10;
int x = arr.at(1);
四、迭代器与遍历
所有顺序容器都支持迭代器,可以用来遍历容器中的元素:
for (auto it = vec.begin(); it != vec.end(); ++it) {std::cout << *it << " ";
}
或者使用更现代的 范围 for 循环(C++11):
for (int x : vec) {std::cout << x << " ";
}
注意:不是所有容器都支持所有类型的迭代器。例如:
vector
和deque
支持随机访问迭代器
list
和forward_list
只支持双向或前向迭代器,不支持随机访问(如 it + 3 不合法)
五、容器适配器(不属于顺序容器,但常被一起讨论)
虽然严格来说不是顺序容器,但 C++ 标准库还提供了几个基于顺序容器的容器适配器,如:
-
stack
(栈):默认基于deque
,也可基于vector
或list
-
queue
(队列):默认基于deque
-
priority_queue
(优先队列):默认基于vector
这些适配器提供了更高级的接口,隐藏了底层容器的细节,适合特定场景使用。
六、管理容器的注意事项
1. 容量与大小
-
size()
:当前容器中有多少元素 -
capacity()
(vector/deque):当前分配的内存能容纳多少元素(无需重新分配) -
reserve(n)
:提前分配至少能容纳 n 个元素的空间,避免多次扩容 -
resize(n)
:改变容器中元素的数量,可以增加或减少
2. 添加/删除元素的影响
-
在 vector 或 deque 中间插入/删除元素会导致其后所有元素的位置移动,开销较大
-
迭代器在插入/删除后可能会失效,特别是 vector,需要特别注意!
七、总结:如何选择顺序容器?
需求 | 推荐容器 |
---|---|
需要随机访问,主要在尾部操作 | vector ✅ 最常用 |
需要头尾都高效操作 + 随机访问 | deque |
需要频繁在任意位置插入/删除 | list 或 forward_list |
元素数量固定,追求安全与性能 | array |
只需单向遍历,极简内存 | forward_list |
📌 小贴士(来自 C++ Primer 的重要建议)
-
优先使用 vector,除非你有明确的理由选择其他容器。
-
避免在 vector 中间频繁插入/删除,考虑使用 list。
-
注意迭代器失效问题,特别是在 vector 中插入/删除后。
-
使用 range-based for 循环和 auto 关键字(C++11 起)让代码更简洁。
-
了解每种容器的优缺点及适用场合,不要盲目使用。
📘 补充:如果你想深入
-
第9章还会介绍:
-
如何初始化不同容器
-
容器间的赋值与拷贝
-
如何使用
emplace
操作(C++11,如emplace_back
,直接在容器内构造元素,避免临时对象) -
容器的性能比较与底层实现分析
-
✅ 总结
C++ Primer 第9章“顺序容器”是学习 C++ 标准库容器最重要的基础之一。掌握好 vector、list、deque 等容器的特性、用法和适用场景,能够帮助你在实际编程中做出合理的选择,写出更高效、更清晰的代码。
好的!我们接着对《C++ Primer》第9章 “顺序容器(Sequential Containers)” 进行更深入、更精细的讲解,从以下几个方面展开:
🔍 第9章 顺序容器 —— 深入详解
我们将从以下几个主题深入:
-
容器的初始化与赋值
-
添加与删除元素(重点:迭代器失效)
-
访问元素的方法与注意事项
-
容器的容量管理(size, capacity, reserve, shrink_to_fit)
-
emplace 操作(C++11 新特性,emplace_back / emplace_front / emplace)
-
迭代器失效问题(非常重要!)
-
forward_list 的特殊操作(因其单向性)
-
容器适配器简要回顾(stack, queue, priority_queue)
-
如何选择合适的容器(再次总结,结合实践)
1️⃣ 容器的初始化与赋值
所有顺序容器都支持多种初始化方式,常见如下:
基本初始化方式
// 默认初始化(空容器)
std::vector<int> v1;
std::list<std::string> l1;// 初始化指定数量的元素(值初始化)
std::vector<int> v2(10); // 10个元素,每个都是0
std::vector<int> v3(10, 42); // 10个元素,每个都是42// 列表初始化(C++11)
std::vector<std::string> v4{"hi", "hello", "world"};
std::list<int> l2{1, 2, 3, 4};// 通过迭代器范围初始化(比如从数组或其他容器中拷贝)
int arr[] = {1, 2, 3};
std::vector<int> v5(std::begin(arr), std::end(arr));
拷贝与赋值
std::vector<int> v6 = v5; // 拷贝构造
std::vector<int> v7;
v7 = v6; // 拷贝赋值
⚠️ 注意:容器间的赋值或拷贝是深拷贝,两个容器之后互不影响。
2️⃣ 添加与删除元素(重点:迭代器失效)
这是顺序容器最常用的操作,但也是最容易出错的地方,尤其是涉及到迭代器失效问题。
通用操作
尾部操作(大部分容器支持)
操作 | 含义 | 支持容器 |
---|---|---|
push_back(t) | 在尾部添加元素 t | vector, deque, list, forward_list(用 push_front) |
pop_back() | 删除尾部元素 | vector, deque, list(forward_list 无 pop_back) |
头部操作(部分容器支持)
操作 | 含义 | 支持容器 |
---|---|---|
push_front(t) | 在头部插入 t | deque, list, forward_list |
pop_front() | 删除头部元素 | deque, list, forward_list(vector 不支持) |
任意位置插入与删除
操作 | 说明 |
---|---|
insert(iter, value) | 在迭代器 iter 指向的位置之前插入 value |
erase(iter) | 删除迭代器 iter 指向的元素 |
erase(start, end) | 删除区间 [start, end) 中的元素 |
🔧 示例:
std::vector<int> vec = {1, 2, 3};
vec.push_back(4); // vec: {1, 2, 3, 4}
vec.insert(vec.begin() + 1, 10); // 在第1个位置前插入10 → {1, 10, 2, 3, 4}
vec.erase(vec.begin()); // 删除第一个元素 → {10, 2, 3, 4}
⚠️ 迭代器失效(Iterator Invalidation)——极其重要!
迭代器失效指的是:当容器中的元素发生变动(如插入、删除)后,原来获取的迭代器可能不再有效,再使用它将导致未定义行为(通常是程序崩溃)。
vector 和 deque:
-
在任何插入或删除操作后,所有指向被插入/删除位置之后元素的迭代器、指针和引用都可能失效!
-
特别是
push_back
如果引起重新分配内存(扩容),则所有迭代器、指针、引用全部失效!
✅ 建议:在对 vector 频繁在中间插入/删除时,考虑使用
list
;如果必须用 vector,注意不要保存过期的迭代器。
list 和 forward_list:
-
插入操作不会使任何迭代器失效(包括指向其他元素的迭代器)。
-
删除操作仅使指向被删除元素的迭代器失效,其他迭代器依旧有效。
3️⃣ 访问元素的方法
方法 | 说明 | 是否支持随机访问 |
---|---|---|
v[i] | 访问下标为 i 的元素(不检查边界) | ✅ vector, deque |
v.at(i) | 访问下标为 i 的元素(会检查边界,越界抛异常 std::out_of_range) | ✅ vector, deque |
v.front() | 返回第一个元素 | ✅ 所有容器(除 array 外都有) |
v.back() | 返回最后一个元素 | ✅ 除 forward_list |
*iter | 通过迭代器解引用访问元素 | ✅ 所有 |
⚠️ 注意:operator[]
和 at()
仅适用于支持随机访问的容器(如 vector、deque),list 不支持 [] 操作!
4️⃣ 容量管理
常用操作
函数 | 作用 | 适用容器 |
---|---|---|
size() | 当前元素个数 | 所有容器 |
empty() | 是否为空 | 所有容器 |
capacity() | 当前分配的存储空间能容纳多少元素(vector/deque) | vector, deque |
reserve(n) | 请求分配至少能容纳 n 个元素的空间(避免多次扩容) | vector, deque |
shrink_to_fit() (C++11) | 请求减少 capacity 以匹配 size(非强制) | vector, deque, string |
🔍 为什么需要 reserve
?
-
vector 在插入元素时如果超出当前 capacity,会触发重新分配内存 + 拷贝所有元素,代价很高。
-
提前调用
reserve(1000)
可以避免多次扩容,提升性能。
5️⃣ emplace 操作(C++11 重要特性)
传统插入如 insert
或 push_back
需要你先构造一个对象,然后将其拷贝或移动到容器中。
而 emplace
系列函数(如 emplace_back
)允许你直接在容器内部“就地构造”对象,省去了临时对象的构造和拷贝/移动过程。
对比:
// 传统方式:先构造对象,再拷贝到容器
std::vector<std::string> vec;
std::string s = "hello";
vec.push_back(s); // 调用了拷贝构造函数// emplace 方式:直接在容器中构造
vec.emplace_back("hello"); // 直接通过参数构造 string,无需临时对象
🔧 常见 emplace 方法:
-
emplace_back(args...)
→ 在尾部直接构造 -
emplace_front(args...)
-
emplace(iter, args...)
→ 在指定位置构造
✅ 推荐在 C++11 及以后尽量使用
emplace
系列,提高性能,特别是对象构造开销较大时。
6️⃣ forward_list 的特殊操作
由于 forward_list
是单向链表,它不支持反向遍历,也不支持随机访问。因此它提供了一些独特的插入/删除接口,比如:
std::forward_list<int> flist = {1, 2, 3};// 在某个元素之后插入
auto prev = flist.before_begin(); // 特殊迭代器,表示第一个元素之前的位置
flist.insert_after(prev, 0); // 在最前面插入0 → {0, 1, 2, 3}// 在指定位置后插入
auto it = flist.begin();
std::advance(it, 2); // 指向3
flist.insert_after(it, 99); // 在3后面插入99 → {0,1,2,3,99}// 删除某个元素之后的元素
flist.erase_after(flist.begin()); // 删除第一个元素之后的 → 删除1
✅ forward_list 没有
push_back
、insert
(不接受迭代器)、size()
成员函数等。
7️⃣ 容器适配器简要回顾(第9章也会提及)
虽然它们不是顺序容器,但常与顺序容器配合使用:
适配器 | 底层默认容器 | 特点 |
---|---|---|
stack | deque(可换成 vector / list) | 后进先出 LIFO |
queue | deque | 先进先出 FIFO |
priority_queue | vector(可换成 deque) | 优先级最高者先出(堆结构) |
这些适配器限制了接口,让你只能进行符合其逻辑的操作(比如 stack 只能 push / pop / top)。
8️⃣ 如何选择合适的容器?(最终建议总结)
需求 | 推荐容器 | 原因 |
---|---|---|
通用、需要随机访问,尾部操作多 | vector | 最常用,效率高 |
头尾都频繁操作 + 随机访问 | deque | 双端高效 + 支持下标访问 |
任意位置频繁插入/删除 | list | 双向链表,插入删除快 |
内存敏感,只需单向遍历 | forward_list | 更省内存,但功能少 |
元素数量固定 | array | 固定大小,安全高效 |
栈结构 | stack (基于 deque/vector/list) | 后进先出 |
队列结构 | queue (基于 deque) | 先进先出 |
优先级队列 | priority_queue (基于 vector) | 堆结构,自动排序 |
✅ 总结 Checklist(学完本章你应该掌握):
-
[x] 理解什么是顺序容器及其与关联容器的区别
-
[x] 熟悉 vector、deque、list、forward_list、array 的特点与适用场景
-
[x] 掌握容器的基本操作:增删改查、迭代器遍历
-
[x] 理解并避免迭代器失效问题(特别是 vector)
-
[x] 会使用
emplace_back
等高效构造方法 -
[x] 会管理容器容量:size, capacity, reserve
-
[x] 能根据需求合理选择容器类型
-
[x] 了解容器适配器(stack/queue/priority_queue)