常见错误
本页面主要列举一些竞赛中很多人经常会出现的错误。
因环境不同导致的错误
scanf
或printf
使用%I64d
格式指示符在 Linux 下可能导致输出格式错误。
会引起 CE 的错误
这类错误多为词法、语法和语义错误,引发的原因较为简单,修复难度较低。
例:
int main()
写为int mian()
之类的拼写错误。写完
struct
或class
忘记写分号。数组开太大,(在 OJ 上)使用了不合法的函数(例如多线程),或者函数声明但未定义,会引起链接错误。
函数参数类型不匹配。
示例:如使用
<algorithm>
头文件中的max
函数时,传入了一个int
类型参数和一个long long
类型参数。1 2 3 4
// query 为返回 long long 类型的自定义函数 printf("%lld\n", max(0, query(1, 1, n, l, r)); //错误 没有与参数列表匹配的 重载函数 "std::max" 实例
使用
goto
和switch-case
的时候跳过了一些局部变量的初始化。
不会引起 CE 但会引起 Warning 的错误
犯这类错误时写下的程序虽然能通过编译,但大概率会得到错误的程序运行结果。这类错误会在使用 -W{warningtype}
参数编译时被编译器指出。
赋值运算符
=
和比较运算符==
不分。示例:
1 2 3 4 5 6 7 8 9
std::srand(std::time(nullptr)); int n = std::rand(); if (n = 1) printf("Yes"); else printf("No"); // 无论 n 的随机所得值为多少,输出肯定是 Yes // 警告 运算符不正确: 在 Boolean 上下文中执行了常量赋值。应考虑改用「==」。
如果确实想在原应使用
==
的语句里使用=
(比如while (foo = bar)
),又不想收到 Warning,可以使用 双括号:while ((foo = bar))
。
由于运算符优先级产生的错误。
示例:
1 2 3 4 5 6
// 错误 // std::cout << (1 << 1 + 1); // 正确 std::cout << ((1 << 1) + 1); // 警告 「<<」: 检查运算符优先级是否有可能的错误;使用括号阐明优先级
不正确地使用
static
修饰符。使用
scanf
读入的时候没加取地址符&
。使用
scanf
或printf
的时候参数类型与格式指定符不符。同时使用位运算和逻辑运算符
==
并且未加括号。- 示例:
(x >> j) & 3 == 2
- 示例:
int
字面量溢出。- 示例:
long long x = 0x7f7f7f7f7f7f7f7f
,1<<62
。
- 示例:
未初始化局部变量。
未初始化变量会发生什么
原文:https://loj.ac/d/3679 by @hly1204
例如我们在 C++ 中声明一个
int a;
但不初始化,可能有时候会认为a
是一个「随机」(其实可能不是真的随机)的值,但是可能将其认为是一个固定的值,但实际上并非如此。我们在简单的测试代码中
https://wandbox.org/permlink/T2uiVe4n9Hg4EyWT
代码是:
1 2 3 4 5 6 7
#include <iostream> int main() { int a; std::cout << std::boolalpha << (a < 0 || a == 0 || a > 0); return 0; }
在一些编译器和环境上开启优化后,其输出为 false。
有兴趣的话可以看 https://www.ralfj.de/blog/2019/07/14/uninit.html,尽管其是用 Rust 做的实验,但是本质是一样的。
局部变量与全局变量重名,导致全局变量被意外覆盖。(开
-Wshadow
就可检查此类错误。)运算符重载后引发的输出错误。
示例:
1 2 3 4
// 本意:前一个 << 为重载后的运算符,表示输出;后一个 << 为移位运算符,表示将 1 // 左移 1 位。 但由于忘记加括号,导致编译器将后一个 << // 也判作输出运算符,而导致输出的结果与预期不同。 错误 std::cout << 1 << 1; 正确 std::cout << (1 << 1);
既不会引起 CE 也不会引发 Warning 的错误
这类错误无法被编译器发现,仅能自行查明。
会导致 WA 的错误
上一组数据处理完毕,读入下一组数据前,未清空数组。
读入优化未判断负数。
所用数据类型位宽不足,导致溢出。
- 如习语「三年 OI 一场空,不开
long long
见祖宗」所描述的场景。选手因为没有在正确的地方开long long
(将整数定义为long long
类型),导致得出错误的答案而失分。
- 如习语「三年 OI 一场空,不开
存图时,节点编号 0 开始,而题目给的边中两个端点的编号从 1 开始,读入的时候忘记 -1。
大/小于号打错或打反。
在执行
ios::sync_with_stdio(false);
后混用scanf/printf
和std::cin/std::cout
两种 IO,导致输入/输出错乱。示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// 这个例子将说明关闭与 stdio 的同步后,混用两种 IO 方式的后果 // 建议单步运行来观察效果 #include <cstdio> #include <iostream> int main() { // 关闭同步后,cin/cout 将使用独立缓冲区,而不是将输出同步至 scanf/printf // 的缓冲区,从而减少 IO 耗时 std::ios::sync_with_stdio(false); // cout 下,使用'\n'换行时,内容会被缓冲而不会被立刻输出 std::cout << "a\n"; // printf 的 '\n' 会刷新 printf 的缓冲区,导致输出错位 printf("b\n"); std::cout << "c\n"; // 程序结束时,cout 的缓冲区才会被输出 return 0; }
由于宏的展开,且未加括号导致的错误。
示例:该宏返回的值并非
而是 。1 2
#define square(x) x* x printf("%d", square(2 + 2));
哈希的时候没有使用
unsigned
导致的运算错误。- 对负数的右移运算会在最高位补 1。参见:位运算
没有删除或注释掉调试输出语句。
误加了
;
。示例:
1 2 3
/* clang-format off */ while (1); printf("OI Wiki!\n");
哨兵值设置错误。例如,平衡树的
0
节点。在类或结构体的构造函数中使用
:
初始化变量时,变量声明顺序不符合初始化时候的依赖关系。- 成员变量的初始化顺序与它们在类中声明的顺序有关,而与初始化列表中的顺序无关。参见:构造函数与成员初始化器列表 的「初始化顺序」
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
#include <iostream> class Foo { public: int a, b; // a 将在 b 前初始化,其值不确定 Foo(int x) : b(x), a(b + 1) {} }; int main() { Foo bar(1, 2); std::cout << bar.a << ' ' << bar.b; } // 可能的输出结果:-858993459 1
并查集合并集合时没有把两个元素的祖先合并。
示例:
1 2
f[a] = b; // 错误 f[find(a)] = find(b); // 正确
freopen
使用a
进行追加写- CCF 的检测环境不会清空输出文件,使用
a
会导致上一位选手的输出也被评测机读入引发 WA
- CCF 的检测环境不会清空输出文件,使用
换行符不同
不同的操作系统使用不同的符号来标记换行,以下为几种常用系统的换行符:
LF(用
\n
表示):Unix
或Unix
兼容系统CR+LF(用
\r\n
表示):Windows
CR(用
\r
表示):Mac OS
至版本 9
而 C/C++ 利用转义序列 \n
来换行,这可能会导致我们认为输入中的换行符也一定是由 \n
来表示,而只读入了一个字符来代表换行符,这就会导致我们没有完全读入输入文件。
以下为解决方案:
多次
getchar()
,直到读到想要的字符为止。使用
cin
读入,这可能会增大代码常数。使用
scanf("%s",str)
读入一个字符串,然后取str[0]
作为读入的字符。使用
scanf(" %c",&c)
过滤掉所有空白字符。
会导致未知的结果
未定义行为会导致未知的结果,可能是 WA,RE 等。编译器通常会假定你的程序不会出现未定义行为,因此出现开 O2 与不开 O2 代码行为不一致的情况。
除以 0(求 0 的逆元)
示例
1
cout << x / 0 << endl;
数组(下标)越界
例如:
未正确设置循环的初值导致访问了下标为 -1 的值。
无向图边表未开 2 倍。
线段树未开 4 倍空间。
看错数据范围,少打一个零。
错误预估了算法的空间复杂度。
写线段树的时候,
pushup
或pushdown
叶节点。正确的做法:不要越界,记得检查自己的代码,使得下标访问数
x
,在定义的下标中。
除 main 外有返回值函数执行至结尾未执行任何 return 语句
即使有一个分支有返回值,但是其他分支却没有,结果也是未定义的。
可以向编译选项中追加
-Wall
,检查编译器是否给出有关于函数未 return 的警告。尝试修改字符串字面量
示例
1 2 3
char *p = "OI-wiki"; p[0] = 'o'; p[1] = 'i';
这样试图修改字符串字面量会导致 未定义行为,应当使用其他 合适 的数据类型,例如
std::string
和char[]
。多次释放/非法解引用一片内存
例如:
未初始化就解引用指针。
指针指向的内存区域已经释放。
使用
erase
或delete
或free
操作应注意不要对同一地址/对象多次使用。
尝试释放由
new []
分配的整块内存的一部分例如:
1 2 3 4 5 6
object *pool = new object[POOL_SIZE]; object *pointer = pool + 10; // 报错! delete pointer;
常见于使用内存池提前分配整块内存后,试图使用
delete
或free()
释放从内存池中获取的单个对象。解引用空指针/野指针
对于空指针:先应该判断空指针,可以用
p == nullptr
或!p
。对于野指针:可以释放指针的时候将其置为
nullptr
以规避。有符号数溢出
例如我们有一个表达式
x+1 > x
。正常输出应当是
true
,但是在INT_MAX
作为x
时输出false
,这时称为signed integer overflow
。可以使用更大的数据类型(例如
long long
或__int128
),或判断溢出。若保证无负数,亦可使用无符号整型。有符号整数溢出可能影响编译优化,例如代码:
1 2 3 4
int foo(int x) { if (x > x + 1) return 1; return 0; }
可能被编译器直接优化为:
1
int foo(int x) { return 0; }
因为编译器可以假定有符号整数永远不会溢出,因此
x > x + 1
永远不会成立。使用未初始化的变量
示例
1 2 3 4 5
int foo(int a) { int t; /* 没有初始化 */ if (/* 使用 */ t > 3) return a; return 0; }
会导致 RE
没删文件操作(某些 OJ)。
排序时比较函数的错误
std::sort
要求比较函数是严格弱序:a<a
为false
;若a<b
为true
,则b<a
为false
;若a<b
为true
且b<c
为true
,则a<c
为true
。其中要特别注意第二点。 如果不满足上述要求,排序时很可能会 RE。 例如,编写莫队的奇偶性排序时,这样写是错误的:1 2 3 4 5 6
bool operator<(const int a, const int b) { if (block[a.l] == block[b.l]) return (block[a.l] & 1) ^ (a.r < b.r); else return block[a.l] < block[b.l]; }
上述代码中
(block[a.l]&1)^(a.r<b.r)
不满足上述要求的第二点。 改成这样就正确了:1 2 3 4 5 6 7 8 9
bool operator<(const int a, const int b) { if (block[a.l] == block[b.l]) // 错误:不满足严格弱序的要求 // return (block[a.l] & 1) ^ (a.r < b.r); // 正确 return (block[a.l] & 1) ? (a.r < b.r) : (a.r > b.r); else return block[a.l] < block[b.l]; }
Windows 下栈空间不足,导致栈空间溢出,Windows 向程序发出 SIGSEGV 信号,程序终止并返回 3221225725(即 0xC00000FD, NTSTATUS 定义为
STATUS_STACK_OVERFLOW
)。
若使用 gcc 编译器,可在编译时加入命令-Wl,--stack=SIZE
以指定栈空间大小限制,其中SIZE
为栈空间大小字节数。Linux 下栈空间不足,导致栈空间溢出,Linux 会在栈堆中乱写
head_info
,此操作绝大多数情况下会导致程序立即退出,显示段错误(核心已转储)/segmentation fault (core dumped)
等字样。
可以在终端下使用ulimit -s SIZE
修改当前终端的栈空间限制,其中SIZE
为栈空间大小千字节数(KB)。
请注意,如果你将栈空间限制设置过大,无穷递归可能导致递归栈过大进而导致系统崩溃。
会导致 TLE
分治未判边界导致死递归。
死循环。
循环变量重名。
循环方向反了。
BFS 时不标记某个状态是否已访问过。
使用宏展开编写 min/max
这种错误会大大增加程序的运行时间,甚至直接影响代码的时间复杂度。在初学者写线段树时尤为多见。
常见的错误写法是这样的:
1 2
#define Min(x, y) ((x) < (y) ? (x) : (y)) #define Max(x, y) ((x) > (y) ? (x) : (y))
这样写虽然在正确性上没有问题,但是如果直接对函数的返回值取 max,如
a = Max(func1(), func2())
,而这个函数的运行时间较长,则会大大影响程序的性能,因为宏展开后是a = func1() > func2() ? func1() : func2()
的形式,调用了三次函数,比正常的 max 函数多调用了一次。注意,如果func1()
每次返回的答案不一样,还会导致这种max
的写法出现错误。例如func1()
为return ++a;
而a
为全局变量的情况。示例:如下代码会被卡到单次查询
导致 TLE。1 2 3 4 5 6 7 8 9 10 11 12 13
#define max(x, y) ((x) > (y) ? (x) : (y)) int query(int t, int l, int r, int ql, int qr) { if (ql <= l && qr >= r) { ++ti[t]; // 记录结点访问次数方便调试 return vi[t]; } int mid = (l + r) >> 1; if (mid >= qr) return query(lt(t), l, mid, ql, qr); if (mid < ql) return query(rt(t), mid + 1, r, ql, qr); return max(query(lt(t), l, mid, ql, qr), query(rt(t), mid + 1, r, ql, qr)); }
使用 + 运算符向
std::string
类字符串追加字符这种错误会创建一个临时
string
变量,修改完成后再赋值给原变量。这种错误无法被编译器优化,在数据量大的情况下可能会导致时间复杂度退化。常见 错误写法:
1 2 3
std::string a; char b = 'c'; a = a + b;
当执行这段代码时,程序首先会创建一个临时
string
变量,随后将a
的值存入临时变量,然后在末尾添加b
的值,最后再存入a
。从 汇编结果 可以看出,
a = a + b
调用了三次std::__cxx11::basic_string
中的功能,分别为operator+
、operator=
和创建变量。正确写法应该是:
1 2 3
std::string a; char b = 'c'; a += b;
这种写法 会直接将字符
b
附加到字符串a
中,仅调用了一次operator+=
。更详细的性能比较可参考 Benchmark。没删文件操作(某些 OJ)。
在
for/while
循环中重复执行复杂度非 的函数。严格来说,这可能会引起时间复杂度的改变。
会导致 MLE
数组过大。
Linux 下的内存占用指标详解
太长不看版:如果在 CCF 系列的考试时声明了一个特别大的全局静态数组,此时需要特别慎重。因为程序声明的数组会全部计入内存占用中(而不像大部分在线评测平台仅计算实际使用的部分),在某些情况下这甚至有可能导致整道题全部 MLE。
-
VSZ (Virtual Memory Size,虚拟内存大小)3
VSZ 表示进程的 虚拟内存大小,是进程可以访问的虚拟地址空间的总大小,通常以 KB 显示。
虚拟内存是一个逻辑概念,通常会比实际内存使用大很多。
在 Linux 下你可以使用
top
命令来查看某个进程的内存占用组成,其中VIRT
这一列就代表它占用的虚拟内存。虚拟内存一般包括进程分配但未实际使用的地址空间,简而言之,申请了多少,虚拟内存就大约是多少。
需要特别注意的是,常用的在线评测平台通常只统计物理内存占用。但 CCF 的评测环境统计的是虚拟内存,这意味着如果你声明了一个全局静态大数组,即使只使用了其中的一小部分,也会占用大量空间。
RSS(Resident Set Size,常驻集大小)4
RSS 表示进程实际占用的 物理内存大小,即驻留在 RAM 中的页帧大小,通常以 KB 显示。
同样的,你也可以用
top
在RES
这一列查看某个进程的物理内存。RSS 一般仅包含实际加载到物理内存的部分,也就是说,实际使用了多少就是多少。
内存占用行为分析
假设声明了以下数组:
1 2
const int SIZE = 1e8; int arr[SIZE]; // 占用空间:4 字节 * 1 亿 = 400 MB
这是一个静态数组,分配在全局数据段。这个数组未经显式初始化,通常会被分配在 BSS 段(如果显式初始化(如全为 0 或其他值),则分配在 DATA 段)。
当完全未使用该数组时(假定编译器不会优化掉该数组)
- 物理内存:如果数组未被访问,按需分页机制(Demand Paging)会使内存页尚未加载到物理内存中。物理内存不会增加或仅略微增加一点(可能加载了一些元数据页)。
- 虚拟内存:数组的大小会计入虚拟内存(增加
400MB
),因为整个数组的虚拟地址空间已经被分配。
部分使用该数组时
假设仅使用数组的少部分元素,例如:
1 2
arr[0] = 1; arr[999999] = 2;
- 虚拟内存:虚拟内存不会改变,仍为
400MB
。 - 物理内存:每次访问数组的一个元素,对应的虚拟页会被加载到物理内存中。假定系统分页大小为
4KB
,那么每页包含 个int
元素。访问数组两次可能加载 2 个页,即增加约 的物理内存。
- 虚拟内存:虚拟内存不会改变,仍为
数组被大半使用
假设对数组的前
个元素赋值:1 2 3
for (int i = 0; i < 50000000; ++i) { arr[i] = i; }
- 虚拟内存 (VSZ):VSZ 仍为
400MB
,不会发生变化。 物理内存 (RSS):此时是连续访问(访问的
个元素在内存地址上相邻),需要加载的分页数为 页。假设每页大小为
4KB
,因此总计 ,物理内存增加到约190MB
。注:如果对该数组赋值时下标随机,则会导致物理内存占用与预测差距较大(因为内存页加载是以地址为准,导致随机赋值时会加载大量内存页)。
简要总结:随着被访问的部分占的比例增加,物理内存趋近于虚拟内存(假定不存在页面回收)。
- 虚拟内存 (VSZ):VSZ 仍为
STL 容器中插入了过多的元素。
经常是在一个会向 STL 插入元素的循环中死循环了。
也有可能被卡了。
-
会导致常数过大
定义模数的时候,未定义为常量。
示例:
1 2
// int mod = 998244353; // 错误 const int mod = 998244353; // 正确,方便编译器按常量处理
使用了不必要的递归(尾递归不在此列)。
将递归转化成迭代的时候,引入了大量额外运算。
只在程序在本地运行的时候造成影响的错误
文件操作有可能会发生的错误:
对拍时未关闭文件指针
fclose(fp)
就又令fp = fopen()
。这会使得进程出现大量的文件野指针。freopen()
中的文件名未加.in
/.out
。
使用堆空间后忘记
delete
或free
。
参考资料与注释
本页面最近更新:2024/12/25 18:46:24,更新历史
发现错误?想一起完善? 在 GitHub 上编辑此页!
本页面贡献者:H-J-Granger, StudyingFather, broken-paint, Chrogeek, ChungZH, Enter-tainer, Estrella-Explore, HuangYiming0608, Ir1d, ksyx, Marcythm, NachtgeistW, orzAtalod, shuzhouliu, yiyangit, 66Leo66, Allenyou1126, alphagocc, bear-good, caijianhong, CCXXXI, ChineseHamberger, CodeZhangBorui, CodingOIer, countercurrent-time, Early0v0, greyqz, HeRaNO, hsfzLZH1, i-yyi, imba-tjd, inclyc, isdanni, MapMaths, mgt, ouuan, SukkaW, Sweetlemon68, swiftqwq, Tiphereth-A, Xeonacid, ylxmf2005, YZircon
本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用