浅谈 OI 中的调试技术
bluewindde · · 科技·工程
upd 2025-03-24 感觉我之前写的很没用,算了
前言
为什么需要调试技术?
你能保证一发 AC?
本文基于我的拙见,介绍一些 OI 中常用的调试技术。
编译警告
多一些警告总是好的。
如果你写了这样的代码:(这是结构自动绑定,C++ 17 的新特性)
// 这是 vector 存带权图的遍历方式
for (auto [v, w] : vec[u]) {
...
}
在伟大的 NOI Linux 默认的 C++ 14 下,欢迎加入紫气贵族大学!
注意一下编译警告是件十分重要的事。
你应该使用这样的编译命令:
// 此时不应该启用优化
// undefined behavior 可能会被优化隐藏掉
g++ joker.cpp -o joker.exe -std=c++14 -Wall -Wextra -Wshadow -Wconversion
// -Wconversion 检查隐式类型转换 警告消息会比较多
调试信息
所有调试信息都应当打印到标准错误流!!!
没有哪个评测系统会检查标准错误流。在代码中应当使用 cerr 输出调试信息,避免因为忘记删调试挂分。
cerr 和 cout 的使用方法一模一样。
(正是因此,你也可以把标准错误流重定向到输出文件,用标准输出流打印调试信息,但我相信没有谁这么做吧)
时间
你怎么知道程序跑了多久?
Dev-Cpp 会显示程序运行时间,但那个不准确,非 Windows 系统也用不了。
在代码中加入这两句:
#include <time.h> // 或者 <ctime>
cerr << "program run costs " << (double)clock() / CLOCKS_PER_SEC << " s" << endl;
// clock() 函数用于获取程序从开始执行以来经过的时间,除以 CLOCKS_PER_SEC 这个常量可以换算成秒
当然,clock() 函数也可以用来卡时。
内存
你怎么知道程序用了多少内存?
- 数组占用的内存可以算出来,只需要熟练掌握
C++为每种静态类型分配的内存。(用sizeof也是不错的选择,可以参考这里;对于结构体,可以看看这里)
例如,下面这段代码声明的 dp 数组占用的内存大小为
int dp[1005][1005];
在没有使用到数组中的元素时,C++ 编译器好像不会初始化它?(好像仅限于数字一类的数据,有没有 dalao 补充一下)
- 对于动态类型,在可能的内存瓶颈(大量使用 STL 的场景)处写一个暂停(
while(1);),这会为你留出足够的时间打开任务管理器查看内存占用。(别忘了删掉暂停)
Checker
遇到输出很多行的题目时,仅凭肉眼难以发现输出的错误,这时需要借助强大的计算机完成调试。
如果你在本地有评测环境,那么大可不必看这一节的内容。
VSCode 等现代 IDE
很多现代集成开发环境都支持多行文本的搜索,在 VSCode 中,可以将答案全部选中,粘贴到输出文件最后,再选中你的程序的输出,按 Ctrl+F2(更改所有匹配项)或 Ctrl+F(查找)或 Ctrl+D(添加下一个匹配项),这样就可以验证输出与答案是否一致。
当然,这种方式只能粗糙地进行全字匹配(模糊搜索可以通过正则表达式完成,但对于竞赛而言,这太复杂了),下面是两种过滤行空白符的 Checker。
C++ Checker
编译时要在命令后面附加传递 -DDEBUG 参数。
附在程序末尾,它会在程序执行完成后将你的输出与答案作比对。
要先使用 cout<<flush 或 cout<<endl 刷新输出流缓存,否则无法读取正确的程序输出。
以下代码演示了程序 task 的 Cheker。
如果题目使用 SPJ(SPJ 一般都是 C++ 写的),这种 Checker 可以方便地执行 SPJ。
#ifdef DEBUG
#include <time.h>
#include <fstream>
#include <string>
#endif
using namespace std;
signed main() {
freopen("task.in", "r", stdin);
freopen("task.out", "w", stdout);
// 这是代码
...
goto end;
end: // 编译命令传参: -DDEBUG
#ifdef DEBUG
{
cerr << "program run costs " << (double)clock() / CLOCKS_PER_SEC << " s" << endl;
ifstream out;
ifstream ans;
out.open("task.out", ios::in);
ans.open("task.ans", ios::in);
int line = 1;
string str;
string std;
while (out >> str && ans >> std) {
string str_real;
string std_real;
{ // 过滤行首尾空格
int i = 0;
for (; i < str.size(); ++i)
if (str[i] != ' ') break;
int j = str.size() - 1;
for (; j >= 0; --j)
if (str[j] != ' ') break;
str_real = str.substr(i, j - i + 1);
}
{
int i = 0;
for (; i < std.size(); ++i)
if (std[i] != ' ') break;
int j = std.size() - 1;
for (; j >= 0; --j)
if (std[j] != ' ') break;
std_real = std.substr(i, j - i + 1);
}
if (str_real != std_real) {
cerr << "line " << line << " : read `" << str << "` expect `" << std << "`." << endl;
}
++line;
}
if (out >> str) {
if (str.size()) {
cerr << "output longer than answer." << endl;
}
}
if (ans >> std) {
if (std.size()) {
cerr << "output shorter than answer." << endl;
}
}
cerr << "end." << endl;
}
#endif
return 0;
}
Python Judger
在 Linux 下使用要修改编译目录和编译器。
使用方法:程序放在考试目录下,name 变量设为程序名,subtaskid 变量设为测试点编号(如果没有编号则设为空字符串,研究源码可以发现这个变量直接拼接在程序名后面)。
如果需要编译并运行,可以将 COMPILE_PROGRAM 常量设为 True。
形象地,文件结构如下:
root ----------------
| joker --------------
| joker.cpp
| joker.in
| joker.ans
| joker.out
| ...
cheker.py
...
对于上面的文件结构,有如下的 checker.py:
import os
import subprocess
import sys
try:
os.chdir(os.path.dirname(sys.argv[0]))
except OSError:
pass
COMPILE_PROGRAM = False
compiler_dir = "C:/MinGW64/bin/"
compiler = "g++.exe"
compile_command = "-static-libgcc -std=c++2a -O2 -lstdc++"
name = "joker"
subtaskid = ""
if COMPILE_PROGRAM:
source_path = os.getcwd() + "/" + name + "/" + name + ".cpp"
out_path = os.getcwd() + "/" + name + "/" + name + ".exe"
cmd = "c: ; & \"%s/%s\" \"%s\" -o \"%s\"" % (compiler_dir, compiler, source_path, out_path)
for i in compile_command.split(sep=" "):
if i:
cmd += " \"%s\"" % i
print(os.getcwd(), "> ", cmd, sep="")
if not os.system(cmd):
print("compiled successfully.")
# os.startfile(out_path)
os.popen
else:
print("compile failed.")
exit()
try:
with open(os.getcwd() + "/" + name + "/" + name + subtaskid + ".out", "r", encoding="utf-8") as file:
out_content = file.read().splitlines()
except OSError:
print("output file not found.")
exit()
try:
with open(os.getcwd() + "/" + name + "/" + name + subtaskid + ".ans", "r", encoding="utf-8") as file:
ans_content = file.read().splitlines()
except OSError:
print("standard answer file not found.")
exit()
if not out_content:
print("output too short.")
exit()
for i in range(len(out_content)):
try:
oout = out_content[i].strip()
aans = ans_content[i].strip()
except IndexError:
print("output longer than standard answer at line %d." % i)
break
else:
if oout != aans:
print("output differ with standard answer at line %d:" % i)
print(" out: ", oout, ";", sep="")
print(" ans: ", aans, ";", sep="")
print(" failed.")
if i < len(ans_content) and i == len(out_content):
print("output shorter than standard answer.")
print("end.")
结语
尽管有如此多的调试技术,但仍应当以一发 AC 要求自己。