news 2026/11/4 11:58:06

第十四章 标准模块库(STL)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
第十四章 标准模块库(STL)

第十四章 标准模板库(STL)

1. STL 概述

1.1 六大部件

STL 是 Standard Template Library 的缩写。名字里带模板两个字,第十三章那张图纸就是它的地基,容器和算法全靠模板撑起来。第一章提过,C++98 这一版把 STL 迎进门,从此写 C++ 不用什么都自己造。

STL 里有六种部件,一间屋子六件家具。

┌─────────────────────────────────────────┐ │ 六大部件 │ ├─────────────────────────────────────────┤ │ 一间屋子,六件家具 │ │ │ │ 容器 数据放哪,vector list map │ │ 迭代器 怎么挨个走一遍 │ │ 算法 排序查找累加,认迭代器 │ │ 函数对象 行为像函数的东西 │ │ 适配器 换接口用,stack queue │ │ 分配器 内存谁管,默认那套够用 │ │ │ │ 挑大梁的:容器、迭代器、算法 │ └─────────────────────────────────────────┘

容器管放数据,迭代器管挨个走,算法管排序查找这些套路。这三样挑大梁,本章的主角。函数对象是行为像函数的东西,算法排序时候能指定谁大谁小,知道有它就行。适配器换接口用,stack 和 queue 就是拿 deque 改的。分配器管内存从哪来,默认那套够用一辈子,日常提都不用提。

1.2 头文件和命名空间

用 STL 先得请头文件。老写法是 iostream.h 那样带 .h 的,九十年代的老貌,现在的编译器不认。新规矩一律不带 .h,vector 住 ,list 住 ,map 住,排序查找住 。

┌────────────────────────────────────────────┐ │ 头文件和大院 │ ├────────────────────────────────────────────┤ │ 两种老写法,一条新规矩 │ │ │ │ #include <iostream.h> 九十年代的老貌 │ │ #include <vector> 现在的写法 │ │ │ │ 头文件一律不带 .h │ │ using namespace std; 全院名字直接写 │ │ std::vector<int> v; 写全名也成 │ │ │ │ vector 住 std 大院,第一章那句话没变 │ └────────────────────────────────────────────┘

第一章说过,std 是标准库住的大院,不声明的话写 cout 要写全名 std::cout。这条对 STL 原样管用。using namespace std;写上,全院名字直接写,vector、sort 都能省掉前缀。代码有规模了,或者多人合作,再老老实实写 std:: 前缀。本书图省事,都写上 using。

2. 容器

2.1 vector 动态数组

vector 是最常用的容器,没有之一。它是个会长个的数组,不用提前说好放几个,尾部加数据这事它包了。

#include <iostream> #include <vector> using namespace std; int main() { vector<int> scores; scores.push_back(78); scores.push_back(92); scores.push_back(55); scores.push_back(100); scores.push_back(88); cout << "size = " << scores.size() << endl; cout << "第一个 " << scores[0] << endl; cout << "最后一个 " << scores[scores.size() - 1] << endl; for (int s : scores) { cout << s << " "; } cout << endl; return 0; }

输出长这样。

size = 5 第一个 78 最后一个 88 78 92 55 100 88

vector 意思是装 int 的 vector,第十三章的图纸在这里第一次派上正经用场。scores[0] 和数组一样下标取值,size() 问个数,push_back 往尾部加。最后那个 for 是范围 for,冒号左边写元素类型,右边写容器,从头到尾走一遍,不用数下标。第五章手写循环数成绩的日子,到这里可以翻篇了。

vector 有个隐藏技能,它会趁你不备偷偷扩容。实测九次 push_back,size 和 capacity 的变化是这样。

#include <iostream> #include <vector> using namespace std; int main() { vector<int> v; for (int i = 0; i < 9; i++) { v.push_back(i); cout << "push " << i << " size " << v.size() << " capacity " << v.capacity() << endl; } return 0; }
push 0 size 1 capacity 1 push 1 size 2 capacity 2 push 2 size 3 capacity 4 push 3 size 4 capacity 4 push 4 size 5 capacity 8 push 5 size 6 capacity 8 push 6 size 7 capacity 8 push 7 size 8 capacity 8 push 8 size 9 capacity 16

capacity 是院子大小,size 是住了几口人。放不下了就换个大院子,整体搬家,搬完旧地址全部作废。这条规矩记住,3.3 节要吃它一次亏。

┌────────────────────────────────────────┐ │ vector 会长个 │ ├────────────────────────────────────────┤ │ push_back 一节,size 长一节 │ │ │ │ 装了 3 个 size 3 capacity 4 │ │ 装到第 5 个 size 5 capacity 8 │ │ │ │ 院子不够就换大的,连人带家当搬一次 │ │ 搬完家,旧地址全部作废 │ └────────────────────────────────────────┘

要是能提前估摸个数,reserve 函数先把院子圈下来,少搬几回家。提一句,知道有它就行。

2.2 其余序列容器

vector 不是一个人在战斗。序列容器一家四口,元素排成一条线,区别在两头和中间的脾气。

┌───────────────────────────────────────────────┐ │ 容器家谱 │ ├───────────────────────────────────────────────┤ │ 四大门户 │ │ │ │ 序列容器 vector deque list array │ │ 关联容器 set map,按键排好 │ │ 无序关联 unordered_set map,点名 │ │ 适配器 stack queue priority_queue │ │ │ │ 家谱底下全是第十三章的图纸 │ └───────────────────────────────────────────────┘

deque 两头都能插都能删,vector 只擅长一头。array 是定长数组,大小写死在类型里,为的是跑起来更快,日常用得少。list 是双向链表,第九章那套 Node 加 next 的现代化版本,中间插删是它的看家本领。

list 插删有多爽,第九章的痛苦就有多深。头插三个数,手写链表要两步加一句注释提醒自己别把旧头弄丢,list 一个函数的事。

#include <iostream> #include <list> using namespace std; int main() { list<int> lst; for (int i = 1; i <= 3; i++) { lst.push_front(i); } cout << "头插结果:"; for (list<int>::iterator it = lst.begin(); it != lst.end(); ++it) { cout << *it << " "; } cout << endl; lst.push_back(99); cout << "尾接 99 之后:"; for (int x : lst) cout << x << " "; cout << endl; return 0; }
头插结果:3 2 1 尾接 99 之后:3 2 1 99

输出和第九章一字不差,3 2 1,后来的站前面。区别在代码量,第九章那版洋洋洒洒一大段,这版二十行不到,还没有 new 没有 delete。链表那章要是当时有 list,我能少熬一个下午。

2.3 关联容器

序列容器管排成一条线的,关联容器管按键找值的。set 是一列不重复的钥匙,自动排好。map 是钥匙对值,也按钥匙排好。

┌─────────────────────────────────────────┐ │ 一对集装箱 │ ├─────────────────────────────────────────┤ │ 一个管去重,一个管查值 │ │ │ │ set 就一列钥匙,不重复,自动排好 │ │ map 钥匙对值,按钥匙排好 │ │ │ │ scores["李四"] = 92; │ │ 一句入库,不用先遍历找人 │ └─────────────────────────────────────────┘

去重和查值是这两位的看家本领。第七个分数录进来,92 和 78 前面都有,set 伸手一收,重复的拦在门外。

#include <iostream> #include <set> #include <map> #include <string> using namespace std; int main() { int raw[7] = {78, 92, 55, 100, 88, 92, 78}; set<int> uniq(raw, raw + 7); cout << "去重后 " << uniq.size() << " 个:"; for (set<int>::iterator it = uniq.begin(); it != uniq.end(); ++it) { cout << *it << " "; } cout << endl; map<string, int> scores; scores["张三"] = 78; scores["李四"] = 92; scores["王五"] = 55; scores["赵六"] = 100; scores["孙七"] = 88; cout << "李四考了 " << scores["李四"] << endl; cout << "班里共 " << scores.size() << " 人" << endl; if (scores.count("李四") == 1) { cout << "李四在册" << endl; } for (map<string, int>::iterator it = scores.begin(); it != scores.end(); ++it) { cout << it->first << " " << it->second << endl; } cout << endl; return 0; }
去重后 5 个:55 78 88 92 100 李四考了 92 班里共 5 人 李四在册 孙七 88 张三 78 李四 92 王五 55 赵六 100

set 的迭代器走出来就是排好序的,55 打头,100 收尾。map 用方括号加钥匙存取,scores 后面跟上钥匙名,一句话查到 92 分,不用遍历不用 if,钥匙本身就是下标。it->first 是钥匙,it->second 是值,一对一对往外递。

还有一对 unordered_set 和 unordered_map,按键排好的换成按键散列,查得更快,走出来没顺序。日常提一句,用到再翻。

2.4 容器适配器

stack 和 queue 不是新容器,是适配器,拿别人换接口。stack 后进先出,queue 先进先出,都只开一个口,不许你挑中间下手。

#include <iostream> #include <stack> #include <queue> using namespace std; int main() { stack<int> st; st.push(1); st.push(2); st.push(3); cout << "栈顶 " << st.top() << ",大小 " << st.size() << endl; while (!st.empty()) { cout << st.top() << " "; st.pop(); } cout << endl; queue<string> q; q.push("张三"); q.push("李四"); q.push("王五"); cout << "队头 " << q.front() << ",队尾 " << q.back() << endl; while (!q.empty()) { cout << q.front() << " "; q.pop(); } cout << endl; return 0; }
栈顶 3,大小 3 3 2 1 队头 张三,队尾 王五 张三 李四 王五

stack 的 top 看栈顶,pop 出栈,3 2 1 出去,后进先出。queue 的 front 是队头,back 是队尾,张三 李四 王五 排着队出去。还有 priority_queue,默认大头优先,提一句。

2.5 怎么选

容器一大家子,新手最常问的问题是用哪个。答案永远是先想清楚要干什么。

┌────────────────────────────────┐ │ 挑容器 │ ├────────────────────────────────┤ │ 先问自己要干什么 │ │ │ │ 只会一头加删 vector │ │ 两头都要插删 deque │ │ 中间频繁插删 list │ │ 查重、要排好 set │ │ 按键查值 map │ │ 后进先出 stack │ │ 先进先出 queue │ │ │ │ 拿不准就先 vector,够用 │ └────────────────────────────────┘

只会一头加删,vector 够用,而且它 cache 友好,跑得最快。两头都要插删,换 deque。中间频繁插删,list。要查重要排好,set。按键查值,map。后进先出 stack,先进先出 queue。实在拿不准,先 vector,等它成为瓶颈再换,多数程序到不了那一步。

3. 迭代器

3.1 迭代器是个什么东西

容器挨个走一遍这事,第六章的指针能干,但每种容器长得不一样,指针认不出 list 的节点。STL 的答案是迭代器,一个泛化的指针,认位置不认地址。用法和指针像一个模子刻出来的。

┌───────────────────────────────────────────┐ │ 泛化的指针 │ ├───────────────────────────────────────────┤ │ 指针认地址,迭代器认位置 │ │ │ │ vector<int>::iterator it = v.begin(); │ │ │ │ *it 拿到那一格的东西 │ │ ++it 挪到下一格 │ │ it != v.end() 还没走到头 │ │ │ │ 换个容器,这套写法原样管用 │ └───────────────────────────────────────────┘

begin 指向第一个,end 指向最后一个的后一格,是个哨兵,解引用它是犯法的。*it 拿到那一格的东西,++it 挪到下一格,it != v.end() 问走到头没有。这段配方在 vector、list、set、map 上原样管用,换个容器换个写法这件事不存在。

上一节 list 那段已经用它走过一遍,再看个 vector 的。

#include <iostream> #include <vector> using namespace std; int main() { vector<int> scores = {78, 92, 55, 100, 88}; for (vector<int>::iterator it = scores.begin(); it != scores.end(); ++it) { cout << *it << " "; } cout << endl; return 0; }
78 92 55 100 88

vector::iterator 这串字面意思是 vector 的迭代器类型,写在尖括号外面,第十三章的类模板语法在这里露脸。日常写循环推荐范围 for,短省事,要删元素或者倒着走时候再请出迭代器本尊。auto 也能替它,auto it = v.begin(),类型让编译器猜,C++11 的东西,提一句。

3.2 五种迭代器

迭代器按力气分五等,力气大的什么都能干,小的干不了大的。

┌─────────────────────────────────────────────┐ │ 五种迭代器 │ ├─────────────────────────────────────────────┤ │ 力气一层比一层大 │ │ │ │ 输入 只读,一遍过,cin │ │ 输出 只写,一遍过,cout │ │ 前向 只往前走,forward_list │ │ 双向 能进能退,list set map │ │ 随机访问 能跳能算,vector deque array │ │ │ │ 会跳的能替不会跳的,反过来不成 │ └─────────────────────────────────────────────┘

输入迭代器只读,一遍过, cin 就是。输出迭代器只写,也是一遍过,cout 就是。前向只往前走,forward_list 和无序容器提供这种。双向能进能退,list set map 都是。随机访问力气最大,能跳能算,it + 3 直接跳到第四格,vector deque array 供着它。

为什么较这个真,因为算法的门槛写在明处。find 这种逐个看的,前向就够。sort 要跳着比的,非随机访问不可,这就是下一节要踩的坑。

3.3 失效这堂课

迭代器有个著名的坑,叫失效。2.1 节说过,vector 放不下就换院子整体搬家。搬完家,旧的 begin 和 end 指向被扔掉的旧院子,手里攥着的迭代器全部作废。这时候解引用,读到哪全看运气。

#include <iostream> #include <vector> using namespace std; int main() { vector<int> v; v.push_back(10); v.push_back(20); vector<int>::iterator it = v.begin(); v.reserve(10); v.push_back(30); v.push_back(40); cout << "留了余地,*it = " << *it << endl; return 0; }
留了余地,*it = 10

reserve 先把院子圈到十间,后面两次 push_back 都在余量里,不放量不搬家,it 指的地方纹丝不动,输出稳稳的 10。把 reserve 那行删掉再跑,it 作废,解引用读到哪全看运气,标准给这种行为起了个名字,叫未定义行为。程序没崩不代表没事,只是这次运气好。

┌───────────────────────────────────┐ │ 失效这堂课 │ ├───────────────────────────────────┤ │ push_back 触发放量 │ │ │ │ 旧 begin 指向被扔掉的旧院子 │ │ 旧 end 同上 │ │ it 读到哪全看运气 │ │ │ │ list 插删不失效,节点各住各的 │ └───────────────────────────────────┘

规律好记。vector 和 deque 插删都可能搬家,erase 删掉一个,后面全体往前挪,被删位置往后都作废。list 是例外,节点各住各的,插删只改邻居的 next 和 prev,别人的迭代器一根毛都不掉。这也是 list 中间插删拿得出手的代价换来的。

躲坑的办法也简单,增删之后重新取一遍 begin 和 end,别攥着旧的不撒手。

4. STL 算法

4.1 算法不认容器

算法住在 里,是 STL 里最庞大的一支部队。它们有个共同脾气,不认容器,只认迭代器。

┌─────────────────────────────────┐ │ 算法不认容器 │ ├─────────────────────────────────┤ │ sort(v.begin(), v.end()); │ │ │ │ 算法 只认迭代器 │ │ 迭代器 知道怎么走 │ │ 容器 提供 begin 和 end │ │ │ │ 三层各管一层,谁也别越界 │ └─────────────────────────────────┘

sort 收两个迭代器,不知道背后是 vector 还是 deque,也不想知道。它按迭代器的步子排序,随机访问的步子能跳,链表那种一步一脚的它使不上劲。三层分工明明白白,算法出套路,迭代器出脚力,容器出场地。谁也别越界,谁也能被换掉。今天写出 sort(v.begin(), v.end()),明天换个容器,这句原样编译。

4.2 常用算法过一遍

排序是算法里的头号招牌。第五章手写冒泡,交换要借临时变量,三个杯子换水不倒第三杯倒不成。当时说 swap 在 里现成的,本章连排序一起还账。

#include <iostream> #include <vector> #include <algorithm> #include <utility> using namespace std; int main() { vector<int> scores = {78, 92, 55, 100, 88}; sort(scores.begin(), scores.end()); cout << "升序:"; for (int s : scores) cout << s << " "; cout << endl; reverse(scores.begin(), scores.end()); cout << "降序:"; for (int s : scores) cout << s << " "; cout << endl; swap(scores[0], scores[scores.size() - 1]); cout << "首尾对调:"; for (int s : scores) cout << s << " "; cout << endl; return 0; }
升序:55 78 88 92 100 降序:100 92 88 78 55 首尾对调:55 92 88 78 100

sort 默认升序,reverse 反手一个降序,swap 首尾对调。第五章那段冒泡,这里三个函数名。想降序排还有一条路,sort 加一个 greater(),函数对象第一次露面,知道有它就行。

查找计数也不在话下。find 挨个比对,找不着返回 end()。count_if 数满足条件的,条件是普通函数,C++11 的 lambda 也能当。accumulate 住 ,累加专用。

#include <iostream> #include <vector> #include <algorithm> #include <numeric> using namespace std; bool isFail(int s) { return s < 60; } int main() { vector<int> scores = {78, 92, 55, 100, 88}; vector<int>::iterator pos = find(scores.begin(), scores.end(), 92); if (pos != scores.end()) { cout << "92 在,下标 " << pos - scores.begin() << endl; } vector<int>::iterator miss = find(scores.begin(), scores.end(), 60); cout << "60 在吗 " << (miss != scores.end()) << endl; cout << "不及格人数 " << count_if(scores.begin(), scores.end(), isFail) << endl; int sum = accumulate(scores.begin(), scores.end(), 0); cout << "总分 " << sum << ",平均 " << (double)sum / scores.size() << endl; return 0; }
92 在,下标 1 60 在吗 0 不及格人数 1 总分 413,平均 82.6

三个细节说一句。find 的回报是迭代器不是下标,随机访问容器上减一下 begin 就折成下标,60 这种不听话的减不出来,所以先用 != 比过 end() 再说话。count_if 收普通函数名 isFail 就够,函数退化成指针递过去,STL 不挑食。总分 413 平均 82.6,和第五章手写那版一个数,现成的和手搓的在这件事上打成平手,平得好。

4.3 算法和容器的分工

认迭代器这条规矩,有时会绊人。list 中间插删天下无敌,sort 却不服务它,谁让它的迭代器不会跳。list 自己有个成员版 lst.sort(),专门补这个洞,用成员算法,别用全局的。

算法这张单子很长,排序查找计数累加复制删除轮换,几百个,常用的占一成。本书不抄单子,知道 里有座宝库,要用啥现翻,比硬背实惠。

本章小结

本章把 STL 的门推开了一条缝。

┌───────────────────────────────────────┐ │ 本章收口 │ ├───────────────────────────────────────┤ │ 一层一层往上收 │ │ │ │ 六大部件,容器迭代器算法挑大梁 │ │ 头文件光写字,大院还是那个 std │ │ vector 会长个,搬家后旧迭代器全废 │ │ set 管去重,map 管查值 │ │ 算法认迭代器不认容器,三层分工 │ │ │ │ 下一章异常处理,越界有个正经说法 │ └───────────────────────────────────────┘

STL 是模板盖起来的楼,六大部件里容器、迭代器、算法挑大梁。头文件不带 .h,名字住 std 大院,第一章那句话原样管用。vector 最常用,会长个,扩容搬家之后旧迭代器全废,躲坑的办法是增删后重新取。set 管去重,map 管按键查值,插删看场合,拿不准先 vector。

迭代器是泛化的指针,begin 加 end 圈出半开区间,五种迭代器按力气分等,算法的门槛就写在迭代器会干什么上。算法不认容器只认迭代器,三层分工,各管一层,换来的是容器随便换、算法照样跑。

手写数组、手写链表、手写冒泡那三样本事没有白学,不知道底层的轮子怎么转,现成的用着也不踏实。到此 C++ 的常用招式差不多了,下一章讲异常处理,vector 下标越界、map 按键扑空这些事,有个正经的说法来接。

附录 A 速查卡

写法含义
#include请 vector 进场
vector v声明一个空 vector
v.push_back(x)尾部加一个
v.size() 与 v.capacity()个数与院子大小
for (int x : v)范围 for 走一遍
v.begin() 与 v.end()头迭代器与尾哨兵
*it 与 ++it解引用与前进
it->first 与 it->secondmap 的钥匙与值
lst.push_front(x)list 头插
set s去重有序集合
map<string, int> m键值对,键有序
sort(v.begin(), v.end())升序排,随机访问专用
find 与 count_if查找与条件计数
accumulate(v.begin(), v.end(), 0)累加,住
st.top() 与 q.front()栈顶与队头

附录 B 排错表

常见现象病根药方
v[10] 输出没谱或直接崩下标越界不检查换 v.at(10),或先和 size() 比
at 越界后程序 terminateout_of_range 没人接异常处理那章 try catch 接
sort 用到 list 上编译失败list 迭代器不是随机访问用成员版 lst.sort()
map 查不存在的键得 0,表还胖了方括号会顺手插入先用 count 问在不在
accumulate 报未声明忘请补头文件
push_back 之后旧迭代器没谱院子搬了家,旧的作废重新取 begin 与 end
find 结果没比 end() 就解引用没找到也当找到先 != 比过再解引用
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/4 11:55:12

MySQL SSL 加密复制:证书配置、MASTER_SSL 与传输强制

个人主页&#xff1a;> for_ever_love__ <&#xff08;欢迎各位大佬莅临&#x1f60a;&#xff09; 其他栏目: > 大模型开发从0到1 < 其他栏目: > iOS项目总结大全 < 其他栏目: > 我想学python了 < 其他栏目: > iOS UI < 文章目录MySQL SSL 加密…

作者头像 李华
网站建设 2026/11/4 11:52:42

4 个招聘平台职位发布时间贴回列表,精确到分钟

4 个招聘平台职位发布时间贴回列表&#xff0c;精确到分钟 【免费下载链接】boss-show-time 展示boss直聘岗位的发布时间 项目地址: https://gitcode.com/GitHub_Trending/bo/boss-show-time 你盯着列表第一条挂着"急聘"的岗位点进去&#xff0c;发现它实际是…

作者头像 李华
网站建设 2026/11/4 11:49:01

Xoodyak算法的各种密码分析方法全面盘点

Xoodyak算法的各种密码分析方法全面盘点Xoodyak作为NIST轻量级密码竞赛的决赛算法&#xff0c;其安全性受到了学界的广泛审查。目前&#xff0c;所有已发表的攻击都针对缩减轮数的版本&#xff0c;其完整的12轮核心置换Xoodoo尚未被任何攻击所突破。以下是根据现有研究&#xf…

作者头像 李华
网站建设 2026/11/4 11:47:22

水上无人机通信链路设计:5公里图传+双北斗抗干扰

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/4 11:32:01

Bilibili-Evolved 使用指南:3 步把 B 站改成你的样子

Bilibili-Evolved 使用指南&#xff1a;3 步把 B 站改成你的样子 【免费下载链接】Bilibili-Evolved 强大的哔哩哔哩增强脚本 项目地址: https://gitcode.com/gh_mirrors/bi/Bilibili-Evolved B 站页面长什么样、下载什么编码&#xff0c;官方说了算&#xff0c;你插不上…

作者头像 李华
网站建设 2026/11/4 11:30:38

构建智能零售库存可视化系统:Hermes Agent实时监控与预警

构建智能零售库存可视化系统&#xff1a;Hermes Agent实时监控与预警 在当今快节奏的零售环境中&#xff0c;实时掌握库存状况并及时预警异常情况对企业的运营效率和盈利能力至关重要。Hermes Agent作为一款强大的智能代理工具&#xff0c;能够帮助零售企业构建高效的库存可视…

作者头像 李华