A.3 编译器和调试器
既然编译器和调试器都是程序,执行方法和普通程序大致相同。在安装时,系统会自动把编译器和调试器程序所在路径加到搜索路径中,因此在执行时不必像./gcc这样加上路径名。
A.3.1 gcc的安装和测试
尽管在现场比赛中,编译器都已安装好,但如果平时练习,一般需要自己安装。如果使用Linux,在安装操作系统时即可选择安装gcc、g++、binutils等包,但若要在Windows中使用C/C++语言,需要手工安装编译器。
本书推荐使用MinGW环境下的gcc,它的好处是和Linux下的gcc一致性较好,而且是免费的。可以到www.mingw.com中下载最新的安装包,然后在安装时选择g++编译器。
安装完毕后,在命令行中执行gcc命令。如果显示gcc: no input files,则安装成功;如果提示不存在这个命令,可能是因为没有把gcc所在目录加到搜索路径中。可以双击控制面板的“系统”图标,并在“高级”选项卡中设置环境变量。在“系统变量”中找到“PATH”(大小写无所谓),它就是可执行程序的搜索路径。请在它的最后加入MinGW安装路径的bin子目录,如C:\MinGW\bin(在安装时记住MinGW的安装路径),保存后重新启动命令行,gcc就应该可以正常工作了。
A.3.2 常见编译选项
先建立一个test.c,试试常见的编译选项。
#include<stdio.h>
main()
{
int a, b;
scanf("%d%d", &a, &b);
int c = a+b;
printf("%d%d\n", c);
}
编译一下,命令为gcc test.c。程序没有输出,代表一切均好。检查目录(Windows下用dir,Linux下用ls),会发现多了一个a.exe(Windows)或a.out(Linux),这就是程序的编译结果。
gcc test.c-o test命令会让编译出的可执行程序名为test.exe(Windows)或test(Linux)。这样,就能用test(Windows)或./test(Linux)方式运行程序。
也许读者已经看出了上述代码中的一些问题,不过当程序更加复杂时,人眼就不一定能快速找到错误了。在这样的情况下,编译选项能起作用:gcc -test.c -o test -Wall。这次,编译器指出了3个警告:main函数没有返回类型、没有返回值、printf的格式字符串可能有问题。还可以进一步用-ansi-pedantic,它会检查代码是否符合ANSI标准(-ansi只是判断是否和ANSI冲突,而-pedantic更加严格)。它进一步指出了上述代码中的另外一个问题:ANSI C中不允许临时声明变量,而必须在语句块的首部声明变量。
在C语言中,另一个常用的编译选项是-lm,它让编译器连接数学库,从而允许程序使用math.h中的数学函数。C++编译器会自动连接数学库,但如果程序的扩展名是.c,且不连接数学库,有时会出现意想不到的结果。
另一个有用的选项是-DDEBUG,它在编译时定义符号DEBUG(可以换成其他,如-DLOCAL将定义符号LOCAL),这样,位于#ifdef DEBUG和#endif中间的语句会被编译。而在通常情况下,这些语句将被编译器忽略(注意,不仅是不会执行,连编译都没有进行)。
可以用-O1、-O2和-O3对代码进行速度优化。一般情况下,直接编译出的程序比用-O1编译出的程序慢,而后者比-O2慢。尽管理论上-O3编译出的程序更快,但由于某些优化可能会误解程序员的意思,一般比赛中不推荐使用。另外,如果你的程序中有一些不确定因素(如使用了未初始化的变量),运行结果可能会和编译选项有关——用-O1和-O2编译出的程序也许不仅是速度有差异,答案甚至都有可能不同!当然,这种情况出现的前提是程序有瑕疵。如果是一个规范的程序,运行结果不会和优化方式有关。
既然编译选项可以影响程序的行为,在正规比赛中,组织方应提前公布编译选项。如果没有公布,选手最好尽早询问。
A.3.3 gdb简介
gdb尽管只是一个文本界面的调试器,但功能十分强大。不管是Linux和Windows下的MinGW,gcc和gdb都是最佳拍档。
gdb的使用方法很简单——用gcc编译成test.exe之后,执行gdb test.exe即可。不过,如果要用gdb调试,编译时应加上-g选项,生成调试用的符号表。
接下来使用l命令,将看到部分源程序清单。如果用l 15,将会显示第15行(以及它前后的若干行)。除此之外,还可以用函数名来定义,如l main将显示main函数开头的附近10行。如果不加参数执行l,将显示下10行;list -将显示上10行。所有这些操作都可以用help list命令来查看。gdb中的命令可以简写(例如list简写成l),大家可以多尝试(提示:试一下命令的前若干个字母)。
运行程序的命令是r(run),但会一直执行到程序结束。如何让它停下来呢?方法是用b(break)命令设置断点。例如,b main命令将在main函数的开始处设置一个断点,则用r命令执行时会在这里停下来。如果想继续运行,请用c(continue)命令,而不是继续用r命令。和list命令类似,b命令既可以指定行号,也可以在指定函数的首部停下来。笔者在调试很多程序时都是以命令b main和r开头的。
如果希望逐条语句地执行程序,不停地用b和c命令太麻烦。gdb提供了一些更加方便的指令,其中最常用的有两个:next(简写为n)和step(简写为s)。其作用都是执行当前行,区别在于如果当前行涉及函数调用,则next是把它作为一个整体执行完毕,而step是进入函数内部。尽管n和s都只有一个字母,但有时还是稍显繁琐。在gdb中,如果在提示符下直接按Enter键,等价于再次执行上一条指令,因此如果需要连续执行s或者n,只需要第一次输入该命令,然后直接连按Enter键即可。另外,和命令行一样,可以按上下箭头来使用历史记录。
另一个常用命令是until(简写为u),让程序执行到指定位置。例如,u 9就是执行到第9行,u doit就是执行到doit函数的开头位置。
停下来以后便打印一些函数值,看看是否和想象的一致。用p(print)命令可以打印出一些变量的值,而info locals(可以简写为i lo)可以显示所有局部变量。如果希望每次程序停下来,则可以用display(简写为disp)命令。例如,display i+1就可以方便地读取i+1的值。它往往和n、s和u等单步执行指令配合使用。如果需要列出所有display,可以用info display(简写为i disp);还可以删除或者临时禁止/恢复一些display,相应的命令为delete display(d disp)、disable display(dis disp)和enable display(en disp)。类似地,也可以根据断点编号删除、禁止和恢复断点,还可以用clear(cl)命令,像b命令一样根据行号或者函数名直接删除断点。
在多数情况下,灵活运用上述功能已经能高效地调试程序了。下面把涉及的命令列出,供读者参考,如附表A-2所示。
附表A-2 gdb常见命令
| 简写 | 全称 | 备注 |
| l | list | 显示指定行号或者指定函数附近的源代码 |
| b | break | 在指定行号或者指定函数开头处设置断点。如b main |
| r | run | 运行程序,直到程序结束或者遇到断点而停下 |
| c | continue | 在程序中断后继续执行程序,直到程序结束或者遇到断点而停下。注意在程序开始执行前只能用r,不能用c |
| n | next | 执行一条语句。如果有函数调用,则把它作为一个整体 |
| s | step | 执行一条语句。如果有函数调用,则进入函数内部 |
| u | until | 执行到指定行号或者指定函数的开头 |
| p | 显示变量或表达式的值 | |
| disp | display | 把一个表达式设置为display,当程序每次停下来时都会显示其值 |
| cl | clear | 取消断点,和b的格式相同。如果该位置有多个断点,将同时取消 |
| i | info | 显示各种信息。如i b显示所有断点,i disp显示display,而i lo显示所有局部变量 |
如果对上述解释有疑问,可输入help以获得详尽的帮助信息。
A.3.4 gdb的高级功能
gdb的功能远不止刚才所讲述的那些。尽管很多功能是专为系统级调试所设,但还有很多功能也能为算法程序的调试带来很大方便。
首先是栈帧的相关命令,其中最常用的是bt,其他命令可以通过help stack来学习。接下来是断点控制命令。commands(comm)命令可以指定在某个断点处停下来后所执行的gdb命令,ignore(ig)命令可以让断点在前count次到达时都不停下来,而condition则可以给断点加一个条件。例如,在下面的循环中:
10 for(i = 0; i < n; i++)
11 printf("%d\n", i);
首先用b 11设置断点(假设编号为2),然后用cond 2 i==5让该断点仅当i=5时有效。这样的条件断点在进行细致的调试时往往很有用。
另外,gdb还支持一种特殊的断点——watchpoint。例如,watch a(简写为wa a)可以在变量a修改时停下,并显示出修改前后的变量值,而awatch a(简写为aw a)则是在变量被读写时都会停下来。类似地,rwatch a(rw a)则是在变量被读时停下。
最后需要说明的是,gdb中可以自由调用函数(不管是源程序中新定义的函数还是库函数)。第一种方法是用call命令。例如,如果想给包含10个元素的数组a排序,可以像这样直接调用STL中的排序函数call sort(a, a+10)。
遗憾的是,如果真的做过这个实验,会发现刚才所说完全是骗人的。gdb会显示不存在函数sort。怎么会这样呢?如果学过宏和内联函数就会知道,很多看起来是函数的却不一定真的是函数,或者说,不一定是调试器识别的函数。为了在gdb中调用sort,可以将它打包:
void mysort(int*p, int*q)
{
sort(p, q);
}
这样,就可以用call mysort(a, a+10)来给数组a排序了。print、condition和display命令都可以像这样使用C/C++函数。例如,可以用p rand()来输出一个随机数,或是专门编写一个打印二叉树的函数,然后在print或者display命令中使用它,还可以编写一个返回bool值的函数,并作为断点的条件。
至此是不是觉得gdb很强大呢?注意,过分地依赖于gdb的调试功能让敏锐的直觉变得迟钝。事实上,笔者建议读者尽量只使用A.3.3节提到的基本功能,甚至尽量不要使用gdb——用输出中间变量的方法,加上直觉和经验来调试算法程序。如果是这样,编程速度和准确性将大大提高。