cpp重学(三)
笔记内容来自https://geek-doc.cn/
- cpp重学(三)
一、对象切片
切片:值传递时的危害
class Shape {public: virtual double area() const { return 0; }};
class Circle : public Shape { double r_;public: Circle(double r) : r_(r) {} double area() const override { return 3.14159 * r_ * r_; }};
void show(Shape s) { // 注意:按值传 std::cout << s.area() << "\n";}
Circle c(5.0);show(c); // 输出 0!不是预期的 78.5show(c) 按值传给 Shape s——c 被复制成 Shape,复制过程只拷贝基类部分,Circle 独有的 r_ 被切掉,虚表指针也指向 Shape 的虚表。于是 s.area() 调的是 Shape::area,输出 0。这个过程就叫对象切片(slicing)。
根源是按值传参时对象的大小是固定的:Shape s 在栈上分配的是 Shape 大小的空间,装不下 Circle。复制时只能”切一块装得下的”——基类部分。C++ 语义上,“Circle 是 Shape 的一种”允许隐式转换,但按值转换必然切割。
同样一个坑:
std::vector<Shape> shapes; // 容器装的是 Shape(按值)shapes.push_back(Circle(5.0)); // 切片!Circle 变成 Shape 存进去所以防范的方法就是传指针或者引用
void show(const Shape& s) { // 引用:不拷贝 std::cout << s.area() << "\n"; // 78.5,多态正常}
void show2(const Shape* s) { // 指针同理 std::cout << s->area() << "\n";}
show(c); // 正常show2(&c); // 正常二、vector用法
vector是能自动生长的数组
#include <vector>
std::vector<int> v; // 空数组v.push_back(1); // 末尾追加v.push_back(2);v.push_back(3);
v.size(); // 元素个数 = 3v[0]; // 随机访问 O(1),和数组一样快v.at(0); // 带边界检查的访问(越界抛异常)v.empty(); // 是否为空
// 遍历for (int x : v) { /* ... */ } // 范围 forfor (auto& x : v) { x *= 2; } // 修改元素加引用
// 初始化std::vector<int> w = {1, 2, 3}; // 列表初始化std::vector<int> z(10, 5); // 10 个 5v[0] 和数组下标一样 O(1) 随机访问、不检查边界(越界是未定义行为);所以用at方法访问最佳,他会抛std::out_of_range 异常。
vector作为数组,内存也是连续的,所以缓存命中率很高。
大小与容量
std::vector<int> v;v.reserve(100); // 预分配能装 100 个元素的内存
v.size(); // 大小:当前实际有多少个元素 = 0v.capacity(); // 容量:当前分配的内存能装多少个 = 100扩容:为啥会卡一下?
vector扩容的原理是:当里面装满后,他会寻找一块更大的内存进行申请新的内存再把原来的内容拷贝过来。
扩容本身是 O(n):申请新内存 + 搬全部元素。虽然容量按倍数增长(通常 1.5~2 倍),均摊下来 push_back 还是 O(1),但扩容发生的那一次会明显卡一下——高频插入场景下,这种间歇卡顿不可接受。
为了预防卡顿,我们可以使用reserve来提前预支空间
std::vector<int> v;v.reserve(1000000); // 先申请好for (int i = 0; i < 1000000; ++i) { v.push_back(i); // 全程不扩容,稳定 O(1)}三、string深入
string是升级版的c字符串
#include <string>
std::string s = "hello";s += " world"; // 拼接s.push_back('!'); // 追加字符
s.length(); // 长度(和 size 等价)s.empty(); // 是否为空s.substr(0, 5); // 取子串 "hello"s.find("wor"); // 查找,返回位置s.compare("hello world"); // 比较(返回 <0/=0/>0)| 对比项 | C 字符串 | std::string |
|---|---|---|
| 长度 | strlen 每次 O(n) | .length() O(1) |
| 拼接 | strcat 要目标缓冲够大 | += 自动扩容 |
| 比较 | strcmp | ==、< 直接比 |
| 释放 | 手动 | 自动(析构) |
| 越界 | 未定义行为 | .at() 抛异常 |
查找失败时的返回值
查找失败返回npos,这是个天文数字
std::string s = "hello world";size_t pos = s.find("xyz");
if (pos != std::string::npos) { // 查到了,pos 是下标 // ...} else { // 没查到 // ...}两个坑:
- 忘了和
npos比较就当下标用,直接越界(npos 是个天文数字); - 返回类型是
size_t(无符号),if (s.find(x) >= 0)这种判断永远为真——无符号数不可能 < 0。
c_str()悬垂陷阱
.c_str() 返回一个 const char*,指向 string 内部缓冲区。这个指针随时可能失效:
const char* p = s.c_str();s += " more text"; // 触发扩容,内部缓冲区换了位置use(p); // p 指向已释放/无效的内存!悬垂规则两条:
c_str()拿到的指针,在下次修改 string 之前有效;- 修改过 string 之后,旧指针作废,必须重新
c_str()。
// 正确姿势:用完即丢,不存起来printf("%s\n", s.c_str()); // 临时用,安全// 或者先拷贝到自己的缓冲区std::string copy = s; // 要长期持有就自己存一份拼接效率
+=比+更快
std::string s = "a";s += "b"; // 追加:尽量复用已有空间s = s + "c"; // 加法:创建新对象再赋值对于高频拼接场景,我们要预分配然后+=
// 慢:每次 + 都新建一个 string(大量分配)std::string r;for (int i = 0; i < 10000; ++i) r = r + std::to_string(i);
// 快:reserve + append/+=(复用空间)std::string r;r.reserve(60000);for (int i = 0; i < 10000; ++i) r += std::to_string(i);string也是容器
支持下标,迭代器
for (char c : s) { /* 逐字符遍历 */ }for (auto it = s.begin(); it != s.end(); ++it) { /* 迭代器 */ }std::sort(s.begin(), s.end()); // 字符排序,算法库直接上四、Array与c数组
c数组存在一个老大难的问题:数组退化成指针(ida里反编译经常能看见喵)
void process(int arr[], int n) { // arr[] 其实退化成 int* // ...}
int data[10];process(data, 10); // 要手传长度C 里数组作为函数参数时退化成指针:int arr[] 等价 int* arr,长度信息丢失。于是每个 C 函数都得额外带一个长度参数,还得指望调用方传对。忘了长度、传错长度、越界写——C 数组三件套。
更隐蔽的是:
int data[10];printf("%zu\n", sizeof(data)); // 40:在定义处,sizeof 是整个数组printf("%zu\n", sizeof(f(data))); // 不可能,长度早没了sizeof 只能在数组定义处得到正确大小,一旦传给函数,sizeof(arr) 就是 8(指针大小)了。
cpp的array
#include <array>
std::array<int, 10> data; // 类型里直接写大小data.size(); // 10,随时可查
std::array<int, 5> a = {1, 2, 3, 4, 5};a[0]; // O(1) 访问a.at(0); // 带边界检查
void process(const std::array<int, 10>& data) { // 长度是类型的一部分,不会丢 for (int x : data) { /* ... */ }}三个关键点:
- 大小是类型的一部分:
std::array<int, 10>和std::array<int, 20>是两个不同类型,传参时类型自带长度,不会退化; - 有
size()、at()、范围 for:迭代器和算法库直接能用,C 数组做不了; - 零额外开销:底层就是裸数组,布局完全一样,没有堆分配、没有虚函数——编译器能把它优化成和内建数组一样。
std::array vs vector
std::array | std::vector | |
|---|---|---|
| 大小 | 编译期固定 | 运行期动态 |
| 内存 | 栈上(作为局部变量时) | 堆上 |
| 扩容 | 不可能 | 自动 |
| 适用 | 大小已知且固定 | 大小可变或不确定 |
| 性能 | 无分配开销 | 可能有扩容开销 |
选择很简单:大小编译期就定死 → std::array;大小运行期才知道/会变 → vector。两者遍历都缓存友好(连续内存),array 因为免堆分配,在嵌入式、高频小场景更有优势。
多维数组
std::array<std::array<int, 3>, 3> grid{}; // 3×3 二维数组
// 遍历for (const auto& row : grid) { for (int x : row) { /* ... */ }}五、list与deque
双向链表list
胖狗ps:在python里是列表哎
#include <list>
std::list<int> l = {1, 2, 3};l.push_back(4); // 尾插 O(1)l.push_front(0); // 头插 O(1)——这是 list 比 vector 强的地方l.insert(++l.begin(), 99); // 中间插入 O(1),不用搬元素
for (int x : l) { /* 遍历 */ }list 是双向链表:每个元素是一个节点,节点里存着前驱/后继指针。在已知位置插入/删除是 O(1)——改两个指针就完事,不像 vector 要搬动后面所有元素。代价是:
- 不能随机访问:
l[3]不存在,要从头走 3 步(O(n)); - 每个节点额外两个指针:8 或 16 字节开销;
- 内存不连续:每个节点单独
new,散落在堆的各处。
为什么list理论快,实际慢?
// 各插入 100 万个元素,然后遍历std::list<int> l;for (int i = 0; i < 1000000; ++i) l.push_back(i);
std::vector<int> v;v.reserve(1000000);for (int i = 0; i < 1000000; ++i) v.push_back(i);- list 的每个
push_back都new一个节点——100 万次堆分配; - vector 一次
reserve后零分配; - 遍历时:vector 顺序读连续内存,每读一次都命中缓存行;list 要跟着指针跳,每跳一次可能就是一次 cache miss——每次 miss 大约慢几十到几百纳秒。
结果:list 遍历可能比 vector 慢 3~10 倍,尽管 list 的插入”理论”是 O(1)。。
经验法则:默认 vector,真的需要”频繁在中间插入删除 + 不在乎遍历慢”才用 list。很多场景其实用 deque 或 vector + 排序更划算。
deque双端队列
#include <deque>
std::deque<int> d;d.push_back(1); // 尾插 O(1)d.push_front(0); // 头插 O(1)——vector 做不到d[2]; // 还能随机访问 O(1)!
for (int x : d) { /* ... */ }deque(double-ended queue)的特点:
- 头尾插入都是 O(1)(vector 头插是 O(n));
- 支持随机访问
d[i]O(1); - 底层是”若干块连续内存”拼起来的,不是整块连续——所以比 list 缓存友好得多,又比 vector 多了头插能力。
| vector | deque | list | |
|---|---|---|---|
| 尾插 | O(1) | O(1) | O(1) |
| 头插 | O(n) | O(1) | O(1) |
| 随机访问 | O(1) | O(1) | O(n) |
| 中间插入 | O(n) | O(n) | O(1) |
| 内存连续 | 整块 | 分块 | 每节点独立 |
| 缓存友好 | 最好 | 好 | 差 |
一句话总结选型:vector 是万金油,deque 是”两头都要能插”的 vector,list 只在”中间插入删除是刚需”时用——且用之前想清楚遍历代价。
六、set与map
map:按键找值
#include <map>
std::map<std::string, int> scores;scores["alice"] = 95; // 插入/更新scores["bob"] = 88;
std::cout << scores["alice"]; // 读取:95auto it = scores.find("bob"); // 查找if (it != scores.end()) { /* 找到了 */ }
for (const auto& [name, score] : scores) { // 有序遍历(按键排序) std::cout << name << ": " << score << "\n";}map<Key, Value>:键 Key 唯一、值 Value 可重复,按键自动排序(默认升序,比较器可自定义)。常见操作全 O(log n):插入、删除、查找、遍历有序。
隐藏行为警告:scores["alice"] = 95 看着只是赋值,其实做了两件事——如果 “alice” 不存在,operator[] 会先插入一个默认值(value 是 0/空串),再赋值。所以:
if (scores["nobody"] > 100) { ... } // 副作用:插入了 "nobody"!胖狗ps:这个坑人东西,写算法的时候被坑了好几回
所以为了避免这个,我们可以用find或者at
auto it = scores.find("lihua");if(it != scores.end()){}//存在// scores.at("nobody"); // 不存在会抛异常set:不重复的有序集合
#include <set>
std::set<int> s = {5, 1, 3, 1}; // 重复的 1 会被忽略s.insert(2);s.erase(1);
s.size(); // 去重后的大小if (s.count(3)) { /* 在不在集合里 */ }for (int x : s) { /* 有序遍历:1 2 3 5 */ }set<T>:元素唯一(插入重复的自动忽略)、有序(遍历即升序)。它就是”不需要值的 map”——set<int> 等价于 map<int, bool> 的语义简化版。常用场景:去重、判断存在、保序集合。
两者的底层
均是红黑树:自平衡二叉搜索树
- 查找/插入/删除都是 O(log n),最坏情况也有保证(树保持平衡);
- 中序遍历天然有序——所以
map遍历就是按键升序; - 代价:每个节点要存颜色标记和树指针,内存开销比
unordered_map略大。
胖狗ps:红黑树会在大佬的高手层进行讲解,这里我们先跳过,喵喵喵
| 用法 | 示例 | 注意 |
|---|---|---|
| 插入不覆盖 | m.emplace(k, v) | insert/emplace 已有键时不覆盖 |
| 统计词频 | m[w]++ | 简洁,但第一次会插入 0 |
| 判断存在 | m.count(k) / m.find(k) | count 只有 0/1(map) |
| 遍历 | for (auto& [k, v] : m) | 结构化绑定 |
| 自定义排序 | map<int, T, greater<int>> | 比较器可换 |
总结:map 是按键有序的字典(map<K,V>)、set 是有序去重集合,底层红黑树保证查找/插入/删除 O(log n)、遍历自动按键升序;m["key"] 找不到键时会自动插入默认值,只想查用 find/at
七、unordered_map与unordered_set
unordered_map:哈希表版字典
#include <unordered_map>#include <unordered_set>
std::unordered_map<std::string, int> u;u["alice"] = 95;u["bob"] = 88;
auto it = u.find("alice"); // 平均 O(1)u.count("alice"); // 0 或 1
std::unordered_set<int> s = {3, 1, 2};s.insert(5);s.erase(3);用法接口和map一模一样,只是底层结构与顺序不同
map / set | unordered_map / unordered_set | |
|---|---|---|
| 底层 | 红黑树 | 哈希表 |
| 查找/插入 | O(log n) | 平均 O(1),最坏 O(n) |
| 有序性 | 按键有序遍历 | 无序(遍历顺序不保证) |
| 内存 | 树节点(省一点) | 哈希桶(多一块) |
| 适用 | 有序遍历、范围查询 | 纯快速查找 |
为啥unordered更快?
有一说一,从名字就能看出来一点了(绷
哈希表把键直接算出一个桶位置(hash(key) % bucket_count),一次定位就到,不用像红黑树那样从根节点一路比较 log n 次:

胖狗ps:相当于靠哈希函数建立了一个映射关系,实现了平均O(1)的查找,缺点就是多占空间呗。
对大数据量(几万、几十万键),O(log n) 和 O(1) 的差距肉眼可见:log₂(100000) ≈ 17 次比较 vs 1 次哈希。所以纯查找场景 unordered 全面占优。
文字游戏“平均O(1)“的坑
- 哈希函数:好哈希让键均匀分布;哈希函数垃圾(或键故意构造对抗)时,全挤一个桶,退化为 O(n)(查链表);
- 最坏情况:攻击者可以构造同哈希的键(哈希碰撞攻击),所以安全敏感场景要留意;
- 遍历无序:
unordered_map的遍历顺序由桶布局决定,和插入顺序、键大小都没关系——想按序输出,得先拷到vector再sort。
选型
| 需求 | 选谁 |
|---|---|
| 快速查找、插入(量大的字典/缓存) | unordered_map |
| 需要按键有序遍历 | map |
| 范围查询(查”a”到”m”之间的键) | map(树支持范围) |
| 数据量很小(几十个) | 都行,map 稍省内存 |
| 键是自定义结构体 | 都要提供哈希/比较,unordered 稍麻烦 |
经验法则(存量文档的原话):只查不遍历用 unordered_map;需要有序或范围查询用 map。词频统计、配置文件解析、缓存这类”键查值”场景,unordered 是默认首选。
八、迭代器与范围for
迭代器:泛化的指针
std::vector<int> v = {3, 1, 2};
for (auto it = v.begin(); it != v.end(); ++it) { std::cout << *it; // *it 解引用,跟指针一模一样}迭代器的操作就是指针那套:* 解引用取元素、++ 移到下一个、!= 比较。它把”怎么在容器里移动”抽象成统一接口,于是:
// 这段代码换成 list、deque、map、set 都能跑for (auto it = c.begin(); it != c.end(); ++it) { std::cout << *it;}胖狗ps:牛皮,python里估计抄的cpp吧哈哈哈。
这就是”泛化”的含义:sort、find 这些算法只认迭代器,不认具体容器。
迭代器类型
不同容器提供不同能力的迭代器:
| 类别 | 能力 | 容器 |
|---|---|---|
| 随机访问 | it + n、it[n]、it - it | vector、deque、array |
| 双向 | ++、-- | list、map、set |
| 前向 | 只能 ++ | forward_list、unordered_map |
判断标准:std::sort 要求随机访问迭代器(要算中间位置),所以不能直接 sort 一个 list(list 只有双向迭代器,要 l.sort() 成员函数)。算法对迭代器能力有要求,这是常见编译错误来源。
范围for的底层
for (int x : v) { ... }编译器会把它展开近似成迭代器的循环
for (auto it = v.begin(); it != v.end(); ++it) { int x = *it; // 拷贝(int 便宜) ...}所以范围 for 的所有注意点都来自迭代器:
for (int x : v):拷贝每个元素(元素大时浪费,用const auto&);for (auto& x : v):引用,能改原元素;- 遍历时不要增删元素(下一节)。
迭代器失效:隐蔽的大雷
容器结构变了,旧迭代器可能”指空”——这叫迭代器失效(invalidation)。经典场景:
std::vector<int> v = {1, 2, 3, 4};auto it = v.begin(); // 指向 1
v.push_back(5); // 可能触发扩容(第 32 篇)// *it; // 危险!扩容后内存换了地方,it 悬垂| 操作 | vector | list / map | unordered_map |
|---|---|---|---|
| 插入(可能扩容) | 全部失效 | 不受影响 | 可能全部失效 |
| 删除某个元素 | 被删位置及之后失效 | 只失效被删的 | 只失效被删的 |
push_back 不扩容 | 之前的迭代器仍有效 | 不受影响 | — |
结论:vector 的插入/删除最容易让迭代器失效(扩容全灭、删除后半段灭);list/map 删除只灭自己。遍历中要删元素,别用范围 for,用迭代器循环 + erase 的返回值:
// 删除所有偶数(vector)for (auto it = v.begin(); it != v.end(); ) { if (*it % 2 == 0) it = v.erase(it); // erase 返回下一个有效迭代器 else ++it;}九、sort/find等算法库
算法只认迭代器
#include <algorithm>#include <vector>
std::vector<int> v = {5, 2, 8, 1};
std::sort(v.begin(), v.end()); // 排序auto it = std::find(v.begin(), v.end(), 8); // 查找int n = std::count(v.begin(), v.end(), 1); // 计数bool any = std::any_of(v.begin(), v.end(), [](int x){ return x < 0; }); // 判断算法接收迭代器范围 [begin, end),对”是什么容器”一无所知——vector、deque、array 全都能用。这就是容器与算法的解耦:你想换容器,算法代码一行不用改。
注意范围是左闭右开 [begin, end):end 指向最后一个元素的”后一个位置”,不含它自己。这是 STL 的统一约定,写循环边界时心里要有这个数。
排序与自定义比较器
默认升序:
std::sort(v.begin(), v.end()); // 升序std::sort(v.begin(), v.end(), std::greater<int>()); // 降序自定义结构体排序——给 sort 第三个参数传比较器(比较函数或 lambda)
struct Student { std::string name; int score; };
std::vector<Student> students = {{"alice", 95}, {"bob", 88}, {"carol", 92}};
std::sort(students.begin(), students.end(), [](const Student& a, const Student& b) { return a.score > b.score; // 按分数降序 });其他算法
std::vector<int> v = {3, 1, 4, 1, 5, 9};
auto it = std::find(v.begin(), v.end(), 4); // 找 4,返回迭代器if (it != v.end()) { /* 找到了,*it == 4 */ }
int ones = std::count(v.begin(), v.end(), 1); // 1 出现 2 次
std::sort(v.begin(), v.end()); // 排序后:std::binary_search(v.begin(), v.end(), 5); // 二分查找 O(log n)(要求已排序)
std::min_element(v.begin(), v.end()); // 最小元素位置std::max_element(v.begin(), v.end()); // 最大元素位置std::accumulate(v.begin(), v.end(), 0); // 求和(<numeric>)std::reverse(v.begin(), v.end()); // 反转十、stack 与 queue
这些玩意是在其他数据结构基础上构建的,也被称作容器适配器(container adapter)
ps:其实都是在链表和数组基础上构建的。。
#include <stack>#include <iostream>
int main() { std::stack<int> s; s.push(1); s.push(2); s.push(3); while (!s.empty()) { std::cout << s.top() << " "; // 3 2 1 s.pop(); }}push 放进去,top 看顶,pop 弹掉,empty 判断空。接口就这几个,没有下标访问、没有迭代器、没有 size 之外的任何”透视”能力——你永远只能碰到栈顶那一个元素。

三者接口都极简,所以名字里带”适配器”而不是”容器”:它们内部各自持有一个底层容器,把底层容器能干的活裁剪成一套固定的窄接口。注意表格里的默认底层:
| 适配器 | 默认底层 | 可换底层 | 特性 |
|---|---|---|---|
std::stack | deque | vector / list | 后进先出 |
std::queue | deque | list | 先进先出 |
std::priority_queue | vector | deque | 堆结构,最大元素在顶 |
为什么 stack 默认选 deque 而不是 vector?因为 deque 头部也能高效操作,而且它扩容时不像 vector 那样整块搬移;对 stack 这种”只从一端进出”的用法,deque 各方面都够用且无坑。queue 同理,需要头部弹出,deque 头尾操作都是 O(1)。priority_queue 需要随机访问(堆的 sift 操作要不停访问中间元素),所以底层必须是 vector 或 deque,不能是 list。
stack:后进先出
#include <stack>
std::stack<int> s;s.push(1); // 入栈int x = s.top(); // 读栈顶(不弹)s.pop(); // 弹栈顶(不返回值)s.empty(); // 是否为空s.size(); // 元素个数有个历史坑要提醒:pop() 不返回被弹出的元素。早期 C++ 设计里,让 pop 返回元素就得先拷贝、再删,拷贝可能抛异常,删了之后返回的值和栈状态就不一致了;所以标准库选择”读顶用 top(),弹顶用 pop()”,两步走。
top() 返回引用,所以也能写 s.top() = 42; 直接改栈顶。但注意:空栈上调用 top() 或 pop() 是未定义行为(UB),程序可能直接崩。写循环前先判 !s.empty(),这个习惯要养成。
底层可换:指定第二个参数即可
#include <stack>#include <vector>
std::stack<int, std::vector<int>> sv; // 用 vector 当底层所以stack的本质就是个:一块内存+一个指着栈顶的指针,汇编里常用sp表示。
queue:先进先出
#include <queue>
std::queue<int> q;q.push(1); // 队尾入队q.push(2);int f = q.front(); // 队头(最先进入的)int b = q.back(); // 队尾(最后进入的)q.pop(); // 队头出队queue 对应 C 里环形缓冲区(ring buffer)的活。环形缓冲的原理是:数组 + head/tail 两个下标,元素出队时 head 往前走,到尾了绕回开头,把数组当成一个环来用,避免每出队一个元素就把后面全部往前搬。这块内容在内存/存储相关的并发队列里经常出现——比如生产者消费者模型,用的就是队列语义。
priority_queue:优先级队列
priority_queue 是三个里最特别的——它内部维护一个二叉堆,top() 永远返回”当前最大”(默认按 < 比较)的元素:
#include <queue>#include <iostream>
std::priority_queue<int> pq;pq.push(3);pq.push(1);pq.push(4);pq.push(1);pq.push(5);
while (!pq.empty()) { std::cout << pq.top() << " "; // 5 4 3 1 1 pq.pop();}输出按从大到小。top() 是 O(1),push/pop 都是 O(log n)——这是堆的特性,插入和删除都在树高(log n)内完成。对比一下:
| 操作 | vector(找最大) | priority_queue |
|---|---|---|
| 插入 | O(1) 尾插 | O(log n) 上浮 |
| 取最大 | 需遍历 O(n) | O(1) |
| 弹最大 | 需先找到 O(n) | O(log n) 下沉 |
所以”随时要取当前最大/最小”的场景,priority_queue 是标准答案:堆排序、Dijkstra 最短路、Top-K 问题、事件驱动模拟(时间最早者优先),都是它的主场。
自定义比较器
默认是大顶堆,我们可以自定义第三个参数
#include <queue>#include <vector>
// 小顶堆:用 std::greater 反转比较方向std::priority_queue<int, std::vector<int>, std::greater<int>> min_pq;
// 自定义类型:按某个字段比较struct Task { int priority; int id;};
struct TaskCmp { bool operator()(const Task& a, const Task& b) const { return a.priority < b.priority; // 优先级大的先出 }};
std::priority_queue<Task, std::vector<Task>, TaskCmp> tasks;注意 TaskCmp 是个结构体而不是函数,这是和 sort 不同的地方——比较器是类型,在编译期就确定了。lambda 也行(C++20 才允许直接用 lambda 类型,之前要先 decltype 包一层),后续 lambda 那篇会展开。这里先记住套路:比较器是个结构体,重载 operator(),返回”a 应该排在 b 后面”为 true。
ps:卧槽,这为啥不通用一下都用函数捏。
总结:stack、queue、priority_queue 是容器适配器——不自己存数据,站在 deque/vector 肩膀上收窄接口,分别用后进先出、先进先出、优先级最大这三种出队规则,把”取哪个”的决策交给类型本身去保证。
部分信息可能已经过时









