PrevNext
Resources
AryanshS

本文部分内容取自这里。

LCPP

如何添加输出语句。

输出变量

基础输出语句

最基本的调试方式是添加输出语句。这个方法简单有效,大多数时候都能达到目的。例如,可以编写下面的代码,检查程序运行到某处时 x 的值。

x = 10 # pretend this variable is important
def dbg():
print("x =", x)
dbg() # x = 10
x = 5000
dbg() # x = 5000

这种输出语句非常适合基础调试。需要编译和执行较为正式的代码版本时,可以将它们注释掉,或通过条件定义将其排除在主代码之外。

标准错误流

不过,输出语句虽然好用,却很难方便地与程序的实际输出分离。当我们需要在线评测系统(OJ)读取输出时,这一点尤为重要。

标准错误流可以快速解决这个问题。我们不再向标准输出流打印,而是把调试信息输出到另一个独立的流——标准错误流。

import sys
x = 10
def dbg():
print("x =", x, file=sys.stderr)
dbg() # outputs 10
x = 5000
dbg() # now outputs 5000

尝试运行这个程序时,你可能会困惑它究竟有什么不同:错误流中的内容看起来就显示在标准输出旁边。但这正是它的妙处!更重要的是,把程序提交到 OJ 后,评测系统完全不会把错误流中的内容当作答案输出。

Warning!

向错误流输出过多内容,提交到 OJ 时可能导致超时(TLE)。

C++

调试宏

下面这个实用宏不仅会输出变量值(v),还会输出行号(__LINE__)和变量名(#v)。

#include <iostream>
using namespace std;
#define dbg(v) cout << "Line(" << __LINE__ << ") -> " << #v << " = " << (v) << endl;
int x = 10; // pretend this variable is important
int main() {
dbg(x); // Line(10) -> x = 10
x = 5000;
dbg(x); // Line(12) -> x = 5000
}

调试 STL 数据结构

C++ 没有为许多内置数据结构提供直接输出函数,因此最好准备一些预先编写的打印代码。这个模板使用起来相当简单,基本支持竞赛编程中所需的所有数据结构。使用方式如下:

#include <iostream>
#include <vector>
#include "debugging.h"
using namespace std;
int main() {
vector<int> arr{1, 2, 3, 4};
cout << arr << endl; // just feed it into cout like any other variable
}
Warning!

USACO 比赛不允许使用预先编写的代码,因此这个模板只能用于其他在线比赛。

Java

Python

断言与警告

Resources
LCPP

包括 static_assert#define NDEBUG

GCC

介绍 #warning#error

使用 assert(cond) 检查条件 cond 是否成立。如果不成立,程序就会终止。例如,下面的代码片段:

#include <cassert>
#include <iostream>
using namespace std;
int main() {
assert(false);
cout << "OK" << endl;
}

会产生以下结果:

prog: main.cpp:6: int main(): Assertion `false' failed.
/tmp/program/run.sh: line 1:   408 Aborted                 ./prog
Command exited with non-zero status 134

如果想禁用所有断言(例如提交前),可以在程序开头添加 #define NDEBUG

#define NDEBUG
#include <cassert>
#include <iostream>
using namespace std;
int main() {
assert(false);
cout << "OK" << endl; // OK
}

压力测试

如果代码得到答案错误(WA),一种调试方式是在随机生成的测试数据上同时运行有问题的解法和另一个你较有把握正确的解法,直到两者产生不同结果。通常使用小规模数据就足够了。详情请参阅视频。

Resources
Errichto

使用脚本进行压力测试。

Errichto

包含上述视频的部分内容。

方案一:单个程序

编写一个程序,其中同时包含两份解法和一个输入生成器。如果使用 C++,应将每份解法分别放入独立的命名空间,以避免编译错误,如下所示:

#include <bits/stdc++.h>
using namespace std;
namespace Solution1 {
int solve(int x) { return x < 10; }
} // namespace Solution1
namespace Solution2 {

当然,只有当两份解法使用相同语言编写时,才能采用这个方案。

方案二:三个程序

另一种方式是编写三个不同的程序:一个正确程序、一个错误程序,以及一个输入生成器。然后可以使用下面的脚本,找出使正确程序与错误程序产生不同结果的输入。如果想学习如何自行编写这类脚本,请参阅这个模块

脚本

调试器

本质上,调试器是一种替你“跟踪代码”的工具。不同语言乃至不同 IDE 的调试器用法都不相同,因此这里仅介绍调试器的基础知识。

使用调试器的优点:

  • 无需编写输出语句,可以节省时间。
  • 可以实时单步执行代码。
  • 对于 C++,可以显示触发运行时错误的代码行。

使用调试器的缺点:

  • 它与在程序各处输出变量值并没有太大区别。
  • 大多数竞赛程序员不使用调试器。
  • 无法看到程序各阶段的整体“输出”。例如,如果想查看程序中 i 的每一个取值,就无法单靠调试器做到。

断点

调试器允许在代码执行过程中将其暂停,并查看某个时刻的变量值。为此,需要在某一行代码上设置“断点”。代码运行到该断点时会暂停,此时可以检查这一刻的各个变量。

还有两种常用且实用的操作。到达断点后,你可能想看看执行当前行后会发生什么。“单步跳过(Step Over)”按钮可以让你移动到下一行。假设当前行为 dfs(0, -1),点击“单步跳过”后,调试器不会展示函数内部的执行过程,而会直接进入下一行。相反,如果点击“单步进入(Step In)”,就会进入该函数,并能在函数内部继续单步执行。

Module Progress:

PrevNext