如何使用printf的进行调试
什么是调试
在平常开发过程中,我们难免会遇到一些bug,比如函数的返回值不符合预期,代码运行了一会就会异常挂断,变量的结果异常等等问题。
一般遇到这些问题的时候最原始的方法就是走读代码,重新过一遍逻辑然后找到代码中的逻辑问题。
但是这个方法有两个问题:
1.代码复杂的时候耗时长。如果代码很多,或者循环嵌套了很多层,这样出现问题的点可能就是其中的一行,因为占很小的一部分,所以很多时候都很难快速找到问题。
2.在检查代码的时候,由于相关知识的缺少导致无法准确的定位到问题点。在很多情况下我们很难走出自己的思维误区,这就会导致在检查的时候还是找不到问题点。又或者因为相关知识的不了解,所以找不到问题。
对此大部分语言有一个调试器的功能,比如c语言的gdb调试,但是这种方法比较复杂和耗时,而且在一些情况下可能无法使用(比如交叉编译到嵌入式板子上运行的代码),所以我在这里推荐一个更加通用的方式,通过使用打印函数(printf)来定位问题点。
printf的作用
相信很多人在学习一门编程语言的时候第一行代码就是利用打印写一个"hello world"
#include<stdio.h>
int main(void)
{
printf("Hello world!\n");
return 0;
}
运行结果如下
不难看出,printf可以在终端中给我们输出一个我们给定的值。
同时printf还可以输出一些我们关注的值,比如
#include<stdio.h>
int main(void)
{
int a = 0;
printf("a = %d\n",a);
a++;
printf("a = %d\n",a);
return 0;
}
运行结果如下
通过上面的代码我们可以知道在经过a++后,变量a从0变成了1。
也就是说我们可以通过printf来打印一些我们关注的变量值的使用,这个在循环中尤其好用,后面会举例说明。
让printf打印更多的有用信息
上面展示的代码是比较少的情况,如果在代码量比较多的情况,运行的时候本来就有很多的打印,那么如何在一堆的打印中找到自己关注的打印呢?
C语言为我们提供了两个宏:
__FILE__用以指示本行语句所在源文件的文件名
__LINE__用以指示本行语句在源文件中的位置信息
两者结合使用就可以知道printf是在什么文件下的哪一行了。
#include<stdio.h>
int main(void)
{
int a = 0;
printf("a = %d,line is %d,file is %s\n",a,__LINE__,__FILE__);
a++;
printf("a = %d,line is %d,file is %s\n",a,__LINE__,__FILE__);
return 0;
}
运行结果如下
将上面的printf进行亿点点优化,得到下面结果
#define DEBUG(format,...) do{\
printf("Info:[%s:%s(%d)]:" format "\n", __FILE__, __FUNCTION__, __LINE__, ##__VA_ARGS__);\
}while(0)
后面只需要使用DEBUG这个宏即可,不需要在结尾加换行,就可以打印关注的变量值。
如何缩小和定位bug
在编写完代码的功能的时候,我们会对功能进行测试,如果发现结果不符合预期,那么就需要思考代码到底是什么问题,这个时候我们就可以使用上面的调试宏来定位问题啦。
题目:泰勒展开:π/4≈1-1/3+1/5-1/7+1/9-…,求π的近似值,要求其最后一项绝对值大于1e-7
错误示范:
#include <stdio.h>
#include <math.h>
#define DEBUG(format,...) do{\
printf("Info:[%s:%s(%d)]:" format "\n", __FILE__, __FUNCTION__, __LINE__, ##__VA_ARGS__);\
}while(0)
int main(void)
{
int s=1,f=1;
double last=1,sum=0;
while(fabs(last)>1e-7)
{
last=s/f;
sum+=last;
s*=(-1);
f+=2;
}
printf("%f\n",sum*4);
return 0;
}
程序洋洋洒洒的就写完了,结果运行后发现结果为4.000000,按道理逻辑上是没有问题的,那是哪里有问题呢?
我们不妨在循环中加入打印来看看循环出了什么问题。于是我在while的末尾加了个DEBUG()函数对last值进行打印,看一下究竟有什么问题
#include <stdio.h>
#include <math.h>
#define DEBUG(format,...) do{\
printf("Info:[%s:%s(%d)]:" format "\n", __FILE__, __FUNCTION__, __LINE__, ##__VA_ARGS__);\
}while(0)
int main(void)
{
int s=1,f=1;
double last=1,sum=0;
while(fabs(last)>1e-7)
{
last=s/f;
sum+=last;
s*=(-1);
f+=2;
DEBUG("last = %f",last);//打印我们关注的last值
}
printf("%f\n",sum*4);
return 0;
}
通过分析不难发现第二次循环的值应该为1-1/3,last的结果应该为0.666667,但是打印却显示为0.000000,这表明last的赋值地方出现了问题,所以我们需要关注到last=s/f这一行即可。
经过分析发现,由于s和f都是int类型,所以在1/3的时候会取整为0,那我们只需要加一个强转就可以解决问题
#include <stdio.h>
#include <math.h>
#define DEBUG(format,...) do{\
printf("Info:[%s:%s(%d)]:" format "\n", __FILE__, __FUNCTION__, __LINE__, ##__VA_ARGS__);\
}while(0)
int main(void)
{
int s=1,f=1;
double last=1,sum=0;
while(fabs(last)>1e-7)
{
last=(float)s/f;
sum+=last;
s*=(-1);
f+=2;
}
printf("%f\n",sum*4);
return 0;
}
注意:由于频繁的调用打印会导致程序运行速度的下降,所以该方法只在实时性要求不高的情况用于调试,调试结束后删掉代码即可。
段错误的排查方式
printf函数不仅仅可以用来查看我们关注的变量值,同时也可以用于定位程序的异常断开问题,比如让人头痛的指针段错误问题。
思路如下:先在广撒网,如果没有思路可以随便加上一些打印,头和尾一定要有即可。
#include <stdio.h>
#include <math.h>
#define DEBUG(format,...) do{\
printf("Info:[%s:%s(%d)]:" format "\n", __FILE__, __FUNCTION__, __LINE__, ##__VA_ARGS__);\
}while(0)
int main(void)
{
DEBUG();
fun1();
DEBUG();
fun2();
DEBUG();
fun3();
DEBUG();
fun4();
DEBUG();
return 0;
}
然后分析段错误是在哪个DEBUG()之后出现的,因为DEBUG()会显示在哪一个文件的第几行,这样就可以缩小问题出现的位置了(问题大概率是在这一行DEBUG和下一个DEBUG之间)。之后只需要反复上面的操作,在两个DEBUG之间再添加多的DEBUG就可以,最终就能定位到段错误出现的哪一行代码,然后就行分析即可。
总结
在编写代码的时候出现问题在所难免,尤其是在初学的时候,如果没有独立的分析能力,那么只会请教别人帮忙解决问题对自己的能力很难有提升。
所以在遇到bug的时候不妨采加打印的方式把问题定位出来,然后进行分析或者找相关的资料,说不定又可以get到一个新的知识点。