浅谈 OI 中的调试技术

· · 科技·工程

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 输出调试信息,避免因为忘记删调试挂分。

cerrcout 的使用方法一模一样。

(正是因此,你也可以把标准错误流重定向到输出文件,用标准输出流打印调试信息,但我相信没有谁这么做吧)

时间

你怎么知道程序跑了多久?

Dev-Cpp 会显示程序运行时间,但那个不准确,非 Windows 系统也用不了。

在代码中加入这两句:

#include <time.h>  // 或者 <ctime>
cerr << "program run costs " << (double)clock() / CLOCKS_PER_SEC << " s" << endl;
// clock() 函数用于获取程序从开始执行以来经过的时间,除以 CLOCKS_PER_SEC 这个常量可以换算成秒

当然,clock() 函数也可以用来卡时。

内存

你怎么知道程序用了多少内存?

例如,下面这段代码声明的 dp 数组占用的内存大小为 1005\times1005\times32\ \texttt{Byte}\approx30.8\ \texttt{MiB}

int dp[1005][1005];

在没有使用到数组中的元素时,C++ 编译器好像不会初始化它?(好像仅限于数字一类的数据,有没有 dalao 补充一下)

Checker

遇到输出很多行的题目时,仅凭肉眼难以发现输出的错误,这时需要借助强大的计算机完成调试。

如果你在本地有评测环境,那么大可不必看这一节的内容。

VSCode 等现代 IDE

很多现代集成开发环境都支持多行文本的搜索,在 VSCode 中,可以将答案全部选中,粘贴到输出文件最后,再选中你的程序的输出,按 Ctrl+F2(更改所有匹配项)或 Ctrl+F(查找)或 Ctrl+D(添加下一个匹配项),这样就可以验证输出与答案是否一致。

当然,这种方式只能粗糙地进行全字匹配(模糊搜索可以通过正则表达式完成,但对于竞赛而言,这太复杂了),下面是两种过滤行空白符的 Checker。

C++ Checker

编译时要在命令后面附加传递 -DDEBUG 参数。

附在程序末尾,它会在程序执行完成后将你的输出与答案作比对。

要先使用 cout<<flushcout<<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 要求自己。