DFS / 递归调试助手

· · 科技·工程

DFS / 递归调试助手

:::success[修订日志] [2026-07-26] 第 3 版。\ [2026-07-16] 第 2 版。\ [2026-01-31~2026-04-01] 第 1 版(疑似为 shi)。 :::

本文小部分借鉴 C++ 调试模板这篇文章的一些想法,若有侵权、纰漏或不妥的地方,请各位指出,我会尽量及时更改。

本文由洛谷题解格式化工具格式化。

0. 前言

0.1 关于工具的定位

在我们编写 DFS / 递归程序时,常见的调试方式有两种:

  1. 在代码中打印中间变量 —— 但是难以直观反映调用层级。

  2. 使用 VSCode / GDB 等调试器单步跟踪 —— 但是只能看到当前时刻的调用栈,无法纵览整个递归过程的完整流程,且在有些情况下无法使用(比如在在线编辑器中写代码,而这些编辑器大多只提供运行的功能)。

本工具的设计目标是填补上述两种方法的空白:将整个递归树从根到叶一次性完整打印,以缩进展示调用层级,帮助开发者从全局审视递归结构是否符合预期,快速定位逻辑错误与冗余搜索

0.2 关于代码

本文的代码涉及一些可能不太常用的 C++11 的语言知识,如果不懂可以先去学习或直接划到下面查看使用方法并获取代码。

:::info[理解本文的代码所需的前置知识]{open}

  1. 模板
  2. 模板参数包(通俗一点:数量可变的模板参数)

1. 代码展示

C++11 及以上才可用!

:::success[代码]{open}

#include <iostream>
#include <utility>

#ifdef ENABLE_DEBUG

void print_split() {}
template <typename T1, typename... RestTs>
void print_split(T1 &&arg1, RestTs &&...rest_args) {
    std::cerr << std::forward<T1>(arg1) << ' ';
    print_split(std::forward<RestTs>(rest_args)...);
}

class DFSLogger {
  private:
    static long long stack_num;
    static long long call_count;

  public:
    template <typename... Args>
    DFSLogger(Args &&...args) {
        ++call_count;
        print_split(call_count, std::string(4 * stack_num, ' '),
                    std::forward<Args>(args)...);
        std::cerr << std::endl;
        ++stack_num;
    }
    ~DFSLogger() { --stack_num; }
    static long long get_stack_num() { return stack_num; }
    static long long get_call_count() { return call_count; }
};

long long DFSLogger::stack_num = 0;
long long DFSLogger::call_count = 0;

#define dfslog_novarname(...) DFSLogger abcdefg__(__VA_ARGS__)
#define dfslog(...)           dfslog_novarname(#__VA_ARGS__, __VA_ARGS__)

#else

#define dfslog_novarname(...) void(0)
#define dfslog(...)           void(0)

#endif

:::

2. 使用方法

首先,调试助手提供了一个在编译时控制是否输出调试信息:ENABLE_DEBUG,定义它就会开启输出调试信息,不定义它就不会开启。

:::info[关于如何定义此宏]{open} 可以通过在编译时向 GCC / Clang 编译器传递 -DENABLE_DEBUG调试助手的代码前添加独立的一行 #define ENABLE_DEBUG 来定义此宏。 :::

定义完上述的 ENABLE_DEBUG 宏后,在你的深搜 / 递归函数的最开始添加一行代码 dfslog(<你要输出的变量>);,就可以输出每次函数调用时你要输出的变量的名称和值,一并显示函数的调用流程和次数,以方便调试。

函数的调用流程通过缩进来显示,一行中输出的缩进个数 +1 等于当时此函数的调用栈的层数(即此函数当前有多少次调用还没退出)。

注意:不要把这行代码写到 if-else 条件分支或循环中去,也尽量避免在同一程序的多个递归中使用。

:::success[举一个简单的例子] 下面代码定义了一个计算斐波那契数列第 n 项的函数 dfs_fib,在其中添加 DFS 调试助手,主函数调用它计算数列的第 7 项。

Code 2

// ... (前面的代码)

int dfs_fib(int x) {
    // 添加这一行。
    dfslog(x);

    // 下面是正常计算斐波那契数列的代码。
    if (x < 2) {
        return 1;
    }
    return dfs_fib(x - 1) + dfs_fib(x - 2);
}

int main() {
    std::cout << dfs_fib(7) << std::endl;
    return 0;
}

运行这段代码会输出以下内容:

1  x 7 
2      x 6 
3          x 5 
4              x 4 
5                  x 3 
6                      x 2 
7                          x 1 
8                          x 0 
9                      x 1 
10                  x 2 
11                      x 1 
12                      x 0 
13              x 3 
14                  x 2 
15                      x 1 
16                      x 0 
17                  x 1 
18          x 4 
19              x 3 
20                  x 2 
21                      x 1 
22                      x 0 
23                  x 1 
24              x 2 
25                  x 1 
26                  x 0 
27      x 5 
28          x 4 
29              x 3 
30                  x 2 
31                      x 1 
32                      x 0 
33                  x 1 
34              x 2 
35                  x 1 
36                  x 0 
37          x 3 
38              x 2 
39                  x 1 
40                  x 0 
41              x 1 
21
其中最后一行是正常输出结果 21,即斐波那契数列第 7 项,前面就是调试助手输出的调试信息了。

3. 解析

3.1 print_split 函数

:::info[放一个这个函数的副本]

void print_split() {}
template <typename T1, typename... RestTs>
void print_split(T1 &&arg1, RestTs &&...rest_args) {
    std::cerr << std::forward<T1>(arg1) << ' ';
    print_split(std::forward<RestTs>(rest_args)...);
}

:::

函数作用:将任意数量的参数以空格为分割输出。

由于 std::cerr<< 语法限制,每次只能输出一定数量的变量(至少 C++17 前是这样的),包展开无法直接输出所有参数,所以我们想到每次只输出第一个参数(代码中的 arg1),用其余的参数(代码中的 rest_args)再次调用本函数递归处理

副本中第 1 行无参数的 print_split 函数是递归的终止条件:没有参数,不做任何事。

:::info[关于 std::forward] 不理解的话看的时候忽略它就行。

作用:用于原样保留原来参数的左右值属性,可以稍微提升一点性能(因为防止了移动语义失效)。

3.2 DFSLogger

:::info[放一个这个类的副本]

class DFSLogger {
  private:
    static long long stack_num;
    static long long call_count;

  public:
    template <typename... Args>
    DFSLogger(Args &&...args) {
        ++call_count;
        print_split(call_count, std::string(4 * stack_num, ' '),
                    std::forward<Args>(args)...);
        std::cerr << std::endl;
        ++stack_num;
    }
    ~DFSLogger() { --stack_num; }
    static long long get_stack_num() { return stack_num; }
    static long long get_call_count() { return call_count; }
};

long long DFSLogger::stack_num = 0;
long long DFSLogger::call_count = 0;

:::

这个类是实现输出调试信息的核心部件。

做到我们想要的在输出变量的同时显示调用流程这个效果,我们第一个想到的做法可能是:

  1. 在进入函数时打印指定数量的缩进和要输出的变量,并增加输出的缩进,表示入栈。
  2. 在离开函数时减少输出的缩进,表示出栈。

但是,这种做法要在每个 return 前加上减少输出的缩进的语句,麻烦,又容易忘记添加(别问我怎么知道的)

于是回顾做法,我们又想到了:定义在函数体开头的对象的生命周期贯穿整个函数,其构造函数会在进入函数时被调用,其析构函数会在离开函数时被调用。

所以我们创建 DFSLogger 这个类,用于进行上述操作。

DFSLogger 类的数据成员

DFSLogger 类有两个私有静态成员:

  1. stack_num 表示每次函数调用栈的大小。
  2. call_count 表示调用类构造函数的次数(同时也代表要调试的递归函数的调用次数)。

DFSLogger 类的构造函数

DFSLogger 类的构造函数是一个模板,用于支持任意个数的参数的输出,它会输出函数的调用过程和调用次数。

它执行以下步骤:

  1. call_count 增加 1,表示又调用了一次构造函数。
  2. call_count 开头的空格(即 4 * stack_num 个空格)和参数转发到 print_split 函数进行输出。
  3. stack_num 增加 1,表示要调试的递归函数又被调用了一次。

DFSLogger 类的析构函数

DFSLogger 类的析构函数将 stack_num 减少 1,调试的递归函数退出了一次。

现在可以解释上面说“不要把这行代码写到 if-else 条件分支或循环中去”的原因了:

DFSLogger 类的其他成员函数

写了约等于没写,正常没人用到,可跳过。

DFSLogger 类有两个静态成员函数:

  1. get_stack_num,用于获取每次函数调用栈的大小。
  2. get_call_count,用于获取调试的递归函数的调用次数。

3.3 dfslog

DFSLogger 类包装的更好看罢了。

:::info[放一个这个宏的副本]

#define dfslog_novarname(...) DFSLogger abcdefg__(__VA_ARGS__)
#define dfslog(...)           dfslog_novarname(#__VA_ARGS__, __VA_ARGS__)

:::

dfslog_novarname 宏:定义一个 DFSLogger 对象,并原样转发参数到其构造函数。

注意:由于这个宏定义的对象占了 abcdefg__ 这个变量名,所以这个函数内不能再用这个名字定义变量了。(其实应该没人会给变量定义成这个名吧)

dfslog 宏:对 dfslog_novarname 的再包装,添加第一个参数为把调用参数转成字符串字面量,方便大家知道自己输出了什么变量。

4. 结语

这个调试助手十分的简陋,只有一些基本功能,但是对于简单的调试我觉得够用了,欢迎各位来提供更好的意见。

感谢您的阅读!