C++ Primer Plus 重读精讲 | STL容器基础:vector、list、deque
·

全文连载前置回顾
- 1-6篇:开发环境、基础数据类型、运算符全集
- 7-10篇:分支结构、三大循环、数组、字符串(字符数组)
- 11-12篇:指针基础与进阶、动态内存分配
- 13-14篇:基础输入输出、结构体
- 15-16篇:函数基础与进阶
- 17篇:动态内存管理
- 18-26篇:名称空间、类与对象、继承、多态、运算符重载、面向对象设计
- 27-29篇:异常处理、文件IO、string类与stringstream
第17篇(动态内存管理)我们学会了用new手动分配数组,但每次都要自己管理大小、扩容、释放。STL(Standard Template Library——标准模板库)把这些工作自动化了:它提供了一套模板化的容器类,用泛型编程的方式管理任意类型的数据,内部自动处理内存分配和释放。
前言
什么是STL? STL是C++标准库的核心组件之一,包含四大部分:
- 容器(Container):存储数据的结构(vector、list、map、set等)
- 算法(Algorithm):操作容器数据的通用算法(sort、find、copy等)
- 迭代器(Iterator):连接容器和算法的桥梁(像指针一样遍历容器)
- 函数对象/适配器:灵活扩展算法行为
STL的核心设计理念是模板化 + 泛型编程:写一次模板代码,可以用在任意类型上(如 vector<int> 存int、vector<string> 存字符串、vector<Device> 存自定义类)。
三大序列容器:
- vector(向量/动态数组):连续内存,支持O(1)随机访问,尾部插入O(1),中间插入O(n)
- list(双向链表):节点不连续,任何位置插入/删除都是O(1),但随机访问需要O(n)
- deque(双端队列):分段连续内存,头尾插入/删除都是O(1),支持O(1)随机访问
一、vector:最常用的动态数组
vector在内部维护一块连续内存,像数组一样用下标访问,但可以自动扩容。
#include <iostream>
#include <vector> // vector头文件
#include <string>
using namespace std;
struct SensorData
{
int id;
string name;
double temperature;
SensorData(int i = 0, string n = "", double t = 0)
: id(i), name(n), temperature(t) {}
};
int main()
{
// ===== 1. 初始化 =====
cout << "=== 1. 初始化 ===" << endl;
vector<int> v1; // 空vector,存int
vector<int> v2(10, 0); // 10个0
vector<int> v3 = {1, 2, 3, 4, 5}; // 初始化列表
vector<string> vs(3, "Hello"); // 3个"Hello"字符串
cout << "v1大小: " << v1.size() << endl;
cout << "v3内容: ";
for (size_t i = 0; i < v3.size(); i++)
cout << v3[i] << " ";
cout << endl;
// ===== 2. 尾部插入:push_back =====
cout << "\n=== 2. push_back尾部插入 ===" << endl;
vector<SensorData> sensors;
sensors.push_back(SensorData(1001, "加热炉A", 78.5));
sensors.push_back(SensorData(1002, "冷却塔B", 28.3));
sensors.push_back(SensorData(2001, "反应釜C", 125.8));
cout << "插入后大小: " << sensors.size() << endl;
// ===== 3. 访问元素 =====
cout << "\n=== 3. 访问元素 ===" << endl;
cout << "sensors[0]: ID=" << sensors[0].id
<< " " << sensors[0].name
<< " T=" << sensors[0].temperature << "度" << endl;
// at()是安全访问(越界抛异常)
try
{
cout << "sensors.at(10): " << sensors.at(10).name << endl;
}
catch (const out_of_range& e)
{
cout << "at(10)越界: " << e.what() << endl;
}
// front()/back()访问首尾
cout << "第一个: " << sensors.front().name << endl;
cout << "最后一个: " << sensors.back().name << endl;
// ===== 4. 遍历 =====
cout << "\n=== 4. 遍历vector ===" << endl;
cout << "方式1: 用索引遍历" << endl;
for (size_t i = 0; i < sensors.size(); i++)
{
cout << " [" << i << "] ID=" << sensors[i].id
<< " " << sensors[i].name
<< " T=" << sensors[i].temperature << "度" << endl;
}
cout << "方式2: 用迭代器遍历(后续详解,这里先展示)" << endl;
for (vector<SensorData>::iterator it = sensors.begin();
it != sensors.end();
++it)
{
cout << " ID=" << it->id << " " << it->name << endl;
}
// ===== 5. 删除元素 =====
cout << "\n=== 5. 删除元素 ===" << endl;
sensors.pop_back(); // 删除最后一个元素
cout << "pop_back后大小: " << sensors.size() << endl;
// 清空所有元素
sensors.clear();
cout << "clear后大小: " << sensors.size()
<< " 是否为空: " << (sensors.empty() ? "是" : "否") << endl;
// ===== 6. 容量与预留 =====
cout << "\n=== 6. 容量管理 ===" << endl;
vector<int> data;
cout << "初始: size=" << data.size() << " capacity=" << data.capacity() << endl;
data.reserve(1000); // 预留1000个元素的空间,避免多次重新分配
cout << "reserve(1000)后: size=" << data.size()
<< " capacity=" << data.capacity() << endl;
// 插入大量数据(因为预留了空间,不会触发重新分配)
for (int i = 0; i < 500; i++) data.push_back(i);
cout << "插入500个后: size=" << data.size()
<< " capacity=" << data.capacity() << endl;
// resize改变元素个数(增大会构造新元素,减小会销毁多余元素)
data.resize(100);
cout << "resize(100)后: size=" << data.size()
<< " capacity=" << data.capacity() << endl;
// ===== 7. 嵌套vector =====
cout << "\n=== 7. 嵌套vector(二维数组) ===" << endl;
vector<vector<int>> matrix(3, vector<int>(4, 0)); // 3x4矩阵,全0
matrix[0][0] = 1;
matrix[1][2] = 5;
matrix[2][3] = 9;
for (size_t row = 0; row < matrix.size(); row++)
{
for (size_t col = 0; col < matrix[row].size(); col++)
{
cout << matrix[row][col] << " ";
}
cout << endl;
}
return 0;
}
vector的内存模型:
- size:当前有多少个元素
- capacity:实际分配的内存可以容纳多少个元素(>=size)
- push_back时如果size达到capacity,会重新分配一块更大的内存(通常2倍),把旧数据拷贝过去,释放旧内存——这是vector偶尔"卡"的原因
- reserve(n):手动预留容量,减少重新分配
- resize(n):改变元素个数(实际大小)
二、list:双向链表
list由节点组成,每个节点包含数据和两个指针(指向前一个和后一个节点)。
#include <iostream>
#include <list> // list头文件
#include <string>
using namespace std;
struct Task
{
int id;
string name;
int priority;
Task(int i, string n, int p) : id(i), name(n), priority(p) {}
};
int main()
{
list<Task> tasks;
// 1. 插入元素(push_back/push_front都很快,O(1))
tasks.push_back(Task(1, "数据采集", 2));
tasks.push_back(Task(2, "数据处理", 1));
tasks.push_back(Task(3, "数据存储", 3));
tasks.push_front(Task(0, "初始化", 1)); // 前端插入
cout << "任务列表(共" << tasks.size() << "个):" << endl;
for (list<Task>::iterator it = tasks.begin(); it != tasks.end(); ++it)
{
cout << " ID=" << it->id << " [" << it->priority << "] " << it->name << endl;
}
// 2. 访问首尾元素
cout << "\n第一个任务: " << tasks.front().name << endl;
cout << "最后一个任务: " << tasks.back().name << endl;
// 3. list不支持[]随机访问(因为节点不连续)
// tasks[1] 编译错误!
// 4. 插入/删除任意位置(找到迭代器后都是O(1))
list<Task>::iterator it = tasks.begin();
++it; // 跳到第二个元素
tasks.insert(it, Task(99, "紧急任务", 0)); // 在第二个位置插入
cout << "\n插入紧急任务后:" << endl;
for (list<Task>::iterator it2 = tasks.begin(); it2 != tasks.end(); ++it2)
cout << " " << it2->name << " (优先级" << it2->priority << ")" << endl;
// 5. 删除元素
tasks.pop_front(); // 删除第一个
tasks.pop_back(); // 删除最后一个
cout << "\n删除首尾后剩 " << tasks.size() << " 个任务" << endl;
// 6. list特有操作:合并/排序
list<int> list1, list2;
list1.push_back(1); list1.push_back(3); list1.push_back(5);
list2.push_back(2); list2.push_back(4); list2.push_back(6);
list1.merge(list2); // 合并两个有序list(list2变为空)
cout << "\n合并后:";
for (list<int>::iterator it3 = list1.begin(); it3 != list1.end(); ++it3)
cout << " " << *it3;
cout << endl;
// 7. 反转
list1.reverse();
cout << "反转后:";
for (list<int>::iterator it4 = list1.begin(); it4 != list1.end(); ++it4)
cout << " " << *it4;
cout << endl;
return 0;
}
三、deque:双端队列
deque(Double-Ended QUEue)是vector和list的折中方案:分段连续内存,支持随机访问,头尾插入都很快。
#include <iostream>
#include <deque>
#include <string>
using namespace std;
int main()
{
deque<string> msgQueue; // 消息队列
// 1. 首尾都可高效插入
msgQueue.push_back("消息1: 正常数据"); // 后端插入
msgQueue.push_back("消息2: 正常数据");
msgQueue.push_front("优先级消息: 紧急报警"); // 前端插入(高优先级)
msgQueue.push_front("优先级消息: 系统重启");
// 2. 支持随机访问(像vector一样用[])
cout << "队列大小: " << msgQueue.size() << endl;
for (size_t i = 0; i < msgQueue.size(); i++)
cout << " [" << i << "] " << msgQueue[i] << endl;
// 3. 首尾都可删除
cout << "\n取出第一个: " << msgQueue.front() << endl;
msgQueue.pop_front(); // 取出后删除
cout << "取出最后一个: " << msgQueue.back() << endl;
msgQueue.pop_back();
cout << "剩余 " << msgQueue.size() << " 条:" << endl;
for (size_t i = 0; i < msgQueue.size(); i++)
cout << " " << msgQueue[i] << endl;
return 0;
}
四、容器对比与选择
| 特性 | vector | deque | list |
|---|---|---|---|
| 内存模型 | 连续内存 | 分段连续 | 节点+指针 |
| 随机访问([]) | O(1) | O(1) | 不支持(O(n)) |
| 尾部插入/删除 | O(1)(平均) | O(1) | O(1) |
| 前端插入/删除 | O(n)(很慢) | O(1) | O(1) |
| 中间插入/删除 | O(n) | O(n) | O(1)(已知迭代器) |
| 遍历速度 | 最快(CPU缓存友好) | 快 | 较慢(指针跳转,缓存不命中) |
| 元素迭代器失效 | 插入/删除后可能全部失效 | 仅被删除元素失效 | 仅被删除元素失效 |
选择建议:
- 大多数场景用vector——默认首选,随机访问快,CPU缓存命中率高
- 需要两端都频繁插入/删除(如队列、任务池)→ deque
- 需要中间频繁插入/删除且不需要随机访问→ list
- 数据量很小(几十个元素)时差异不明显,按习惯选即可
五、工业场景实战:设备数据用vector管理
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <string>
#include <cstdlib>
#include <ctime>
#include <cstring>
using namespace std;
struct DeviceReading
{
int deviceId;
string name;
double temperature;
double pressure;
int timestamp;
};
// 生成模拟数据
vector<DeviceReading> generateReadings(int count)
{
vector<DeviceReading> readings;
readings.reserve(count); // 预留容量,避免多次重新分配
string names[] = {"加热炉A", "加热炉B", "冷却塔", "反应釜", "阀门控制器"};
for (int i = 0; i < count; i++)
{
DeviceReading r;
r.deviceId = 1000 + (i % 5) * 10;
r.name = names[i % 5];
r.temperature = 25.0 + (rand() % 1000) / 10.0;
r.pressure = 0.5 + (rand() % 150) / 100.0;
r.timestamp = time(0) + i;
readings.push_back(r);
}
return readings;
}
// 统计:返回报警记录数
int analyzeAndReport(const vector<DeviceReading>& data)
{
int alertCount = 0;
double sumT = 0, sumP = 0;
double maxT = 0;
string maxTDevice;
// const引用遍历(只读,避免拷贝)
for (size_t i = 0; i < data.size(); i++)
{
const DeviceReading& r = data[i]; // 引用,不拷贝
sumT += r.temperature;
sumP += r.pressure;
if (r.temperature > maxT)
{
maxT = r.temperature;
maxTDevice = r.name;
}
if (r.temperature > 80.0 || r.pressure > 1.5)
{
alertCount++;
}
}
cout << "\n=== 统计报告 ===" << endl;
cout << "总记录数: " << data.size() << endl;
cout << "平均温度: " << sumT / data.size() << "度" << endl;
cout << "平均压力: " << sumP / data.size() << "MPa" << endl;
cout << "最高温度: " << maxT << "度 (" << maxTDevice << ")" << endl;
cout << "报警记录: " << alertCount << "条" << endl;
return alertCount;
}
// 过滤:返回温度高于阈值的记录
vector<DeviceReading> filterHighTemp(const vector<DeviceReading>& data, double threshold)
{
vector<DeviceReading> result;
for (size_t i = 0; i < data.size(); i++)
{
if (data[i].temperature > threshold)
{
result.push_back(data[i]);
}
}
return result;
}
// 导出到CSV
bool exportToCSV(const vector<DeviceReading>& data, const string& filename)
{
ofstream out(filename.c_str());
if (!out.is_open()) return false;
out << "DeviceID,Name,Temperature,Pressure,Timestamp" << endl;
for (size_t i = 0; i < data.size(); i++)
{
const DeviceReading& r = data[i];
out << r.deviceId << "," << r.name << ","
<< r.temperature << "," << r.pressure << ","
<< r.timestamp << endl;
}
return true;
}
int main()
{
srand(time(0));
// 1. 生成数据
cout << "=== 生成模拟数据 ===" << endl;
vector<DeviceReading> data = generateReadings(100);
cout << "已生成 " << data.size() << " 条记录" << endl;
// 2. 分析全部数据
analyzeAndReport(data);
// 3. 过滤高温记录
cout << "\n=== 过滤温度>80度的记录 ===" << endl;
vector<DeviceReading> highTemp = filterHighTemp(data, 80.0);
cout << "共有 " << highTemp.size() << " 条高温记录" << endl;
for (size_t i = 0; i < highTemp.size() && i < 10; i++)
{
cout << " ID=" << highTemp[i].deviceId
<< " " << highTemp[i].name
<< " T=" << highTemp[i].temperature << "度" << endl;
}
// 4. 导出
cout << "\n=== 导出数据 ===" << endl;
if (exportToCSV(data, "readings.csv"))
cout << "已导出到 readings.csv" << endl;
// 5. vector的一些其他操作
cout << "\n=== vector进阶操作 ===" << endl;
cout << "当前容量: " << data.capacity() << endl;
// 在第5个位置插入一条(比较慢,O(n))
data.insert(data.begin() + 5, DeviceReading{9999, "手动插入", 99.9, 2.0, time(0)});
cout << "insert后大小: " << data.size() << endl;
// 删除第3条
data.erase(data.begin() + 2);
cout << "erase后大小: " << data.size() << endl;
// 缩小容量(把空着的内存释放掉,C++11)
data.shrink_to_fit();
cout << "shrink_to_fit后容量: " << data.capacity() << endl;
return 0;
}
六、独家C#语法对照
| 对比维度 | C++ | C# | 工业开发差异 |
|---|---|---|---|
| 动态数组 | std::vector<T> |
List<T> |
两者核心特性几乎一致 |
| 声明方式 | vector<int> v(10); |
List<int> v = new List<int>(10); |
C#需要显式new |
| 添加元素 | v.push_back(x); |
v.Add(x); |
API类似 |
| 元素访问 | v[i] 或 v.at(i) |
v[i](越界抛异常) |
C#默认行为类似at() |
| 容量管理 | v.capacity()/v.reserve(n)/v.resize(n) |
v.Capacity/v.Capacity=n/v.Count |
类似,C#属性代替方法 |
| 链表 | std::list<T> |
LinkedList<T> |
API差异较大,C#ListNode独立操作 |
| 双端队列 | std::deque<T> |
C#无直接对应,可用LinkedList<T>模拟 |
C#原生没有双端队列类型 |
| 元素类型 | 任意类型(模板实例化) | 任意类型(泛型) | 泛型概念类似,实现机制不同 |
七、重读专属:STL容器六大常见错误
- 错误1:迭代器失效后继续使用 — vector插入/删除后,旧迭代器可能指向已释放的内存。操作后应重新获取迭代器
- 错误2:用int接收size()返回值 — size()返回
size_t(无符号整数)。for (int i = 0; i < v.size(); i++)没问题,但v.size() - 1在空容器时是极大值。最好用size_t做索引 - 错误3:在循环中用
[]越界访问 —vector[i]越界不抛异常(类似C数组),是未定义行为。想安全访问用at(i)或自己判断边界 - 错误4:频繁在vector中间插入/删除 — 这是O(n)操作,大量元素时非常慢。如果频繁在中间操作,应考虑用list(但list不支持随机访问)
- 错误5:传vector到函数不用引用 —
void func(vector<int> v)会拷贝整个vector,效率极低。应该用void func(const vector<int>& v)(只读)或void func(vector<int>& v)(需修改) - 错误6:在循环中push_back导致迭代器失效 —
for (auto it = v.begin(); it != v.end(); ++it) { v.push_back(x); }第一次push_back后it可能失效,导致无限循环或崩溃。循环修改容器要特别小心
八、原书课后习题解析
习题:读取一行整数,用vector存储并反转输出
#include <iostream>
#include <vector>
#include <sstream>
#include <string>
using namespace std;
int main()
{
cout << "请输入一行整数(空格分隔): ";
string line;
getline(cin, line);
// 用stringstream解析
istringstream iss(line);
vector<int> numbers;
int num;
while (iss >> num) // 逐个读入整数
numbers.push_back(num);
cout << "共读取 " << numbers.size() << " 个整数" << endl;
// 正向输出
cout << "正向: ";
for (size_t i = 0; i < numbers.size(); i++)
cout << numbers[i] << " ";
cout << endl;
// 反向输出
cout << "反向: ";
for (int i = (int)numbers.size() - 1; i >= 0; i--)
cout << numbers[i] << " ";
cout << endl;
// 求最大值、最小值、平均值
if (!numbers.empty())
{
int minVal = numbers[0], maxVal = numbers[0];
double sum = 0;
for (size_t i = 0; i < numbers.size(); i++)
{
if (numbers[i] < minVal) minVal = numbers[i];
if (numbers[i] > maxVal) maxVal = numbers[i];
sum += numbers[i];
}
cout << "最小: " << minVal << ", 最大: " << maxVal
<< ", 平均: " << sum / numbers.size() << endl;
}
return 0;
}
核心考点:vector作为动态数组使用、push_back动态增长、size_t索引遍历、stringstream解析输入。注意反向遍历时size_t是无符号,直接i = size-1到i>=0会变成死循环(无符号永远>=0),所以示例中用了int。后续学了STL迭代器和反向迭代器后,这类操作会更简洁。
本篇总结
- vector:连续内存、O(1)随机访问、尾部操作快、中间操作慢。默认首选
- list:节点链表、O(1)任意位置插入删除(已知迭代器)、不支持随机访问。特殊场景使用
- deque:分段连续、两端O(1)插入删除、支持随机访问。队列/任务池场景
- 通用操作:
push_back尾部插入、pop_back尾部删除、size()元素个数、empty()是否为空、clear()清空、front()/back()访问首尾 - 容量管理:
reserve(n)预留空间减少重新分配、capacity()实际容量、shrink_to_fit()释放闲置空间 - 传参约定:总是用引用传vector,只读加const——避免昂贵的深拷贝
- 迭代器注意:vector插入/删除后旧迭代器可能失效(后续详细讨论)
下篇预告
下一篇第三十一篇:STL算法(Algorithm)。学习STL中几十种通用算法——排序sort、查找find、遍历for_each、拷贝copy、计数count、累加accumulate等——配合容器使用,让数据处理代码更简洁高效。
更多推荐



所有评论(0)