如何优雅的调试

本文最后更新于:2022年4月27日 晚上

如何优雅的调试

第零步:编译开关

善用$-Wall$编译开关,Dev-C++在**工具-编译选项-代码生成/优化-代码警告-显示最多警告信息(-Wall)**选择Yes。$-Wall$开关允许编译器输出更多warning信息,包括但不仅限于:不同数据类型之间的比较,if中少了个等号,未使用的变量名,函数返回值错误。

第一步:瞪眼

先根据自己的输出结果推断哪里可能出了问题,再重点检查那部分代码。

其中一个非常有用的方法,就是通过人脑,代入一组输入,来模拟计算机的运行,一个语句一个语句运行下去,然后思考运行到这句语句为止是否有跟我想要实现的不一样的地方。需要特别注意的是,手动模拟代码的时候,应尽可能少的想当然,尽可能一步一步跟着代码走。

举个例子,在手写链表的时候,如果发现输出和输入正好相反,就是

1
2
Input: 10 3 7 12 1 -1
Output: 1 12 7 3 10

那么就应该着重去看有关输出的代码出现了什么问题。

之后,重要的是学会看编译器的报错信息。以下为一个例子

1
2
3
4
5
6
camp903.cpp: In function 'void solve()':
camp903.cpp:67:15: warning: unknown conversion type character 'l' in format [-Wformat=]
67 | printf("%lld\n", ans);
| ^
camp903.cpp:67:12: warning: too many arguments for format [-Wformat-extra-args]
67 | printf("%lld\n", ans);

说明了什么?

在函数$void\ solve()$里面,代码中的67行,对应着printf(“%lld\n", ans);这条语句,出现了一个$warning$信息,意思是未知的标识符$l$.

再举一个例子

1
2
3
4
5
6
.\camp904.cpp: In function 'void dfs_lca(int, int)':
.\camp904.cpp:87:16: error: invalid conversion from 'int (*)[32]' to 'int' [-fpermissive]
87 | fa[u][0] = fa;
| ^~
| |
| int (*)[32]

意思就是在函数$void\ dfs_lca(int, int)$中,代码中的第$87$行$16$列,出现了一个非法的从$int (*)[32]$转换到$int$的错误

为什么会有非法的类型转换?因为这是一个赋值操作,赋值给了一个类型不匹配并且无法强制转换到目标类型的值,所以解决方法就是修改后面的变量。

总而言之,就是通过编译器给的具体错误定位,包括函数,第几行第几列,以及什么类型的错误,也就是$error:$后面的英文,翻译懂吧,翻译,来确定错误点,然后修改错误

第二步:静态查错

到这,就是编译器无法发现的错误了,也就是所谓的“逻辑错误”。

那什么是静态查错?

就是在完成的函数中,在某些地方增加$printf$语句输出变量的值,来观察整个程序运行过程中,变量的值的改变的行为是否符合我们的预期。由于是等待整个程序运行完成后一并查看变量在所有运行流程中的值,相对于动态,也就是程序边运行边查看变量的值,的查错方法,所以称为静态查错。

一般而言,输出变量的值的位置,会被添加在读入用户输入后、某段特定功能代码执行过程中(例如循环里面)和执行后、某个函数执行后,以此一步步缩小错误点,最终定位到错误发生的位置。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
for(int i = 290; i >= 175; i--) {
if(v[i].size() == 0) continue;
//printf("%d\n", v[i].size());
if(v[i].size() <= k) ans += v[i].size();
else {
int great = 0;
rep(j, 0, v[i].size() - 1) {
if(v[i][j] >= s) great++;
}
if(great + k >= v[i].size()) ans += v[i].size();
else ans += (great + k);
}
}

在这段代码中,我怀疑$v[i].size()$可能出现了什么问题,我就在每次循环运行的时候让他输出$v[i].size()$,也就是注释掉的那行代码,观察是否跟我预期的结果一样。倘若$v[i].size()$不符合预期,就可以往前思考是什么修改了$v[i].size()$,进而一步步定位错误点。

当然,输出可以变得更加user-friendly一点,比如也顺带输出$i$的值,便可以知道是在那一层循环里输出了对应的值。

也可以多次使用这种方法,就是在程序中添加多个$printf$语句,来观察某一个部分的运行情况以及程序整体的运行情况。

到这为止,基本上可以解决99%的问题了,但有些问题是程序整个部分有问题导致的,他每一块都是对的,但加到一起就不对了,或者是一些更加细小的错误,或者是一些运行时错误(即RE)导致的程序异常退出问题,可以借助GDB定位错误点。

第三步:GDB

在此我非常推荐使用Windows的各位安装一个Ubuntu WSL,在Windows应用商店里可以找到。

因为在Linux下,有些RE是会被输出在控制台的,比如$Segmentation\ fault$.

也可以使用DEV-C++自带的调试器,他也是调用了GDB,但我对这个东西印象非常不好(在NOIP2018的考场上他就没有正常运行起来过!)。使用方法也比较简单,在DEBUG模式下编译,右键行号以添加断点,在下方的“调试”菜单中添加查看变量,在这贴上一篇博客,里面比较详细的介绍了DEV-C++自带的调试器的用法Dev C++调试程序方法详解

接下来介绍命令行(终端)下的GDB的简要用法

第零步:配置环境

右键“此电脑”-属性-高级系统设置-环境变量-用户变量中双击Path打开,点击新建,将g++编译器的文件路径(例如C:\MinGW\bin)复制进去,然后一路确定下去。

在WSL下,输入sudo apt-get install gcc安装gcc和g++环境

第一步:生成文件

可以使用DEV-C++编译,也可以在cmd(或者终端)中先$cd$到$c++$文件所在路径,比如cd c:\Users\hands\Desktop.

随后输入g++ ./$你的文件名$.cpp -o a -g -Wall -std=c++11

生成完后,再输入gdb ./a.exe(如果是Ubuntu的话就是gdb ./a),就可以开始使用GDB了

第三步:熟悉命令

下面只列出一些用的较多的命令,其他更多功能可以自行搜索(

命令 功能 使用方法
r(run) 运行程序 直接输入r,表示开始调试
b(break) 添加断点 b 行数,例如“b 100”,在b行让程序停止运行,便于查看变量信息
d(delete) 删除断点 d 行数,删除该行处已经设置过的断点
p(print) 输出变量 p variable,输出变量variable的值,可以是结构体,数组,地址
n(next) 执行下一行 直接输入n,执行下一行内容,不管内容多么复杂
s(step) 执行下一行 直接输入s,与上面的区别是如果下一行是函数会进入函数,而不是直接执行完函数
c(continue) 继续 直接输入c,执行到下一个断点处后停止
i(info) 查看信息 一些常用的subcommand是:info local(查看当前局部变量),info symbol(查看全局变量,需要加上地址)
quit 退出调试 退出调试

需要注意的是,上述命令,是在非程序要求用户输入的时候输入的,也就是出现**(gdb)**的时候输入

当然这只是一些入门用法,GDB完全可以变得更加强大,进阶用法可以在各类博客轻松找到

第四步:自己尝试

自己动手试试,一切就会变得熟悉起来的。

命令不是重点,会用,能达成调试的目的就行。

重要的是观察变量的变化,动态的变化,程序运行过程中的变化,来定位错误点

如果用不惯GDB也可以转头去用DEV-C++的调试器(

相比于静态查错,GDB可以帮助我们更好的观察出一些错误,而不用对着所有运行时变量反向推理是哪一步出了什么问题。

写在最后

调试是写程序过程中非常非常重要的事情,最好是自己来完成这最后一步,熟练了debug才能更好的理解程序的运行,能力也就提高了。

比如瞪眼可以帮助理解程序如何运行,进一步厘清自己的思路

静态可以帮助理解程序内部的联系

GDB可以观察到更深层次的运行过程

debug也是提升能力的一个必不可少的关键步骤,会写也会调试,那才能说是真正学习了编程。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!