下面的许多输出可能因平台而异。
检查越界
向越界的数组下标写入数据称为缓冲区溢出。C++ 发生缓冲区溢出时,不一定会产生运行时错误。例如,下面的代码在 ide.usaco.guide 上会产生运行时错误,但在我的计算机上会输出 4。
#include <iostream>#include <vector>using namespace std;int main() {vector<int> invalid_vec{1};vector<int> valid_vec{1234};cout << valid_vec[0] << "\n"; // outputs 1234for (int i = 0; i < 10; i++) {invalid_vec[i] = i; // may or may not error}cout << valid_vec[0] << "\n"; // may output 4}
为确保访问越界下标时抛出错误,可以使用 vector::at 代替 vector::operator[],如下所示:
#include <iostream>#include <vector>using namespace std;int main() {vector<int> invalid_vec{1};vector<int> valid_vec{1234};cout << valid_vec.at(0) << "\n"; // outputs 1234for (int i = 0; i < 10; i++) {invalid_vec.at(i) = i; // throws std::out_of_range}cout << valid_vec.at(0) << "\n";}
现在,访问 vector 时 C++ 会检查边界,并产生以下输出:
1234 terminate called after throwing an instance of 'std::out_of_range' what(): vector::_M_range_check: __n (which is 1) >= this->size() (which is 1) 1 zsh: abort ./$1 $@[2,-1]
行号
注意,上面的输出不包含运行时错误发生处的行号。要显示行号,可以使用 gdb 或 lldb 等调试器。更多信息请参阅调试器一节。
未指定的求值顺序
创建字典树或可持久化线段树时,可能会遇到下面这种出乎意料的行为。
#include <bits/stdc++.h>using namespace std;vector<int> res{-1};int add_element() {res.push_back(-1);return res.size() - 1; // index of added element}
使用 -std=c++17 编译并运行上述代码,会得到预期输出:
0 1 1 2 2 3 3 4 4 5
但使用 -std=c++14 编译并运行时,会得到意外结果:
0 -1 1 -1 2 3 3 -1 4 5
无论使用 -std=c++17 还是 -std=c++14,只要先把 add_element() 的结果保存到临时变量,就会产生预期输出。
#include <bits/stdc++.h>using namespace std;vector<int> res{-1};int add_element() {res.push_back(-1);return res.size() - 1; // index of added element}
问题在于,res[i] = add_element(); 只有在 add_element() 先于 res[i] 求值时才有效。如果先对 res[i] 求值,随后 add_element() 又导致 res 的内存重新分配,那么 res[i] 就会失效。res[i] 与 add_element() 的求值顺序并未规定(至少在 C++17 之前如此)。
关于出现这种情况的原因,请参阅这篇 Stack Overflow 帖子的讨论(这里还有一个类似问题)。
GCC 警告选项
本节和下一节将介绍一些可添加到 g++ 编译命令中的选项,以辅助调试。
| Resources | |||||
|---|---|---|---|---|---|
| CF | 包含下面的全部选项。 | ||||
| GCC | 以下选项的官方文档。 | ||||
以下是 Ben 使用的警告选项:
-Wall -Wextra -Wshadow -Wconversion -Wfloat-equal -Wduplicated-cond -Wlogical-op
下面给出其中一些选项的示例。
面向 USACO Guide IDE 用户
可以在 ide.usaco.guide 中自定义编译选项。
-Wall
启用许多(但并非全部)警告选项,包括 -Wuninitialized 和 -Wunused-variable。
#include <bits/stdc++.h>using namespace std;int main() {int x;cout << x;}
编译输出:
main.cpp: In function ‘int main()’:
main.cpp:6:10: warning: ‘x’ is used uninitialized in this function [-Wuninitialized]
6 | cout << x;
| ^
-Wextra
启用一些 -Wall 未启用的警告选项,例如 -Wmissing-field-initializers。
#include <bits/stdc++.h>using namespace std;struct s {int f, g, h;};int main() { s x = {3, 4}; }
编译输出:
main.cpp: In function ‘int main()’:
main.cpp:7:18: warning: missing initializer for member ‘s::h’ [-Wmissing-field-initializers]
7 | s x = { 3, 4 };
| ^
-Wconversion
对可能改变数值的隐式转换发出警告。
#include <bits/stdc++.h>using namespace std;int main() {double x = 5.5;int y = x;cout << y;}
编译输出:
main.cpp: In function ‘int main()’:
main.cpp:6:13: warning: conversion from ‘double’ to ‘int’ may change value [-Wfloat-conversion]
6 | int y = x;
| ^
-Wshadow
| Resources | |||||
|---|---|---|---|---|---|
| LCPP | |||||
#include <bits/stdc++.h>using namespace std;int x;int main() {int x = 5;cout << x;}
编译输出:
main.cpp: In function ‘int main()’:
main.cpp:7:6: warning: declaration of ‘x’ shadows a global declaration [-Wshadow]
7 | int x = 5;
| ^
main.cpp:4:5: note: shadowed declaration is here
4 | int x;
| ^
-Wfloat-equal
当浮点数用于相等性比较时发出警告。
#include <bits/stdc++.h>using namespace std;int main() {double x = 1.0 / 49 * 49;cout << (x == 1.0); // 0}
编译输出:
main.cpp: In function ‘int main()’:
main.cpp:6:16: warning: comparing floating-point with ‘==’ or ‘!=’ is unsafe [-Wfloat-equal]
6 | cout << (x == 1.0);
| ~~^~~~~~
GCC 调试选项
这些选项可能拖慢编译,甚至降低运行速度,因此在速度至关重要时(例如 Facebook Hacker Cup)不要启用。
-fsanitize 参数无法与 MinGW 配合使用。如果使用 Windows 但仍想使用这些参数,可以考虑改用在线编译器(或安装 Linux)。
-fsanitize=undefined
示例:数组越界
不使用 -fsanitize=undefined 时,程序会成功执行,但输出无意义的值:
#include <bits/stdc++.h>using namespace std;int main() {int v[5];cout << v[5] << endl; // may output an arbitrary integer}
使用 -fsanitize=undefined 后,程序仍会成功执行,但标准错误流会输出以下运行时错误:
main.cpp:6:13: runtime error: index 5 out of bounds for type 'int [5]'
main.cpp:6:13: runtime error: load of address 0x7ffc4efaf2d4 with insufficient space for an object of type 'int'
0x7ffc4efaf2d4: note: pointer points here
11 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 f0 15 40 00 00 00 00 00 00 00 00 00 00 00 00 00
^
示例:vector 越界
下面的代码会产生段错误:
#include <bits/stdc++.h>using namespace std;int main() {vector<int> v;cout << v[-1] << endl;}
输出:
/tmp/program/run.sh: line 1: 71 Segmentation fault ./prog Command exited with non-zero status 139
使用 -fsanitize=undefined 后,会产生信息更丰富一些的错误消息:
/opt/rh/devtoolset-10/root/usr/include/c++/10/bits/stl_vector.h:1046:34: runtime error: applying non-zero offset 18446744073709551612 to null pointer /tmp/program/run.sh: line 1: 1845 Segmentation fault ./prog Command exited with non-zero status 139
示例:整数溢出
#include <bits/stdc++.h>using namespace std;int main() {int x = 1 << 30;cout << x + x << endl;}
使用 -fsanitize=undefined 后,程序仍会成功执行,但标准错误流会输出以下运行时错误:
main.cpp:6:14: runtime error: signed integer overflow: 1073741824 * 2 cannot be represented in type 'int'
示例:检测多个错误
默认情况下,未定义行为检测器在发现错误后会尝试继续运行。例如,下面的程序在使用 -fsanitize=undefined 时会产生多个运行时错误:
#include <bits/stdc++.h>using namespace std;int main() {cout << (1 << 32) << endl;cout << (1 << 32) << endl;cout << (1 << 32) << endl;}
标准错误:
main.cpp:5:13: runtime error: shift exponent 32 is too large for 32-bit type 'int' main.cpp:6:13: runtime error: shift exponent 32 is too large for 32-bit type 'int' main.cpp:7:13: runtime error: shift exponent 32 is too large for 32-bit type 'int'
要禁用这种行为,并在检测到第一个错误后退出,可以同时使用 -fsanitize=undefined 和 -fno-sanitize-recover。
标准错误:
main.cpp:5:13: runtime error: shift exponent 32 is too large for 32-bit type 'int' Command exited with non-zero status 1
-fsanitize=address
| Resources | |||||
|---|---|---|---|---|---|
| GCC |
| ||||
示例:vector 越界
回顾上一小节中的示例,它会产生段错误:
#include <bits/stdc++.h>using namespace std;int main() {vector<int> v;cout << v[-1] << endl;}
使用 -fsanitize=address 编译后会得到:
标准错误
为了获得更有用的信息,还应使用 -g 参数编译。它会根据程序行号生成包含调试信息的文件。
标准错误
示例:数组越界
#include <bits/stdc++.h>using namespace std;int main() {int v[5];cout << v[5] << endl;}
使用 -fsanitize=address -g:
标准错误
-D_GLIBCXX_DEBUG
该选项会启用调试模式,将每个 STL 容器替换为相应的调试容器。
| Resources | |||||
|---|---|---|---|---|---|
| GCC |
| ||||
回顾下面这个会产生段错误的程序。
#include <bits/stdc++.h>using namespace std;int main() {vector<int> v;cout << v[-1] << endl;}
使用 -D_GLIBCXX_DEBUG 后会产生以下输出:
调试输出
使用 LLDB 调试器
| Resources | |||||
|---|---|---|---|---|---|
| LLVM | |||||
回顾检查越界一节的示例,其中的输出不包含运行时错误发生处的行号。下面演示如何使用 lldb 输出行号。假设 C++ 源文件名为 prog.cpp,可执行文件名为 prog。
- 在编译命令中加入
-g并进行编译。 - 使用
lldb prog对prog启动调试模式。 - 使用
r开始运行程序。 - 使用
bt显示栈回溯。
输出
如上一模块所述,调试器还有许多其他功能,但它们对竞赛编程而言并不是特别有用。