从调试的英文(debug)就可以看出来,调试的过程就是“消灭”bug的过程
而从中文中,我们可以感受到调试就是把程序不断的运行(试),并不断的修改(调)
但是如果什么都不做的话,就搁哪儿干巴巴的运行程序,除非程序能够成精,自己把自己的错误说出来,恐怕运行几年你也调不出个所以然来
那么我们又应该用什么方法来调试呢?
这篇文章中我们就来介绍一种最简单的调试方法,只要你有编辑器和编译器,那么就可以使用这个方法调试
我们都知道,printf能够在命令行(终端)上以一定的格式来输出一些信息。如果我们能够较好的利用这个功能,我们便可以记录程序运行的结果,输出一些变量的值,通过这些信息,我们能够解决一些简单的错误
以上一章最后的那个错误程序为例,我们来简单的体会一下这种调试方法。下面是那个错误的代码:
#include <stdio.h> #include <stdlib.h> int main() { int a, b, result; int operators; printf("请输入一个算式:"); scanf("%d %c %d", &a, &operators, &b); switch(operators) { case '+': result = a + b; break; case '-': result = a - b; break; case '*': result = a * b; break; case '/': result = a / b; break; } printf("%d\n", result); return 0; }
这个程序的输出结果和我们所期望的完全不同,而我们现在还不太清楚问题出现点的地方,我们便可以在一些关键的地方加上printf,比如我们需要检查一下是否能正确的输入数据,我们便可以在使用scanf输入后添加一个printf输出,将我们刚刚输入的值打印出来。
printf("调试信息:输入的算式为%d %c %d\n", a, operators, b);
再次运行这个程序:
现在,似乎一切正常,可以说目前我们还没有获得任何有用的信息。
不过你也不要着急,调试才刚刚开始!
使用输出语句,不仅仅只能打印变量的值,还能告诉我们程序运行到了何处,下面我们可以在switch中的各个分支中插入一个puts
如此,我们就能通过观察屏幕的输出来了解到哪个分支被执行了。修改后的代码如下:
#include <stdio.h> #include <stdlib.h> int main() { int a, b, result; int operators; printf("请输入一个算式:"); scanf("%d %c %d", &a, &operators, &b); printf("调试信息:输入的算式为%d %c %d\n", a, operators, b); switch(operators) { case '+': result = a + b; puts("+"); break; case '-': result = a - b; puts("-"); break; case '*': result = a * b; puts("*"); break; case '/': result = a / b; puts("/"); break; } printf("%d\n", result); return 0; }
如此一来,只要屏幕输出了+-*/之中的任意一个,就说明对应的分支被执行了。下面让我们运行程序查看结果:
似乎并没有什么变化,这意味着我们的程序中没有任何一个分支结构被执行了!
换句话说,我们的switch条件没有一个被命中,我们的输入值不属于其中任何一种情况。
然而,一开始我们加入的printf却告诉我们输入的确实是正确的符号,我们得到了一个前后矛盾的结果!
为了弄清楚我们究竟输入了什么东西,我们可以选择使用%d而不是%c来打印operator,根据这个值我们可以知道我们输入的到底是什么东西。
为此,将第十行的%c修改为%d,我们再来看看:
operator是一个非常大的数,显然并不属于ASCII中的任何一个字符。
这说明我们问题很可能出现在输入的地方!
这次,我们将错误的可能范围缩小到了5-9行。
经过我们的检查,我们发现scanf中,我们使用了%c来输入一个字符,而储存字符的变量operator却是整型的
我们将其修改为char再来看看
啊哈,这样就对了嘛!
其实在一开始我们编译的时候,编译器就给出了如下的警告:
…\main.c|9|warning: format ‘%c’ expects argument of type ‘char *’, but argument 3 has type ‘int *’ [-Wformat]|
这个意思是说,在第九行的scanf中,%c格式所期望的参数类型是char*,而实际的类型是int*,因此发出了一个警告。为了完全明白这个警告的含义,我们需要等到学习指针,不过这个警告显然有助于我们检查程序中的错误。因此,尽管出现警告仍然可以通过编译,但是在许多情况下,警告意味着程序中可能存在潜在的错误,应当引起你的注意
这个程序并没有在运行的时候发生崩溃,而在我们学到后面的时候,由于内存操作等地方的不慎,很容易造成程序崩溃,同样是使用printf,我们可以在崩溃的前面输出一些所需的信息,来查找崩溃的原因。我们可以形象的称这个方法为“临终遗言法”,即在程序死掉的最后一刻,让它说出它的“遗言”。我们将在后面的学习中演示。
使用输出函数来帮助调试是一个很简单的方法,它不需要额外的工具。
然而正是这一点这又使它变得麻烦:你必须在许多地方加上一堆又一堆的输出语句,更恐怖的是你可能还需要在解决问题后一行一行的将他们移除……
因此人们想出了一个叫做“断言”的东西,他能使这些用于调试的语句在我们完成调试进行最后发布的时候自动的不被编译器编译进去。
我们将在后面介绍断言的使用方法。
对于规模稍大的程序,仅仅靠在程序中打印变量的值等方法是远远不够的,我们需要一个功能更为强大程序来辅助我们对程序的调试。
这种程序便是调试器(debugger)。
下一篇文章我们就要学习如何使用调试器来调试。