课程 · C++ 速查

C++ 速查

这本书的代码里出现、而课程本身不打算教的那些 C++ 写法。 每条三样:一句话说清它在干什么、(有的话)这本书为它踩过的坑、以及本书哪一章第一次用到它

75 条其中 34 条带「⚠ 容易踩的地方」10 组9 份可运行示例50 道例题与练习
怎么用它

这是一张的表,不是一门 C++ 课。它假设你已经会变量、iffor、函数和普通数组 —— 从这儿往上,才是本书代码里会冒出来、 而正课不会停下来讲的那些东西。

每一组底下都有一份能直接点「运行」的最小示例:与其读解释,不如把数字改一改看它变成什么。

⚠ 光查是学不会的 —— 每一组都配了题

一张表再厚,也只是「看过」。所以每一组另开了一页, 放 20 道例题(题面 + 样例 + 完整参考代码 + 一步步的讲解)和30 道练习(只给题面和一句提示,答案折叠着,先自己写)。 十组一共 50 道,每道都能直接点运行。

零基础的话,别从这张表读起 —— 挑一组,点进它的练习页,从第一道例题开始做。

位运算 · 整数类型与溢出 · 数组与 vector · 字符串 string · 排序与比较器 · STL 容器(本书只用到这几个) · 读入与输出 · 函数、引用与递归 · 零碎但天天用的那几样 · 报错了怎么读

为什么会有这一页

课程的顺序是按算法排的,不是按 C++ 语法排的,于是有些写法会出现得很早、 讲得很晚。量过一遍:全书有 15 章的正文代码在第 46 章(位运算)之前就用了真位运算 —— 最早是第 2 章的 1LL << n、 第 3 章的 mask >> i & 1,中间隔着四十多章。vector 更极端:从第 2 章起几乎每份代码都有它,而没有任何一章教过它。 这一页就是补这个缺口的。

这一页有什么
  1. 位运算 10 条 · 带可运行示例 · 例题与练习 5 道 →
  2. 整数类型与溢出 6 条 · 带可运行示例 · 例题与练习 5 道 →
  3. 数组与 vector 8 条 · 带可运行示例 · 例题与练习 5 道 →
  4. 字符串 string 9 条 · 带可运行示例 · 例题与练习 5 道 →
  5. 排序与比较器 8 条 · 带可运行示例 · 例题与练习 5 道 →
  6. STL 容器(本书只用到这几个) 8 条 · 带可运行示例 · 例题与练习 5 道 →
  7. 读入与输出 5 条 · 带可运行示例 · 例题与练习 5 道 →
  8. 函数、引用与递归 6 条 · 带可运行示例 · 例题与练习 5 道 →
  9. 零碎但天天用的那几样 8 条 · 带可运行示例 · 例题与练习 5 道 →
  10. 报错了怎么读 7 条 · 例题与练习 5 道 →

1. 位运算

10 条 · 做题 →

缺口最大的一组:全书 15 章的正文代码在第 46 章之前就用了它,最早是第 2 章。

地基只有一句话:一个整数就是一排开关。 我们平时把 13 读成「十三」,而计算机里它是 1101 —— 从右往左数,第 0 号开关开、第 1 号关、第 2 号开、第 3 号开。位运算就是拨这排开关的几个动作,它们不是数学运算,别拿加减乘除的直觉去套。

⚠ 两个编号要一次记牢,后面所有条目都按这个约定:最右边那一位是「第 0 位」(不是第 1 位),而且越往左位数越大。第 i 位代表的数值是 2ⁱ —— 所以 1101 = 8 + 4 + 0 + 1 = 13。

★ 而这排开关彼此没有一根线连着:对第 3 位做的事不会影响第 2 位。这就是为什么「一个集合」可以用「一个整数」表示(第 28 章状压 DP 的全部立论),也是为什么第 46 章那道题能用异或一路消下去。

★★ 优先级是这一组唯一一个会咬人的语法问题:位运算的优先级比比较运算还低。所以凡是位运算和 == / < 写在一起,一律加括号 —— 这条没有例外,看到就加。

  • x >> i & 1
    取出 x 的第 i 位(从右往左数,最右边是第 0 位),结果只会是 0 或 1。
    怎么读分两步读。x >> i 是「整排开关右移 i 格」,把你要的那一位挪到最右边;再 & 1 是「只留最右边那一位,其余全抹掉」。拿 x = 13(1101)、i = 2 走一遍:13 >> 2 = 11 = 3,3 & 1 = 1 ⇒ 第 2 位是 1。✓
    什么时候用凡是「一个整数代表一组选/不选」的地方都要用它。第 3 章那份二进制枚举里,mask >> i & 1 问的就是「第 i 个数选了没有」;第 28 章问的是「第 i 个城市去过没有」。
    讲透它的是 第 46 章
    第 3 章用到 →
  • x & 1
    判奇偶 —— 等于 1 就是奇数。它其实就是「取第 0 位」。
    怎么读为什么成立:二进制里除了最低位,每一位代表的都是 2 的正整数次方,全是偶数。所以整个数的奇偶只由最低位决定 —— 而 & 1 恰好只留最低位。
    什么时候用x % 2 完全等价,写哪个都对。⚠ 但对负数不一样:-3 % 2 是 −1,而 -3 & 1 是 1 —— 判奇偶时后者反而更省心。
    讲透它的是 第 46 章
    第 46 章用到 →
  • 1 << n
    2 的 n 次方。1 后面跟 n 个 0,所以左移一位就是乘 2。
    怎么读1……0001,左移 3 位变成 1000,也就是 8 = 2³。十进制里「后面添个 0 就是乘 10」,二进制里「后面添个 0 就是乘 2」,是同一件事。
    什么时候用两个场合:算「一共有多少种选法」(n 个东西各选/不选 ⇒ 1 << n 种),以及造一个「只有第 i 位是 1」的数去拨那一位。
    ⚠ ⚠ n 到 31 就已经溢出了(1 << 31 是负数),到 32 更是直接未定义。要大的写 1LL << n
    第 3 章用到 →
  • 1LL << n
    同上,但用 long long 算 —— n 超过 30 就得用这一种。
    怎么读1 << n 里的 1int,整句就按 int 算,而 int 只装得下 2³¹−1。1LL 是「long long 类型的 1」,整句就升级成 long long,能到 2⁶³−1。
    什么时候用第 2 章汉诺塔的总步数 2ⁿ − 1,n 可以到 60 ⇒ 非 1LL 不可。判据很简单:n 有没有可能超过 30,有就写 LL。
    第 2 章用到 →
  • x | (1 << i)
    把第 i 位设成 1(其余位不动)。集合里「加入第 i 个元素」。
    怎么读|(或)的规则是「两边只要有一个是 1,结果就是 1」。1 << i 造出一个「只有第 i 位是 1」的数,和 x 一或:第 i 位被顶成 1,其余位是「x 的那一位 | 0」= 原样。例:13 | (1 << 1) = 1101 | 0010 = 1111 = 15。
    什么时候用第 28 章往状态里「加一个已访问的城市」;也常写成 x |= 1 << i
    第 14 章用到 →
  • x & ~(1 << i)
    把第 i 位设成 0。集合里「去掉第 i 个元素」。
    怎么读~ 是「每一位都翻过来」。1 << 20100~(1 << 2) 就是 ……1011 —— 一个「只有第 2 位是 0,其余全是 1」的掩码。再 &(与,两边都是 1 才是 1):第 2 位被按成 0,其余位是「x 的那一位 & 1」= 原样。例:13 & ~(1 << 2) = 1101 & 1011 = 1001 = 9。
    什么时候用第 28 章「从状态里去掉当前城市,回头看上一步是从哪来的」。
    第 28 章用到 →
  • x ^ (1 << i)
    把第 i 位翻过来:0 变 1、1 变 0。
    怎么读^(异或)的规则是「两边不一样才是 1」。和 1 异或必翻,和 0 异或不变 —— 所以只有第 i 位被翻,其余原样。
    ⚠ 本书正文一次都没用到它 —— 列在这儿是为了让那三个动作凑齐一套。
    本书暂未用到
  • x & -x
    lowbit:只留下最低位的那个 1(连同它后面的 0)。
    怎么读-x 在计算机里是「把 x 每一位翻过来再加 1」(这叫补码)。拿 x = 12 = 1100:翻过来是 …0011,加 1 变成 …0100。和原来的 1100 一与,只剩 100 = 4 —— 正是最低位那个 1。⚠ 这一段是本条唯一需要「相信」的地方,第 46 章第 9 步会把补码讲明白。
    什么时候用第 38 章树状数组每一行都在用:i += i & -i 往上跳父节点,i -= i & -i 抹掉最低位那个 1 往前挪。也可以用来「取出集合里最小的那个元素」。
    讲透它的是 第 46 章
    第 4 章用到 →
  • __builtin_popcount(x)
    数一数 x 的二进制里有几个 1。GCC 内置,比赛能用。
    怎么读__builtin_popcount(13) = 3,因为 1101 里有三个 1。它是一条 CPU 指令,比自己写循环数快得多。
    什么时候用状压 DP 里问「这个状态已经访问了几个城市」——集合大小就是 1 的个数,不用另外存。
    ⚠ long long 要用 __builtin_popcountll,写错了只会数低 32 位。
    第 28 章用到 →
  • (x & 1) == 0
    ⚠ 括号不能省:位运算的优先级比比较运算还低。
    怎么读x & 1 == 0,编译器先算 1 == 0 得到 0,整句变成 x & 0 —— 恒等于 0,也就是这个判断永远是假。它不报错、不警告,只是安静地一直答「不是」。
    什么时候用记一条无脑规则就行:位运算只要和 == / < / > 出现在同一个表达式里,就加括号。
    ⚠ 同一类还有一个:a & b == c 也会先算 b == c。而 << 的优先级比 + 还低 —— 1 << n + 11 << (n+1),不是 (1 << n) + 1
    本书暂未用到
bitops.cpp跑一遍看看
// 位运算速查 —— 七个动作,跑一遍就能看见
//
// 为什么这一份排在第一个:本书**第 2 章**就用了 `1LL << n`、
// **第 3 章**的 `bitmask.cpp` 用了 `mask >> i & 1`,
// 而真正把位运算讲透的是**第 46 章** —— 中间隔着四十多章。
// 这一份不讲道理,只把「那几个符号在干什么」摆出来给你看。
//
// 一句话地基:**一个整数就是一排开关。**
// 13 写成二进制是 1101,也就是第 0、2、3 号开关是开的(从右往左数,从 0 开始)。
// 下面七个动作,每一个都只是在拨这排开关。
#include <bits/stdc++.h>
using namespace std;
// 把 x 的低 8 位打成 0/1 串,方便肉眼对照
string bits8(int x) {
string s;
for (int i = 7; i >= 0; i--) s += char('0' + (x >> i & 1));
return s;
}
int main() {
int x = 13; // 13 = 1101
cout << "x = " << x << " 二进制 " << bits8(x) << "\n\n";
// ① 取出第 i 位:先右移 i 位把它挪到最右边,再和 1 做「与」
cout << "① 取第 i 位 x >> i & 1\n";
for (int i = 0; i < 4; i++)
cout << " i=" << i << " -> " << (x >> i & 1) << "\n";
// ② 判奇偶:就是第 0 位
cout << "\n② 判奇偶 x & 1 -> " << (x & 1)
<< " (1 是奇数,0 是偶数)\n";
// ③ 2 的幂:1 左移 n 位
cout << "\n③ 2 的 n 次 1 << n\n";
for (int n = 0; n <= 4; n++) cout << " n=" << n << " -> " << (1 << n) << "\n";
// 注意:1 是 int,只有 31 位可用。要算 2^40 必须写 1LL << 40。
cout << " 1 << 40 是错的(int 装不下),要写 1LL << 40 = " << (1LL << 40) << "\n";
// ④ 把第 i 位设成 1:或上一个「只有第 i 位是 1」的数
cout << "\n④ 置 1 x | (1 << i) i=1 -> " << setw(2) << (x | (1 << 1))
<< " " << bits8(x | (1 << 1)) << "\n";
// ⑤ 把第 i 位设成 0:与上一个「只有第 i 位是 0」的数
cout << "⑤ 清 0 x & ~(1 << i) i=2 -> " << setw(2) << (x & ~(1 << 2))
<< " " << bits8(x & ~(1 << 2)) << "\n";
// ⑥ 把第 i 位翻过来
cout << "⑥ 翻转 x ^ (1 << i) i=0 -> " << setw(2) << (x ^ (1 << 0))
<< " " << bits8(x ^ (1 << 0)) << "\n";
// ⑦ lowbit:最低位的那个 1(连同它后面的 0)。第 38 章树状数组每一行都在用它。
cout << "\n⑦ lowbit x & -x\n";
for (int v : {6, 8, 7, 12})
cout << " " << setw(2) << v << " (" << bits8(v) << ") -> " << (v & -v) << "\n";
// 附:数一个数里有几个 1(GCC 内置,比赛能用)
cout << "\n附 数 1 的个数 __builtin_popcount(" << x << ") = "
<< __builtin_popcount(x) << "\n";
// 最后一个坑:位运算的优先级**比比较运算还低**。
// 想问「y 的第 0 位是不是 0」(也就是 y 是不是偶数),写成 y & 1 == 0 会先算
// 1 == 0 得到 0,整句变成 y & 0 —— 恒等于 0,也就是**永远答「不是」**。括号不能省。
int y = 12; // 12 是偶数,正确答案应该是 1
cout << "\n坑 y = " << y << "\n";
cout << " y & 1 == 0 编译器读成 y & (1 == 0) = " << (y & (1 == 0)) << " <- 错\n";
cout << " (y & 1) == 0 = " << ((y & 1) == 0) << " <- 对\n";
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

2. 整数类型与溢出

6 条 · 做题 →

本书最常咬人的一类 bug,而且它不报错:编译过、样例过、肉眼看也正常,就是答案错。第 11、19、23、35、43 章各栽过一次。

每个整数类型都有一个固定的抽屉大小。装不下的时候,C++ 不会报错、不会提醒,它只是把超出去的部分扔掉,留下一个看起来很正常的错数字 —— 这叫溢出。

★ 记两个数量级就够:int 大约 2.1 × 10⁹(二十一亿),long long 大约 9.2 × 10¹⁸。竞赛题面里常写 1 ≤ aᵢ ≤ 10⁹,单个数 int 装得下 —— 可两个这样的数一乘就是 10¹⁸,当场爆掉

⇒ ★★ 所以动笔之前有一个三十秒的动作:拿题面的上限乘一遍,看最大的那个中间值有多大。不是看输入多大,也不是看答案多大,是看算的过程中出现过的最大的那个数。本书里栽的每一次都栽在这儿。

★ 拿不准就一律 long long,代价几乎为零。只有「开一个几千万个元素的大数组」时,才值得回头精打细算 —— 那时内存翻一倍是真的会 MLE。

  • int
    上限约 2.1 × 10⁹(二十一亿)。题目里的数一旦可能超过它,就得换。
    怎么读精确值是 2 147 483 647(= 2³¹ − 1),最小是 −2 147 483 648。记不住就用 INT_MAX / INT_MIN,它们在 <bits/stdc++.h> 里现成。
    什么时候用循环变量、数组下标、题面上限在 10⁹ 以内的单个数值 —— 这些用 int 就够。
    第 1 章用到 →
  • long long
    上限约 9.2 × 10¹⁸。本书的默认选择 —— 拿不准就用它,代价几乎为零。
    怎么读精确值是 9 223 372 036 854 775 807(= 2⁶³ − 1)。写字面常量要带后缀 LL(如 1LL),用 printf 打它要用 %lld
    什么时候用求和、求乘积、计数类的答案,一律先用它。第 19 章那道题的「总等待时间」顶格是 10¹⁰ 级别 —— 单个数都在 int 里,加起来就不在了。
    讲透它的是 第 11 章
    第 1 章用到 →
  • (long long)a * b
    两个 int 相乘之前,先把一个转成 long long,整句才会按 long long 算。
    怎么读C++ 算一个表达式时,先按两边的类型算完,再赋值a * b 两边都是 int ⇒ 这次乘法就是 int 乘法,10⁵ × 10⁵ = 10¹⁰ 当场溢出,之后再赋给 long long 也救不回来 —— 错的数已经算出来了。前面加一个 (long long),整句就升级成 long long 乘法。
    什么时候用两个下标相乘、两个坐标差相乘(dx*dx + dy*dy)、计数乘计数 —— 都要先过一遍这个动作。
    ⚠ 写 long long c = a * b; 没用 —— 等号右边那次乘法仍然是 int 乘法,溢出已经发生了。
    第 5 章用到 →
  • 0x3f3f3f3f
    当「无穷大」用的那个数(约 1.06 × 10⁹)。
    怎么读为什么不直接用 INT_MAX:最短路里要算 dist[u] + w,而 INT_MAX + 任何正数 当场溢出,绕回成一个负数 —— 于是「走不到的点」反而成了最短的。0x3f3f3f3f 的好处是它自己加自己还不溢出(约 21.2 亿 < 21.47 亿),而且它的每个字节都是 0x3f ⇒ 可以 memset(d, 0x3f, sizeof d) 一次填满整个数组。
    什么时候用第 32、33 章最短路,第 34 章最小生成树,任何「先设成无穷大再不断取 min」的地方。
    ⚠ 选它是因为它自己加自己还不溢出 —— 最短路里 dist[u] + w 要先加再比。
    第 29 章用到 →
  • -7 / 2
    整数除法向零取整,得 −3(不是 −4);-7 % 2 得 −1,取模的符号跟着被除数走。
    怎么读「向零取整」就是把小数部分直接砍掉:−3.5 砍成 −3,而不是往下取到 −4。砍完之后等式仍然成立:(-7)/2*2 + (-7)%2 = −6 + (−1) = −7。
    ⚠ ⚠ 取模题里最容易出事:答案要求非负时 a % p 可能是负的,要写成 (a % p + p) % p 才保险 —— 第 42、43 章都这么写。
    本书暂未用到
  • v.size()
    ⚠ 它是无符号的。空数组上 v.size() - 1 不是 −1,是一个天文数字。
    怎么读「无符号」就是这个类型没有负数,于是 0 − 1 不会变成 −1,而是绕回它能表示的最大值(18 446 744 073 709 551 615)。写 for (int i = 0; i < v.size() - 1; i++),空数组时循环条件成了「0 < 天文数字」⇒ 循环真的跑起来了,然后越界
    什么时候用写「倒数第二个元素」、写「相邻两两比较」的循环时必踩。
    ⚠ 两个改法:(int)v.size() - 1(先转成有符号),或者换成 i + 1 < v.size()(不做减法)。
    本书暂未用到
inttype.cpp跑一遍看看
// 整数类型速查 —— 它装得下多大,以及什么时候会悄悄装不下
//
// 这是本书最常咬人的一类 bug,而且它**不报错**:算出来的数就是错的,
// 编译过、样例过、肉眼看也很正常。第 11、19、23、35、43 章都各栽过一次。
#include <bits/stdc++.h>
using namespace std;
int main() {
// ① 两个类型的上限,背下来这两个数量级就够
cout << "int 上限 " << INT_MAX << " (约 2.1 x 10^9,二十一亿)\n";
cout << "long long 上限 " << LLONG_MAX << " (约 9.2 x 10^18)\n\n";
// ② 最常见的溢出现场:两个 int 相乘。
// a 和 b 各自都装得下,可它们的**积**装不下 —— 而乘法是按 int 算完才赋值的。
int a = 100000, b = 100000;
long long right = (long long)a * b; // 先把一个转成 long long,整句就按 long long 算
cout << "② a = " << a << ", b = " << b << "\n";
cout << " 真正的 a*b = " << right << ",而 int 上限只有 " << INT_MAX << "\n";
cout << " => 写 int c = a * b; 就装不下了;要写 long long c = (long long)a * b;\n";
cout << " ⚠ 只把结果声明成 long long 没用:long long c = a * b; 里那次乘法「仍然是 int 乘法」\n\n";
// ③ 移位同理:1 是 int,1 << 40 装不下
cout << "③ 1LL << 40 = " << (1LL << 40) << " (写成 1 << 40 就错了)\n\n";
// ④ 除法是向零取整,不是向下取整 —— 负数上两者不一样
cout << "④ 7 / 2 = " << 7 / 2 << " 7 % 2 = " << 7 % 2 << "\n";
cout << " -7 / 2 = " << -7 / 2 << " -7 % 2 = " << -7 % 2
<< " (取模的符号跟着被除数走)\n\n";
// ⑤ size() 是**无符号**的,这一条每年都有人栽
vector<int> v; // 空的
cout << "⑤ 空 vector 的 v.size() = " << v.size() << "\n";
cout << " 而 v.size() - 1 = " << v.size() - 1 << " <- 不是 -1,是一个天文数字\n";
cout << " => 写循环边界要么用 (int)v.size() - 1,要么换成 i + 1 < v.size()\n";
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

3. 数组与 vector

8 条 · 做题 →

从第 2 章起几乎每份代码都有 vector,而没有任何一章教过它。

你已经会写 int a[100]; 了。vector 就是同一个东西,只多给你三件事:长度可以是运行时才知道的变量可以随时往后加它自己知道自己有多长。除此之外用法一模一样 —— 照样 a[0]a[i],下标照样从 0 开始。

★ 为什么本书几乎全用它而不用裸数组:一是题目的 n 是读进来的,int a[n]; 在标准 C++ 里其实不合法(GCC 容忍而已);二是 vector<int> a(n); 保证全是 0,而函数里的 int a[100]; 里面是垃圾值。

⚠ 但有一个场合反过来用裸数组:几百万个元素的大数组要开成全局的。写在 main 里的数组放在栈上(一般只有 8 MB),开大了直接段错误;全局数组不占栈,而且自动清零。竞赛代码里 const int N = 100005; int a[N]; 就是这个习惯。

★ 还有一条贯穿全书的:数组越界 C++ 不会报错。 它只是安静地读写了别人的内存,于是程序可能在一个毫不相干的地方崩掉,或者答案莫名其妙 —— 查这类问题的第一招是临时把 a[i] 换成 a.at(i)

  • vector<int> a(n)
    开一个长度为 n 的数组,全部初始化成 0。n 可以是运行时才知道的变量。
    怎么读读成「一个装 int 的 vector,名字叫 a,长度 n」。尖括号里写的是装什么,括号里写的是装多少个。换成 vector<long long> a(n) 就是装 long long。
    什么时候用读完 n 之后开数组,这是本书每道题的第一句。
    第 2 章用到 →
  • vector<int> a(n, 7)
    同上,但全部填 7。
    怎么读第二个参数是「每一格的初值」。不写就是 0。
    什么时候用最短路里 vector<int> dist(n, 0x3f3f3f3f); —— 一句话把整张表设成无穷大。
    本书暂未用到
  • a.size() / a.push_back(x) / a.back()
    有多长 / 往后加一个 / 最后一个元素。
    怎么读push_back 把 x 接在末尾,长度自动 +1 —— 不用你先想好开多大。a.back() 就是 a[a.size()-1]a.pop_back() 则是删掉最后一个。
    什么时候用搜索时往路径里记一步(path.push_back(v)),回溯时再 pop_back() 撤销 —— 第 3、4 章那套「进入 → 递归 → 撤销」就是这两句。
    第 3 章用到 →
  • vector<vector<int>> f(R, vector<int>(C, 0))
    R 行 C 列的二维数组,全 0。
    怎么读从里往外读:vector<int>(C, 0) 是「一行,C 个 0」;外面那层是「R 个这样的行」。取元素照样 f[i][j]
    什么时候用DP 的表格(第 21 章起每一章都是它)、网格图、邻接矩阵。
    ⚠ ⚠ 行列别写反:f(R, vector<int>(C)) 的第一个数是行数。写反了不一定崩,只是答案错 —— 而且小数据上常常正好是方阵,看不出来。
    第 6 章用到 →
  • for (int x : a)
    范围 for:把 a 里每个元素依次取出来叫 x。
    怎么读它等价于 for (int i = 0; i < a.size(); i++) { int x = a[i]; … },只是不用管下标。⚠ 注意 x一份复制品
    什么时候用只读地遍历一遍(求和、打印)时最省事;要用到下标就老老实实写 for (int i…)
    ⚠ 要改元素得写 for (int &x : a) —— 少一个 & 就只是改了一份复制品。
    本书暂未用到
  • 全局数组 vs 局部数组
    全局数组自动全是 0,函数里的 int t[5]; 不初始化就是垃圾值。
    怎么读写在所有函数外面的叫全局,程序启动时由系统一次性清零;写在函数里的在栈上,那块内存上次谁用过就留着谁的数据。
    什么时候用⚠ 大数组(几十万以上)一律全局,否则爆栈;⚠ 而多组数据的题里,全局数组每组之间要自己清,它只在启动时清过一次。
    ⚠ 「第一次跑对、第二次跑错」十有八九是这个。vector<int> a(n); 则保证全 0。
    本书暂未用到
  • memset(a, 0, sizeof a)
    按字节填。只认三个值:0、−1、0x3f。
    怎么读它填的单位是字节不是元素。一个 int 有 4 个字节,memset(a, 1, …) 是把每个字节都填成 1 ⇒ 每个 int 变成 0x01010101 = 16843009,而不是 1。只有 0(四个 0 还是 0)、−1(四个 0xff 还是 −1)、0x3f(拼出 0x3f3f3f3f)这三个刚好合用。
    什么时候用一次性把整张 dist 表设成无穷大:memset(d, 0x3f, sizeof d);。要填别的值就用 fill(a, a + n, v)vector 的构造参数。
    memset(a, 1, ...) 得到的不是 1,是 16843009 —— 它填的是每个字节。
    第 29 章用到 →
  • a.at(i)
    带越界检查的下标。a[i] 越界不会报错,只会安静地读写别人的内存。
    怎么读at 每次都先检查下标在不在范围内,越界就抛异常、程序当场停在那一行;[] 不检查,所以更快,但错了没人告诉你。
    什么时候用⚠ 只在排查 bug 时临时换成 at,查完换回去 —— 它在内层循环里是真的慢。
    ⚠ 查这类问题时临时换成 at,越界会当场抛异常;查完再换回去(at 更慢)。
    本书暂未用到
vec.cpp跑一遍看看
// 数组与 vector 速查 —— 本书从第 3 章起几乎每份代码都有它
//
// vector 就是「长度可以在运行时决定的数组」。
// 你已经会写 int a[100]; 了,那么 vector 只多给你三件事:
// 长度可以是变量、可以随时往后加、以及它自己知道自己有多长。
#include <bits/stdc++.h>
using namespace std;
int g[5]; // 全局数组:自动全是 0
int main() {
int n = 5;
// ① 开一个长度为 n 的数组,全部初始化成 0
vector<int> a(n); // a[0..4],全是 0
vector<int> b(n, 7); // 全是 7
cout << "① a = "; for (int x : a) cout << x << ' '; cout << "\n";
cout << " b = "; for (int x : b) cout << x << ' '; cout << "\n\n";
// `for (int x : a)` 叫范围 for,意思是「把 a 里每个元素依次取出来叫 x」。
// 要**改**元素得写 for (int &x : a)(加一个 &,见 funcref.cpp)。
// ② 下标、长度、往后加
a[0] = 3; a[4] = 9;
a.push_back(100); // 长度变成 6
cout << "② a.size() = " << a.size() << " a[0] = " << a[0]
<< " 最后一个 a.back() = " << a.back() << "\n\n";
// ③ 二维:一个「元素是 vector 的 vector」
int R = 3, C = 4;
vector<vector<int>> f(R, vector<int>(C, 0)); // R 行 C 列,全 0
f[1][2] = 5;
cout << "③ 二维 f[1][2] = " << f[1][2] << ",共 " << f.size()
<< " 行 " << f[0].size() << " 列\n\n";
// ④ 全局数组自动清零,局部的**不会** —— 这是最常见的「第一次跑对、第二次跑错」
cout << "④ 全局 g[3] = " << g[3] << " (全局数组一定是 0)\n";
cout << " 而函数里写 int t[5]; 不初始化的话,里面是垃圾值\n";
cout << " vector<int> a(n); 则保证全 0 —— 这也是本书偏爱它的原因之一\n\n";
// ⑤ memset 只认三个值:0、-1、0x3f。它是按**字节**填的,不是按元素。
int m[4];
memset(m, 0, sizeof(m));
cout << "⑤ memset(m, 0, sizeof m) -> " << m[0] << "\n";
memset(m, 0x3f, sizeof(m));
cout << " memset(m, 0x3f, ...) -> " << m[0]
<< " (一个「很大但加一倍也不溢出」的数,常当无穷大用)\n";
cout << " ⚠ memset(m, 1, ...) 得到的不是 1,是 " << (memset(m, 1, sizeof(m)), m[0]) << "\n\n";
// ⑥ 越界不会报错 —— 它只是安静地读写了别人的内存
cout << "⑥ a 只有 " << a.size() << " 个元素,写 a[100] = 1 不会有任何提示,\n";
cout << " 但程序可能在很远的地方崩掉,或者答案莫名其妙。\n";
cout << " 查这类问题可以临时把 a[i] 换成 a.at(i):越界会当场抛异常。\n";
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

4. 字符串 string

9 条 · 做题 →

和 vector 一样的处境:第 1 章的代码里就有它,而第 47 章才正式讲字符串。

string 就是「一排 char」,而且它自己知道自己有多长。你可以像数组一样 s[0]s[i] 地取,也可以整个拿来比大小、拼接、当函数的返回值 —— 这比 C 语言那种 char s[100] 省心得多。竞赛里除非卡常,一律用它。

★ 最该先记牢的一条是「字符也是整数」char 在 C++ 里本来就是一个小整数,'a' 是 97、'0' 是 48。所以 c - '0' 把字符 '7' 变成数字 7,c - 'a' 把字母变成 0~25 的编号 —— 后者是开计数数组 cnt[26] 的前提,第 47 章起几乎每道字符串题都要用。

⚠ 而字符串比大小是字典序,不是数值:"100" < "99"的(因为 '1' < '9')。第 44 章高精度里那个「比大小要先比位数」的坑,根子就在这儿。

  • string s; cin >> s;
    读一个不带空格的串。
    怎么读>> 读到空格或换行就停 —— 所以它读的是「一个词」,不是「一行」。要读一整行用 getline
    什么时候用读单词、读一串没有空格的数字字符、读一行地图。
    第 3 章用到 →
  • s.size() / s[i] / s.back()
    长度 / 第 i 个字符(从 0 数)/ 最后一个字符。
    怎么读和数组一模一样。遍历可以写 for (int i = 0; i < (int)s.size(); i++),也可以写 for (char c : s)
    ⚠ ⚠ s.size() 是无符号的 —— 和 int i 比较时编译器会警告,空串上 s.size() - 1 更是一个天文数字。习惯写 (int)s.size()
    第 1 章用到 →
  • s + t s += c
    拼接。+ 造一个新串,+= 接在自己后面。
    怎么读+= 快得多:s = s + c 会把整个 s 复制一遍再加一个字符,在循环里就是 O(n²)。拼长串一律用 +=
    什么时候用把一个数字逐位拼成字符串、把答案一行一行攒起来最后一次输出。
    本书暂未用到
  • s.substr(i, len)
    截一段:从下标 i 开始、取 len 个字符。
    怎么读只写一个参数 s.substr(i) 就是「从 i 到结尾」。例:"hello world".substr(6, 5) = "world"
    什么时候用第 18 章字串变换、第 49 章哈希里取一段子串。
    ⚠ ⚠ 第二个参数是长度,不是「结束下标」—— 这是最常见的 off-by-one 来源。
    第 18 章用到 →
  • s == t s < t
    == 比内容(不是地址),< 是字典序。
    怎么读字典序就是「逐位比,第一个不同的位说了算;都相同则短的在前」。所以 "abc" < "abd",而 "ab" < "abc"
    ⚠ ⚠ "100" < "99" 是真的 —— 字符串比大小和数字比大小是两回事。第 44 章高精度比大小必须先比位数,原因就在这儿。
    本书暂未用到
  • c - '0' c - 'a'
    字符 → 整数。前者把 '7' 变成 7,后者把字母变成 0~25。
    怎么读char 本来就是一个小整数(ASCII 码):'0' 是 48、'7' 是 55,两者一减正好是 7。字母同理,'a' 是 97。反过来 (char)('a' + 3) 就是 'd'
    什么时候用开计数数组的前提int cnt[26]; cnt[c - 'a']++;。第 47 章起到处都是。
    第 6 章用到 →
  • s.find(t)
    找子串,返回第一次出现的下标;找不到返回 string::npos
    怎么读string::npos 是一个很大的数(不是 −1)。⚠ 判「找到没有」要写 != string::npos,写成 > 0 会漏掉「在开头找到」这一种。
    什么时候用第 47 章统计单词出现次数。⚠ 它是朴素查找,最坏 O(nm) —— 大数据要上 KMP(第 48 章)。
    第 7 章用到 →
  • to_string(x) stoi(s)
    数字 ↔ 字符串互转。
    怎么读to_string(255) 得到 "255"stoi("255") 得到 255。大数用 stoll(转 long long)。
    什么时候用第 15 章把一个棋盘状态压成字符串当 map 的键。
    第 2 章用到 →
  • reverse(s.begin(), s.end())
    原地翻转整个串。
    怎么读和 vector 用的是同一个 reverse —— 它对任何「一段区间」都管用。
    什么时候用第 44 章高精度:数要倒着存(个位在下标 0),读进来之后第一件事就是翻转。
    第 10 章用到 →
str.cpp跑一遍看看
// 字符串 string 速查 —— 本书第 1 章就在用它,而第 47 章才正式讲字符串
//
// 一句话地基:**string 就是「一排 char」,而且它自己知道自己有多长。**
// 你可以像数组一样 s[0]、s[1] 地取,也可以整个拿来比大小、拼接、当返回值。
// 这比 C 语言的 char 数组省心得多,竞赛里除非卡常,否则一律用 string。
#include <bits/stdc++.h>
using namespace std;
int main() {
string s = "hello";
// ① 长度和取字符 —— 和数组一模一样,下标从 0 开始
cout << "① s = \"" << s << "\"\n";
cout << " s.size() = " << s.size() << " s[0] = " << s[0]
<< " s[4] = " << s[4] << " 最后一个 s.back() = " << s.back() << "\n";
cout << " 逐个字符:";
for (int i = 0; i < (int)s.size(); i++) cout << s[i] << ' ';
cout << "\n (也可以写 for (char c : s))\n\n";
// ② 拼接:+ 和 += 都行,char 也能直接加上去
string t = s + " world";
t += '!';
cout << "② s + \" world\" 再 += '!' -> \"" << t << "\"\n\n";
// ③ 截一段:substr(起点, 长度)。只写起点就是「从这儿到结尾」
cout << "③ t.substr(6, 5) = \"" << t.substr(6, 5) << "\" (从下标 6 起,取 5 个)\n";
cout << " t.substr(6) = \"" << t.substr(6) << "\" (从下标 6 到结尾)\n\n";
// ④ 比大小:== 比内容(不是地址),< 是**字典序**
cout << "④ (\"abc\" == \"abc\") = " << (string("abc") == string("abc")) << "\n";
cout << " (\"abc\" < \"abd\") = " << (string("abc") < string("abd")) << " 字典序,逐位比\n";
cout << " ⚠ 而 (\"100\" < \"99\") = " << (string("100") < string("99"))
<< " —— 字符串比大小和数字比大小是两回事\n\n";
// ⑤ 找子串:找到返回下标,找不到返回 string::npos(一个很大的数)
size_t pos = t.find("world");
cout << "⑤ t.find(\"world\") = " << pos << "\n";
cout << " t.find(\"xyz\") 找不到,等于 string::npos 吗 -> "
<< (t.find("xyz") == string::npos) << "\n";
cout << " ⚠ 判「找到没有」要写 != string::npos,别写 > 0(下标 0 也是找到了)\n\n";
// ⑥ 字符也是整数 —— 这是竞赛里最常用的一个转换
char c = '7';
cout << "⑥ '7' - '0' = " << c - '0' << " (字符数字 -> 整数)\n";
char d = 'e';
cout << " 'e' - 'a' = " << d - 'a' << " (字母 -> 0~25 的编号,开计数数组要用)\n";
cout << " (char)('a' + 3) = " << (char)('a' + 3) << " (编号 -> 字母)\n\n";
// ⑦ 和数字互转
cout << "⑦ to_string(255) = \"" << to_string(255) << "\"\n";
cout << " stoi(\"255\") + 1 = " << stoi("255") + 1 << "\n\n";
// ⑧ 整串翻转 —— 和 vector 用的是同一个 reverse
string r = s;
reverse(r.begin(), r.end());
cout << "⑧ reverse 之后 \"" << s << "\" -> \"" << r << "\"\n";
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

5. 排序与比较器

8 条 · 做题 →

第 10 章才正式教排序,可从第 2 章起就在用 sort。

要记的只有一件事:sort 不知道「谁该排前面」,那是你告诉它的。不告诉它,它就按 < 排(数字从小到大、字符串按字典序)。

★ 它要的两个参数是「从哪儿开始」和「到哪儿结束」,而结束那个是「最后一个的下一格」。所以整个 vector 是 a.begin(), a.end(),裸数组是 arr, arr + n;只排前 5 个就是 a.begin(), a.begin() + 5。这个「左闭右开」的习惯贯穿整个 STL。

⚠ 自己写比较规则时,函数返回的是「a 该排在 b 前面吗」,不是「谁更大」。想降序就写 return a > b; —— 读成「a 比 b 大时,a 排前面」。

★★ 而这里有一条会让程序直接崩溃的规矩:比较函数里绝对不能写 >=<=。sort 要求「两个元素一样时必须返回 false」,写成 >= 时 a 和 b 相等也返回 true,sort 内部就会越过数组边界一直找下去。⚠ 而小数据上它常常正好不崩,第 10 章量过这件事。

  • sort(a.begin(), a.end())
    升序排整个 vector。
    怎么读a.begin() 指向第一个元素,a.end() 指向最后一个的下一格(不是最后一个)。所以这一对正好圈住整个数组。复杂度 O(n log n)。
    什么时候用贪心题的第一步几乎都是它(第 19、20 章);二分查找之前也必须先排好。
    第 3 章用到 →
  • sort(arr, arr + n)
    裸数组的写法:首地址、尾后地址。
    怎么读裸数组没有 .begin(),但数组名本身就当「第一个元素的地址」用,arr + n 就是「第 n 格的地址」= 尾后。和上一条完全等价。
    ⚠ ⚠ 如果下标是从 1 开始存的(a[1..n]),要写 sort(a + 1, a + n + 1)。写成 sort(a, a + n) 会把没用的 a[0] 排进去、漏掉 a[n]
    本书暂未用到
  • sort(a.begin(), a.end(), greater<int>())
    降序。
    怎么读第三个参数就是「比较规则」。greater<int>() 是 STL 给好的「大的排前面」。⚠ 后面那对空括号不能少 —— 它要的是一个对象,不是类型名。
    什么时候用也可以不用它:sort 完之后 reverse(a.begin(), a.end()) 一样。
    第 16 章用到 →
  • struct
    把几个变量打包成一个。存一条边、一个学生、一个状态都用它。
    怎么读struct Edge { int u, v, w; }; 定义了一个新类型,之后 Edge e; e.u = 1; 或者 vector<Edge> es;。⚠ 花括号后面那个分号不能漏。初始化可以直接写 Edge e = {1, 2, 5};,顺序就是声明的顺序。
    什么时候用第 34 章存边(起点、终点、边权三个数要一起排序)、第 10 章存学生(名字 + 分数)。
    讲透它的是 第 29 章
    第 10 章用到 →
  • [](const T &a, const T &b) { return ...; }
    lambda:就地写一个没名字的小函数,交给 sort 当比较规则。
    怎么读[] 是它的开头标记(意思是「不捕获外面的变量」),后面的括号是参数、花括号是函数体。整个东西可以直接当参数塞进 sort 的第三个位置,不用另外在外面定义一个函数。例:sort(v.begin(), v.end(), [](const Node &a, const Node &b){ return a.w < b.w; });
    什么时候用按结构体的某个字段排序时。要用到外面的变量就把 [] 写成 [&]
    ⚠ 返回的是「a 该排在 b 前面吗」,不是「谁更大」。
    第 6 章用到 →
  • 比较器里不许写 >=
    sort 要的是严格小于:a 和 b 一样时必须返回 false。
    怎么读sort 内部靠「总能找到一个比我小的」来停下来。如果 a == b 时你也说「a 排前面」,它就会一直往前找、越过数组边界 —— 表现是 Segmentation fault 或者莫名其妙的结果。
    什么时候用要「分数相同时按学号排」,不是改成 >=,而是再比一个字段if (a.score != b.score) return a.score > b.score; return a.id < b.id;
    ⚠ 写成 >= 遇到并列可能直接崩(越界读),而小数据上看着完全正常 —— 第 10 章量过。
    讲透它的是 第 10 章
    本书暂未用到
  • stable_sort
    sort 不稳定:并列的两个元素谁在前不确定。要保住输入顺序用它。
    怎么读「稳定」的意思是:比较规则判不出高下的两个元素,排完之后相对顺序和原来一样sort 不保证这件事,stable_sort 保证(代价是稍慢一点、要额外内存)。
    什么时候用题面写着「分数相同时按输入顺序输出」 —— 要么用 stable_sort,要么在比较规则里把输入序号也写进去(后者更稳,推荐)。
    ⚠ 第 45 章量过一次反过来的:同一个「漏掉输入顺序」的疏忽,配 sort 是 WA,配(稳定的)冒泡反而 AC。
    第 10 章用到 →
  • pair<int, int>
    两个值打包。.first / .second 取出来。
    怎么读struct 省事,代价是字段没有名字。造一个用 {a, b}make_pair(a, b)vector<pair<int,int>> v; v.push_back({1, 2});
    什么时候用Dijkstra 的堆里放「(距离, 编号)」、存图时放「(邻居, 边权)」。
    ⚠ 它自带比较规则:先比 first,first 一样再比 second —— 所以「按 x 排、x 同按 y 排」根本不用写比较器。⚠ 而谁放 first 是约定不是类型,写反了编译器一声不吭。
    第 14 章用到 →
sortcmp.cpp跑一遍看看
// 排序与比较器速查 —— 本书第 10 章正式教排序,但从第 2 章起就在用 sort
//
// 你要记的只有一件事:sort 不知道「谁该排前面」,那是你告诉它的。
// 不告诉它,它就按 < 来排。
#include <bits/stdc++.h>
using namespace std;
struct Student { // struct 就是「把几个变量打包成一个」
string name;
int score;
};
int main() {
// ① 最常见的两种写法:vector 用 begin/end,裸数组用指针
vector<int> v = {5, 2, 9, 1};
sort(v.begin(), v.end());
cout << "① 升序 "; for (int x : v) cout << x << ' '; cout << "\n";
int arr[4] = {5, 2, 9, 1};
sort(arr, arr + 4); // 裸数组:首地址 和 尾后地址
cout << " 裸数组 "; for (int x : arr) cout << x << ' '; cout << "\n\n";
// ② 降序:给它第三个参数
sort(v.begin(), v.end(), greater<int>());
cout << "② 降序 "; for (int x : v) cout << x << ' '; cout << "\n\n";
// ③ 按自己的规则排:写一个「a 该排在 b 前面吗」的函数。
// [](...){...} 这个写法叫 lambda,可以理解成「就地写一个没名字的小函数」。
vector<Student> s = {{"li", 90}, {"wang", 75}, {"zhao", 90}, {"sun", 60}};
sort(s.begin(), s.end(), [](const Student &a, const Student &b) {
return a.score > b.score; // 分数高的排前面
});
cout << "③ 按分数从高到低\n";
for (auto &t : s) cout << " " << setw(5) << t.name << " " << t.score << "\n";
cout << "\n";
// ④ 两条规矩,每一条都真的会咬人:
//
// (a) 比较函数里**不能写 >=**。sort 要求「严格小于」:a 和 b 一样时必须返回 false。
// 写成 a.score >= b.score,遇到并列时程序可能直接崩(越界读),
// 而小数据上它看着完全正常 —— 第 10 章量过这件事。
//
// (b) sort **不稳定**:分数一样的两个人,谁在前面是不确定的。
// 上面 li 和 zhao 都是 90 分,顺序不保证和输入一致。
// 要保住输入顺序,要么用 stable_sort,要么把「输入序号」也写进比较规则。
vector<Student> s2 = {{"li", 90}, {"wang", 75}, {"zhao", 90}, {"sun", 60}};
stable_sort(s2.begin(), s2.end(), [](const Student &a, const Student &b) {
return a.score > b.score;
});
cout << "④ stable_sort:并列时保住输入顺序\n";
for (auto &t : s2) cout << " " << setw(5) << t.name << " " << t.score << "\n";
cout << "\n";
// ⑤ pair 自带比较规则:先比 first,first 一样再比 second。
// 所以「按 x 排,x 一样按 y 排」根本不用写比较器。
vector<pair<int, int>> p = {{2, 5}, {1, 9}, {2, 1}};
sort(p.begin(), p.end());
cout << "⑤ pair 默认先比 first 再比 second\n ";
for (auto &q : p) cout << '(' << q.first << ',' << q.second << ") ";
cout << "\n";
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

6. STL 容器(本书只用到这几个)

8 条 · 做题 →

不用通读 STL 文档。这一组就是这本书从头到尾用到的全部,而且每个容器只用两三个动作。

这五个容器解决的是同一类问题:我要把一堆东西存起来,然后按某种规矩取出来。差别只在「按什么规矩取」—— 谁先进先出、谁后进先出、谁永远给最大的、谁能快速查在不在。

★ 一张表就能选完:要「一层一层地扩散」用 queue(BFS);要「走到底再退回来」用 stack(不过 DFS 直接写递归就行,递归自带栈);要「每次拿当前最小/最大的」用 priority_queue(Dijkstra、合并果子);要「查一个东西在不在、而且要去重」用 set要「按一个键查一个值」用 map

⚠ 代价要知道:queue / stack / vector 的动作都是 O(1),而 priority_queue / set / map 的每一次操作是 O(log n) —— 在 10⁵ 次操作的题里无所谓,在 10⁷ 次的内层循环里就会成为瓶颈。

★ 还有一条新手常卡的语法:queue<int> q; 里的尖括号写「装什么」,所以装 pair 就是 queue<pair<int,int>> q;,装结构体就是 queue<Node> q;

  • queue<int>
    先进先出。BFS 全靠它。动作只有 push / front / pop / empty。
    怎么读想成排队买票:push 是来一个人站到队尾,front 是看看排头是谁,pop 是排头走人(⚠ 它不返回那个值,要先 front 拿到再 pop)。
    什么时候用BFS 的骨架永远是这五句:起点 push → while (!q.empty())front 取出 → pop → 把邻居 push 进去。第 14 章起每一章都是它。
    第 14 章用到 →
  • stack<int>
    后进先出。动作是 push / top / pop / empty —— 取元素叫 top 不叫 front。
    怎么读想成一摞盘子:只能往最上面放,也只能从最上面拿。
    什么时候用第 35 章的单调栈。⚠ 而 DFS 不需要它 —— 递归函数自带的调用栈就是它,只有在「递归太深会爆栈、必须手动改成循环」时才会自己写一个。
    第 35 章用到 →
  • priority_queue<int>
    每次取出最大的(大根堆)。
    怎么读它不是排好序的 —— 它只保证堆顶是最大的那个pushpop 都是 O(log n),取 top 是 O(1)。⚠ 取元素同样叫 top,而且 pop 不返回值。
    什么时候用第 37 章合并果子、第 32 章 Dijkstra。凡是「反复取当前最优的那个」就是它。
    讲透它的是 第 32 章
    第 31 章用到 →
  • priority_queue<int, vector<int>, greater<int>>
    小根堆。这一长串背下来就行 —— Dijkstra 每次都要写。
    怎么读三个参数分别是「装什么」「底下用什么容器装」「怎么比」。中间那个 vector<int> 是默认值,只是因为要写第三个参数,才不得不把它也补上。⚠ 两个 >> 中间在 C++11 之后不用空格了,但写成 > > 也对。
    什么时候用Dijkstra 里放 pair 时是 priority_queue<pair<int,int>, vector<pair<int,int>>, greater<>>。★ 另一个偷懒写法:把值取负塞进大根堆,取出来再取负 —— 效果一样,少写一串。
    第 32 章用到 →
  • set<int>
    自动去重、自动有序。查在不在用 count,O(log n)。
    怎么读s.insert(x) 插入(已经有了就什么都不做),s.count(x) 返回 0 或 1,s.erase(x) 删掉。遍历 for (int x : s) 出来是从小到大的。
    什么时候用「这个状态之前访问过吗」(第 15 章八数码)、「把重复的去掉」。⚠ 要允许重复就用 multiset
    第 29 章用到 →
  • map<K, V>
    一张「键 → 值」的表,键自动有序。cnt[w]++ 时键不存在会自动新建、值从 0 开始。
    怎么读当成「下标可以是任意类型的数组」来用:map<string,int> cnt; cnt["ab"]++;。遍历用 for (auto &kv : mp)kv.first 是键、kv.second 是值。
    什么时候用数每个单词出现几次;给一堆很大的编号做映射(不想开一个 10⁹ 的数组时)。
    ⚠ ⚠ 光是读一下 cnt["zz"] 也会顺手把这个键建出来 —— 只想查在不在就用 count
    第 15 章用到 →
  • unordered_map<K, V>
    不排序的 map,平均更快。顺序不确定,别指望遍历出来是有序的。
    怎么读它用哈希表实现,平均 O(1);map 用平衡树,稳定 O(log n)。用法完全一样,换个名字就行。
    什么时候用只查不遍历、而且数据量大时用它。⚠ 但它最坏会退化成 O(n),有些出题人会专门卡这个 —— 稳妥起见比赛里 map 更保险。
    第 15 章用到 →
  • auto
    「类型你自己推,我不写了」。最常见于 for (auto &kv : mp)
    怎么读编译器看等号右边是什么类型,就把 auto 换成什么。auto x = 1; 里 x 是 int;for (auto &kv : mp) 里 kv 是那一对键值。
    什么时候用类型名很长的时候(pair<int,int>、迭代器)。⚠ 加 & 是为了不复制一份 —— 遍历一个装 string 的容器时,少这个 & 会白白复制每个串。
    第 14 章用到 →
stlbox.cpp跑一遍看看
// STL 容器速查 —— 本书只用到五个,而且每个只用两三个动作
//
// 不用通读 STL 文档。下面这张单子就是这本书从头到尾用到的全部。
#include <bits/stdc++.h>
using namespace std;
int main() {
// ① queue:先进先出。BFS(第 14 章起)全靠它。
queue<int> q;
q.push(1); q.push(2); q.push(3);
cout << "① queue 先进先出:";
while (!q.empty()) { cout << q.front() << ' '; q.pop(); }
cout << "\n 用到的动作只有 push / front / pop / empty\n\n";
// ② stack:后进先出。DFS 改成迭代写法时用(第 13 章)。
stack<int> st;
st.push(1); st.push(2); st.push(3);
cout << "② stack 后进先出:";
while (!st.empty()) { cout << st.top() << ' '; st.pop(); }
cout << "\n 动作是 push / top / pop / empty —— 注意取元素叫 top 不叫 front\n\n";
// ③ priority_queue:每次取出**最大**的。第 32 章 Dijkstra、第 37 章堆都要它。
priority_queue<int> big;
for (int x : {3, 1, 4, 1, 5}) big.push(x);
cout << "③ priority_queue 默认大根堆:";
while (!big.empty()) { cout << big.top() << ' '; big.pop(); }
cout << "\n";
// 要每次取**最小**的,就得把那一长串类型写全 —— 背下来就行:
priority_queue<int, vector<int>, greater<int>> small;
for (int x : {3, 1, 4, 1, 5}) small.push(x);
cout << " 小根堆 priority_queue<int, vector<int>, greater<int>>:";
while (!small.empty()) { cout << small.top() << ' '; small.pop(); }
cout << "\n\n";
// ④ set:自动去重 + 自动有序。查一个数在不在,O(log n)。
set<int> s = {5, 2, 5, 9};
cout << "④ set 去重且有序:";
for (int x : s) cout << x << ' ';
cout << "\n s.count(5) = " << s.count(5) << "(在不在,只会是 0 或 1)"
<< " s.size() = " << s.size() << "\n\n";
// ⑤ map:一张「键 -> 值」的表,键自动有序。
map<string, int> cnt;
for (string w : {"ab", "cd", "ab"}) cnt[w]++; // 键不存在时自动新建,值从 0 开始
cout << "⑤ map 数个数:";
for (auto &kv : cnt) cout << kv.first << '=' << kv.second << " ";
cout << "\n ⚠ 光是写 cnt[\"zz\"] 去读它,也会「顺手把这个键建出来」 —— "
<< "只想查在不在就用 cnt.count(\"zz\")\n";
cout << " 现在 cnt.size() = " << cnt.size() << ",读一下 cnt[\"zz\"]="
<< cnt["zz"] << " 之后 size 变成 " << cnt.size() << "\n\n";
// ⑥ auto 和范围 for:上面已经用了好几次
// `auto &kv : cnt` 的意思是「依次取出 cnt 里每一项,类型你自己推,我不写了」。
// 加 & 是为了不复制一份(大对象上这很值钱),顺便也能就地修改。
vector<int> v = {1, 2, 3};
for (int &x : v) x *= 10; // 加 & 才改得动原来的
cout << "⑥ for (int &x : v) x *= 10 -> ";
for (int x : v) cout << x << ' ';
cout << "\n";
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

7. 读入与输出

5 条 · 做题 →

第 1 章的解析代码就在用关同步那两行,而真正解释它的是第 45 章第 11 步。

这一组里每一条都被本书量过,不是传说。而要先分清两件常被混在一起的事:「读对」和「读快」。 读错了是 WA(比如 getline 吃到一个空行),读慢了是 TLE —— 两者的症状完全不同,救法也完全不同。

★ 先说「读快」这一半:什么时候要管它,是一道算术题。数一数这道题总共要读多少个数:10⁶ 个以内,默认 cin 就够(约 0.16 秒);到 10⁷ 个,就必须关同步(第 6 章那道题读 47 MB,不关同步会超时)。⇒ 不到这个量级就别折腾,写快读反而容易写错。

★ 再说「读对」:>>自动跳过空格和换行,所以读一串数字从来不用管格式。但 getline 不跳 —— 它从当前位置一直读到换行为止。两者混用时,前一个 >> 留下的那个换行就会被 getline 当成「一个空行」读走。这是本组最常见的坑。

  • ios::sync_with_stdio(false); cin.tie(nullptr);
    关掉 cin 和 scanf 的同步,读入能快好几倍。
    怎么读C++ 的 cin 默认要和 C 的 scanf 共用同一套缓冲,保证两者混着用也不乱序 —— 这份保证很贵。第一句把它断开,cin 就用自己的缓冲。第二句是解开 cincout 的绑定(默认每次 cin 前都会先把 cout 刷出去)。⚠ 两句都写在 main 的第一行,写在读入之后就没用了。
    什么时候用读的数超过 10⁶ 个就写上。反正代价为零,本书的习惯是一律写
    ⚠ 关了之后就不要再混用 scanf / printf。第 41 章量到读这一侧值 6.2 倍,而写那一侧只值 1.1 倍 —— 两条并排的建议,分量差一个数量级。
    讲透它的是 第 45 章
    第 1 章用到 →
  • '\n' 而不是 endl
    endl 除了换行还会强制刷新缓冲区,输出量大时慢很多。
    怎么读「刷新缓冲区」就是「立刻把攒着的字都真正写出去」。正常情况下攒一批一起写最快,而 endl 每一行都强制写一次。输出十万行就是十万次系统调用。
    什么时候用只有在「程序可能中途崩掉、想看到已经打出来的内容」时才需要 endl。竞赛里一律 '\n'
    ⚠ 第 41 章和第 48 章各量过一次,都是 8.8 倍。
    本书暂未用到
  • while (cin >> x)
    读到文件结尾。题目不告诉你有几个数时用它。
    怎么读cin >> x 这个表达式本身有一个值:读成功了是「真」,读到头了是「假」。所以可以直接拿它当循环条件。
    什么时候用第 22 章导弹拦截那种「输入不给个数」的题。⚠ 本地测试时要用 Ctrl+D(Linux)告诉它输入结束,不然它一直等。
    第 24 章用到 →
  • getline(cin, s)
    读一整行(含空格)。
    怎么读cin >> s 遇到空格就停,所以读「一行带空格的句子」只能用 getline。它从当前位置一直读到换行,并把那个换行吃掉但不放进 s
    什么时候用读一整行英文句子、读一整行地图。
    ⚠ ⚠ 紧跟在 cin >> n 后面用会读到一个空串 —— 因为 >> 停在数字后面,那一行的换行还留在输入里,getline 当场就撞上它。改法:中间加一句 cin.ignore();(扔掉一个字符)。
    第 47 章用到 →
  • printf("%lld", x)
    long long 的格式是 %lld,int 是 %d,保留两位小数是 %.2f
    怎么读printf 靠你写的格式串去猜参数类型 —— 写错了它不会报错,只会打出垃圾。常用的四个:%d(int)、%lld(long long)、%s(C 字符串)、%.2f(double 两位小数)。
    什么时候用要控制小数位数时 printf 最省事(cout 要写 fixed << setprecision(2))。⚠ 打 string 要写 s.c_str(),直接塞 %s 会崩。
    第 26 章用到 →
io.cpp跑一遍看看
// 读入输出速查 —— 本书第 6 章的正文代码就在用关同步那两行,第 10 章才解释它
//
// 试着把右边的输入改一改再运行。
#include <bits/stdc++.h>
using namespace std;
int main() {
// ① 这两行是什么意思
// cin 默认和 C 的 scanf 保持同步,这份同步在数据量大时很贵。
// 关掉它,cin 能快好几倍(第 6 章那道题读 47 MB,第 41 章量到差 6.2 倍)。
// 代价只有一条:关了之后**不要再混用 scanf / printf**。
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n; // >> 会自动跳过前面的空格和换行
cout << "① 读到 n = " << n << "\n";
// ② getline 的坑:>> 停在数字后面,**那一行的换行还留在输入里**。
// 紧接着 getline 会读到一个空串。
string bad;
getline(cin, bad);
cout << "② 紧跟着 getline 读到的是 [" << bad << "] <- 空的,就是上面那个换行\n";
// 正确做法:先把那个换行扔掉(或者干脆再 getline 一次)。
string line;
getline(cin, line);
cout << " 再 getline 一次才是真正的一行:[" << line << "]\n";
cout << " 更稳的写法是 >> 之后写一句 cin.ignore();\n";
// ③ 读到文件结尾:题目不告诉你有几个数时用这个
int x, sum = 0, cnt = 0;
while (cin >> x) { sum += x; cnt++; }
cout << "③ 剩下的 " << cnt << " 个数读完了,和 = " << sum << "\n";
// ④ 换行用 '\n',别用 endl。
// endl 除了换行还会**强制刷新缓冲区**,输出量大时慢很多
// (第 41、48 章各量过一次,都是 8.8 倍)。
cout << "④ 用 '\\n' 换行,不要用 endl\n";
// ⑤ printf 的格式:int 用 %d,long long 用 %lld,double 保留两位用 %.2f。
// ⚠ 上面关了同步,这里就不该再用 printf 了;单独用时才写。
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

8. 函数、引用与递归

6 条 · 做题 →

第 1 章就开始写递归函数,而参数表里那个 & 到第 10 章才顺口解释一句。

先说那个 &。C++ 传参数默认是传值:函数拿到的是一份复制品,在里面怎么改都影响不到外面。参数类型后面加一个 & 就变成传引用:函数拿到的就是外面那个东西本身。

★ 而对数组、字符串、结构体来说,这个 & 不只是「能不能改」的问题,更是快慢的问题 —— 不写 &,每次调用都要把整个数组复制一遍。一百万个 int 实测差 6.7 倍,而递归里调用几十万次就是几十万次复制。⇒ 参数是容器时一律写 const T &const 表示「我只读不改」,顺带防手滑)。

★★ 再说递归。递归函数每进一层,它的局部变量都要在上占一块地方,而栈通常只有 8 MB。所以两件事要当心:一是必须有出口(否则无限递归,几万层就崩);二是别在递归函数里开大数组(每层都占一份)。

⚠ 本书量过一个反直觉的数:同一台机器上,递归函数体里多几个变量,能递归的层数就从 17.4 万掉到 2.4 万 —— 能递归多深不是机器的属性,是「你在那个函数里写了什么」的属性。

  • void f(int x)
    传值:函数里改的是一份复制品,外面那个不会变。
    怎么读调用 f(a) 时,a 的值被抄进 x。之后 x = 99; 改的只是那份抄件。
    什么时候用参数是单个 int / char / double 时就这么写,复制一个数不值几个钱。
    本书暂未用到
  • void f(int &x)
    传引用:函数里改的就是外面那个。
    怎么读x 不是副本,而是外面那个变量的另一个名字。所以 x = 99; 真的改了外面的 a
    什么时候用想让函数「顺手改掉某个变量」时 —— 比如 void dfs(int u, int &cnt) 里累加计数。⚠ 不过本书更常用全局变量干这件事,递归里少传一个参数更清爽。
    讲透它的是 第 10 章
    第 10 章用到 →
  • f(const vector<int> &a)
    参数是数组 / 字符串 / 结构体时,一律这么写。
    怎么读三个记号各管一件事:vector<int> 是类型,& 是「别复制」,const 是「我不改它」。少了 & 就是整份复制;少了 const 编译器不会拦你手滑改掉调用方的数据。
    什么时候用任何把 vector / string / struct 当参数的函数。⚠ 比较器 lambda 里也要写([](const Node &a, const Node &b))—— sort 会调用它几百万次。
    第 6 章用到 →
  • 全局变量
    所有函数都看得见,不用一层层传。递归里省下大量参数。
    怎么读写在所有函数外面就是全局的。它们在程序启动时自动清零,而且不占栈。
    什么时候用本书的习惯:图、答案、访问标记这些「整道题共用」的东西放全局,只有「这一层特有」的量才当参数传(比如 dfs 的当前节点 u)。
    ⚠ ⚠ 多组数据的题里,全局变量每组之间必须自己清干净 —— 它只在程序启动时清过一次。这是「第一组对、第二组起全错」的头号原因;另一个代价是「谁改了它」不好追。
    本书暂未用到
  • 递归的出口
    每个递归函数都必须有一句「到这儿就 return,不再往下递」。
    怎么读写递归的顺序永远是:① 先写出口(什么情况下不用再递了、答案直接是什么);② 再写递推(假设小一号的问题已经算对了,我怎么用它拼出答案)。⚠ 第 ② 步的关键是相信它会算对,不要在脑子里展开执行 —— 第 1 章讲的就是这件事。
    什么时候用每一个递归函数。没有出口的表现是程序跑一两秒后 Segmentation fault。
    本书暂未用到
  • 别在递归函数里开大数组
    每进一层,局部变量都要在栈上占一块地方。
    怎么读能递归多深 = 栈上限 ÷ 每层占多少字节。本书实测过两份:一份精简的 dfs 每层 48 字节、能递归 17.4 万层;一份在函数里拼字符串的每层 341 字节、只能 2.4 万层
    什么时候用⚠ 判据是一道算术题:这道题最深会递归多少层? 顶格 n = 10⁵ 的链式数据就是 10 万层,已经踩在门槛上了 ⇒ 本书的结论是「顶格题一律写迭代,别赌」。
    讲透它的是 第 30 章
    本书暂未用到
funcref.cpp跑一遍看看
// 函数、引用与递归速查 —— 那个 & 到底是什么意思
//
// 本书第 1 章就开始写递归函数,而参数表里的 & 在第 10 章才顺口解释。
// 这一份把它单独拎出来。
#include <bits/stdc++.h>
using namespace std;
void byValue(int x) { x = 99; } // 传值:改的是一份复制品
void byRef(int &x) { x = 99; } // 传引用:改的就是原来那个
// 传 vector 时这个 & 不只是「能不能改」的问题,还是**快慢**的问题:
// 不写 &,每次调用都会把整个数组复制一遍。
long long sumCopy(vector<int> a) { long long s = 0; for (int x : a) s += x; return s; }
long long sumRef(const vector<int> &a) { long long s = 0; for (int x : a) s += x; return s; }
// ^^^^^ const 的意思是「我只读,不改」—— 能防手滑,也让调用方放心
int depth = 0; // 全局变量:所有函数都看得见,不用传来传去
void dive(int k) { // 递归:函数调用自己
depth = max(depth, k);
if (k == 5) return; // 出口。没有出口就是无限递归 -> 栈溢出
dive(k + 1);
}
int main() {
int a = 1; byValue(a);
cout << "① byValue(int x) 之后 a = " << a << " <- 没变\n";
int b = 1; byRef(b);
cout << " byRef(int &x) 之后 b = " << b << " <- 变了\n\n";
vector<int> v(1000000, 1);
auto t0 = chrono::steady_clock::now();
long long s1 = sumCopy(v);
auto t1 = chrono::steady_clock::now();
long long s2 = sumRef(v);
auto t2 = chrono::steady_clock::now();
double msCopy = chrono::duration<double, milli>(t1 - t0).count();
double msRef = chrono::duration<double, milli>(t2 - t1).count();
cout << "② 一百万个元素求和,答案都是 " << s1 << " / " << s2 << "\n";
cout << fixed << setprecision(3);
cout << " 传值 vector<int> a " << msCopy << " ms <- 先复制了一百万个 int\n";
cout << " 传引用 const vector<int> &a " << msRef << " ms\n";
cout << " => 参数是数组 / 字符串 / 结构体时,「一律写 const T &」,除非你真想改它\n\n";
dive(0);
cout << "③ 递归 dive(0) 最深到了第 " << depth << " 层\n";
cout << " 每进一层,函数的局部变量都要在「栈」上占一块地方。\n";
cout << " 所以两件事要当心:一是必须有出口,二是「别在递归函数里开大数组」\n";
cout << " (本书量过:函数体里多几个变量,能递归的层数就从 17 万掉到 2.4 万)。\n";
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

9. 零碎但天天用的那几样

8 条 · 做题 →

单独讲不值一节,可不知道就会卡住 —— 而它们在本书每一份代码里都出现。

这一组里没有一样是「算法」,全是写代码的手势。每一条都只要看一眼就会,但没人告诉你的话,读别人的代码时就会卡在那儿。

  • #include <bits/stdc++.h>
    一句话把所有标准库都包进来。
    怎么读它不是标准的一部分,是 GCC 提供的一个「万能头」。正式项目里不该用(编译慢),但竞赛评测机基本都是 GCC,本书一律用它 —— 省得去记 vector 在哪个头、sort 在哪个头。
    ⚠ ⚠ 如果你的编译器不认它(比如苹果的 clang),就老老实实写<iostream> <vector> <algorithm> <string> <cstring> 这几个。
    第 1 章用到 →
  • using namespace std;
    之后写 cout 就行,不用写 std::cout
    怎么读标准库的名字都住在一个叫 std 的命名空间里。这一句是「把它们都拿到台面上来」。
    ⚠ ⚠ 它偶尔会撞车:自己定义一个叫 countnexty1hash 的全局变量,会和标准库里的同名东西冲突,报一句很长的 reference to ... is ambiguous。改法就是换个名字(cntnxt)。
    第 1 章用到 →
  • const int N = 100005;
    把数组大小写成一个常量,别到处散落魔法数字。
    怎么读那个多出来的 5 是留余量的:下标从 1 开始要多一格,加哨兵还要几格。题面写 n ≤ 100000,就写 100005
    什么时候用配合全局数组:const int N = 100005; int a[N]; —— 这是竞赛代码最常见的开头。
    第 1 章用到 →
  • swap(a, b)
    交换两个变量,不用自己写中间变量。
    怎么读对 vector、string 这些也管用,而且是 O(1) 的(只换内部指针,不真的搬数据)。
    什么时候用排序、双指针里到处都是;第 3 章全排列里「换过去、递归、换回来」就是它。
    第 6 章用到 →
  • max(a, b) min(a, b)
    取大 / 取小。三个以上要加一对花括号:max({a, b, c})
    怎么读⚠ 两个参数的类型必须一样max(1, 2LL) 编译不过,要写 max(1LL, 2LL) 或者 max<long long>(1, 2)。这是新手最常撞的一条编译错误。
    什么时候用DP 的转移方程几乎每一行都是 f[i] = max(f[i], f[j] + w)
    第 1 章用到 →
  • abs(x)
    绝对值。
    怎么读整数用 abs,浮点数用 fabs(在 C++ 里 abs 也能处理浮点,但写 fabs 更清楚)。
    什么时候用算曼哈顿距离 abs(x1-x2) + abs(y1-y2)、判两个数差多少。
    第 3 章用到 →
  • INT_MAX LLONG_MAX
    类型的上界,比自己背「二十一亿」靠谱。
    怎么读求最小值时常写 int best = INT_MAX; 然后不断 best = min(best, x);
    ⚠ ⚠ 但如果后面要拿它做加法,用 0x3f3f3f3f 更安全(见第二组)—— INT_MAX + 1 当场溢出成负数。
    第 3 章用到 →
  • cond ? a : b
    三目运算符:一行的 if-else,而且它是一个有值的表达式
    怎么读读成「如果 cond 成立就取 a,否则取 b」。因为它有值,所以可以直接写在 cout << 里、写在 return 后面。
    什么时候用cout << (n % 2 ? "奇" : "偶");return x > 0 ? x : -x;
    ⚠ ⚠ 它的优先级很低,放进 cout <<要加括号,否则编译报错或者结果不对。
    本书暂未用到
misc.cpp跑一遍看看
// 零碎但天天用的那几样 —— 单独讲不值一节,可不知道就会卡住
//
// 这一份里没有一样是「算法」,全是「写代码的手势」。
#include <bits/stdc++.h>
using namespace std;
// ★ 全局数组开多大:写成一个常量,别到处散落魔法数字
// 竞赛里的习惯是「题面上限 + 5」,那 5 是给下标从 1 开始、以及哨兵留的余量。
const int N = 100005; // 题面说 n <= 100000
int a[N]; // 全局数组:自动全是 0,而且不占栈
int main() {
// ① swap:交换两个变量,不用自己写中间变量
int x = 1, y = 2;
swap(x, y);
cout << "① swap 之后 x = " << x << ", y = " << y << "\n\n";
// ② min / max:两个数直接比;三个以上要加一层花括号
cout << "② max(3, 7) = " << max(3, 7)
<< " min(3, 7) = " << min(3, 7) << "\n";
cout << " 三个数:max({3, 7, 5}) = " << max({3, 7, 5}) << " (注意那对花括号)\n";
cout << " ⚠ max(1, 2LL) 编译不过 —— 两个参数的类型必须一样,要写 max(1LL, 2LL)\n\n";
// ③ abs:绝对值
cout << "③ abs(-5) = " << abs(-5) << " abs(5 - 9) = " << abs(5 - 9) << "\n\n";
// ④ 类型的上下界,比自己背 21 亿靠谱
cout << "④ INT_MAX = " << INT_MAX << " LLONG_MAX = " << LLONG_MAX << "\n";
cout << " 求最小值时常写 int best = INT_MAX; 然后不断 best = min(best, x);\n";
cout << " ⚠ 但如果后面要拿它做加法,用 0x3f3f3f3f 更安全(见「整数类型」那一组)\n\n";
// ⑤ 三目运算符:一行的 if-else,它是一个「表达式」,有值
int n = 7;
cout << "⑤ (n % 2 ? \"奇\" : \"偶\") -> " << (n % 2 ? "奇" : "偶") << "\n";
cout << " 它等价于 if (n % 2) … else …,但可以直接写在 cout 里\n\n";
// ⑥ 全局数组 vs 局部数组:大数组一定要放全局(或 static),否则爆栈
a[3] = 42;
cout << "⑥ 全局 int a[" << N << "]:a[0] = " << a[0] << "(自动清零),a[3] = " << a[3] << "\n";
cout << " 而在 main 里写 int b[100005]; 是放在「栈」上的,栈一般只有 8 MB —— \n";
cout << " 开到几百万就直接 Segmentation fault,而且编译不报错。\n";
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

10. 报错了怎么读

7 条 · 做题 →

编译器的话不难懂,只是没人翻译过。这张表覆盖本书写代码时真正撞到过的那几条。

★ 先记一条通用的读法:只看第一条错误,改完再编译一遍。C++ 的编译错误会「雪崩」—— 前面一个分号引发后面几十条,而后面那几十条全是噪声。一次只改最上面那一条。

★★ 第二条:报错的行号常常指到出错行的下一行。 因为编译器是读到下一行才发现「上一句没写完」。看到行号先往上看一行。

⚠ 第三条,也是最要紧的:能编译过不等于是对的。 越界、溢出、没初始化、递归太深 —— 这四样编译器一句话都不会说,它们要么在运行时崩,要么安静地给出一个错答案。下面最后三条讲的就是它们。

  • expected ';' before ...
    上一行漏了分号。
    怎么读例:int a = 1 少了分号,编译器读到下一行 cout << a; 才发现不对,于是报的是下一行的行号。⇒ 看到这条一律往上看一行。
    ⚠ ⚠ 另一个高频来源:struct Node { int x; } 后面那个分号不能漏 —— 花括号结尾一般不用分号,唯独 struct / class 要。
    本书暂未用到
  • 'xxx' was not declared in this scope
    这个名字编译器不认识。
    怎么读按可能性从高到低查三件事:① 拼错了cnt 写成 cnf);② 作用域不对 —— 它定义在某个 {} 里面,出了那对花括号就不存在了;③ 忘了 #include(本书一律用 <bits/stdc++.h>,所以基本不是这个原因)。
    ⚠ ⚠ 还有一种最气人的:在函数 A 里调用了写在它下面的函数 B。C++ 是从上往下读的 ⇒ 要么把 B 挪到 A 前面,要么在最上面先写一行声明 int B(int);
    本书暂未用到
  • no matching function for call to ...
    函数名对,但参数对不上。
    怎么读两种:个数不对,或者类型不对。最常撞的是 max(1, 2LL) —— max 要求两个参数类型完全一样,int 和 long long 混着传就找不到匹配的版本。改成 max(1LL, 2LL)
    ⚠ 报错信息里会列出一串「candidate:」,那是它试过的所有版本 —— 不用逐条看,直接回去核对自己传的参数类型。
    本书暂未用到
  • invalid operands of types ...
    这两个类型不能这么运算。
    怎么读例:string s = "ab"; int x = s + 1; —— 串加数字没有定义。想把数字接到串后面要写 s + to_string(1);想把串变成数字要写 stoi(s)
    本书暂未用到
  • comparison of integer expressions of different signedness
    有符号和无符号在比大小。
    怎么读多半是 int iv.size() 在比。size() 是无符号的,比较时 int 会被悄悄转成无符号 —— 于是 -1 < v.size() 竟然是的。
    ⚠ ⚠ 它只是警告,编译照样过。但空数组上 i <= v.size() - 1 会真的错。改法:i < (int)v.size()
    本书暂未用到
  • Segmentation fault(运行时)
    越界访问,或者递归太深爆了栈。编译期一句话都不会说。
    怎么读查的顺序:① 数组开够了没(a[n] 要开 n+1 格,下标从 1 开始还要再多一格);② 下标算出来会不会是负的;③ 递归有没有出口、会不会太深。
    什么时候用最快的定位法:把可疑的 a[i] 临时换成 a.at(i),越界会当场抛异常并告诉你下标是多少,而不是让程序在别处崩。
    讲透它的是 第 30 章
    本书暂未用到
  • 编译过了,可答案不对
    八成是这四样之一:溢出、没初始化、越界、读错。
    怎么读一条一条排除:① 溢出 —— 把所有 int 换成 long long 再跑一遍,变对了就是它;② 没初始化 —— 多组数据的题里全局数组每组都要清;③ 越界 —— 见上一条;④ 读错 —— 把读进来的东西原样打印出来看一眼,这一步能省掉一半的调试时间。
    什么时候用⚠ 而如果只是「部分测试点错」,那多半不是这四样,是算法本身在某种数据上不对 —— 这时候该写对拍了(术语表里有)。
    本书暂未用到

没找到想查的写法?先确认它是不是算法概念而不是 C++ 语法 —— 那些在术语表里,那张表指的是「真正教它的那一章」。