本文只是提供一种思路,一种个人经验,旨在用一种很粗俗也比较接地气的方式,提示菜鸟,为什么、以及怎样在C语言程序中引入debug宏,养成并保持良好习惯。
面试被问到调试的问题,
我说:初期printf,后来gdb,
又问:printf怎么搞?直接改程序?
回答:是~!
他没提,我也知道,应该用debug宏,但真的是没怎么用那东西,也没想解释太多,回想了一下不用debug宏的原因:
我一个小程序试很多东西,改一会,思路和程序结构和参数设置全变样了,为了便捷地测试一些想要的C语言特性或运行结果,不停的在源程序上加对照组,改功能,相当于版本1、版本2、版本x同体~~~~printf不改怎么能行呢,给我打印个X出来看看对错——X刚才就不存在~!
还有一个不用debug宏的原因就是,某种程度上,debug宏开关的理念和把printf注释掉是一样的(个人理解)。
不过借口归借口,还是写的程序太小了,大一点的工程不用就不现实了,也不够“高大上”,话说回来,频繁改、程序小就是不用的借口了么?
恰巧昨天看了一道C语言面试题,认为至少那个网页给的答案是错的,于是想测试一下,题目如下:
请问下面程序有什么错误?
int a[60][250][1000],i,j,k;
for(k=0;k<=1000;k++)
for(j=0;j<250;j++)
for(i=0;i<60;i++)
a[i][j][k]=0;
答案:把循环语句内外换一下
个人认为:错误关键在于k <= 1000的那个等于号——越界了。跟顺序没关系。
==================================================================================================================
另外,即使排除k <= 1000造成的数组越界,linux下仍然segmentation fault(core dumped),1000*250*60*4byte==60000000B,60MB左右,按估算空间照内存空间大小差得远呢,不知道为什么,是栈不够?反正我把1000改成100就行了~!也许某些我不知道的原因,这样算不对!不过,这都是另一个或是另两个话题了,本次是对上边的那个“答案”质疑,想办法证明是错的,主要是为了在过程中引入debug宏。
==================================================================================================================
版本1:贴“答案”给出的方法,只改for循环的顺序:
int a[60][250][1000],i,j,k;
for(i = 0;i< 60;i++)
for(j=0;j < 250;j++)
for(k = 0;k < =1000;k++)
a[i][j][k] = 0;
for(i= 0;i < 60;i++)
for(j=0;j < 250;j++)
for(k = 0;k < =1000;k++)
printf("%d\t",a[i][j][k]);
明显的不行~!
版本2.0:首先,需要把那个k<= 1000改成k < 1000,来验证我的想法,证明“参考答案”错误。
#include<stdio.h>
main(){
inta[60][250][1000],i,j,k;
for(k = 0;k< 1000;k++)
for(j=0;j < 250;j++)
for(i = 0; i < 60;i++)
a[i][j][k] = 0;
for(i = 0;i < 60;i++)
for(j=0;j < 250;j++)
for(k = 0;k < 1000;k++)
printf("%d\t",a[i][j][k]);
}
结果segmentation fault(core dumped)了。
版本2.1:小修改。直接猜到了方向,把1000改成100了,这下程序运行正常。
版本3: k < 100,赋值语句从0改成i+j+k了,for内层加上大括号,打印也升级为可读性更强的格式~
inta[60][250][100],i,j,k;
for(i = 0;i< 60;i++)
for(j=0;j < 250;j++)
for(k = 0;k < 100;k++){
a[i][j][k] = i+ j + k;
printf("a[%d][%d][%d] = %d\t",i,j,k,a[i][j][k]);
}
这些看起来很自然,但是,如果这些都是在一个源文件内改的,那会有多麻烦,而且原来的还找不回来,可能这里有比较好的一个法子,就是/*注释代码段*/
但是那样依然比较麻烦,开关的灵活性稍差,还有不小心注释错了范围的问题。其实即使有这点问题,也依然能用,但是既然已经这么麻烦了,何不尝试引入宏,养成一个比较有用的好习惯呢?因为这只是小test,正经的程序要大得多。
合体版本:
#include<stdio.h>
//通过宏激活三个版本,分别是数组越界,数组不越界但是仍然core dump,和一个改小数组的可执行(完成)版本。
//#define WRONG_H_
//#define K_EQ_1000_H_
#define K_EQ_100_H_
main(){
#ifdef WRONG_H_
inta[60][250][1000],i,j,k;
for(i = 0;i< 60;i++)
for(j=0;j < 250;j++)
for(k = 0;k <= 1000;k++){
a[i][j][k] = i+ j + k;
printf("a[%d][%d][%d] = %d\t",i,j,k,a[i][j][k]);
}
#endif
#ifdef K_EQ_1000_H_
inta[60][250][1000],i,j,k;
for(k = 0;k< 1000;k++)
for(j=0;j < 250;j++)
for(i = 0; i < 60;i++){
a[i][j][k] = i+ j + k;
printf("a[%d][%d][%d] = %d\t",i,j,k,a[i][j][k]);
}
#endif
#ifdef K_EQ_100
int a[60][250][100],i,j,k;
for(i = 0;i < 60;i++)
for(j=0;j < 250;j++)
for(k = 0;k < 100;k++){
a[i][j][k] = i + j + k;
printf("a[%d][%d][%d] = %d\t",i,j,k,a[i][j][k]);
}
#endif
}
虽然是有点作用了,不过真正的debug可能 耦合度低一点,代码 重复度也应该小,本例中大部分代码重复(也可以只把循环中某一换用#ifdef和#endif括起来)。
既然知道改进方向,何不尝试一下,因为for顺序明显没影响,所以就统一了,降低耦合度么。不过数组定义的k等于1000和100那个没法拆,只能分开算了,我要体现出debug宏相对于注释的优势来。
首先,尝试拆分代码段,只对有区别的语句用宏分类,其次,不同宏之间可以用||来合并。不过还是有问题:不能使用下边这种只有if没有else的形式:
#include<stdio.h>
#define ORIGINAL_H_
//#define K_EQ_1000_H_
//#define K_EQ_100_H_
#define FINAL_H_
main(){
#ifdef K_EQ_1000_H_||ORIGINAL_H_
inta[60][250][1000],i,j,k;
for(k = 0;k< 1000;k++)
#endif
#ifdef K_EQ_100_H_ || FINAL_H_
inta[60][250][100],i,j,k;
for(k = 0;k< 100;k++)
#endif
for(j=0;j < 250;j++)
for(i = 0; i < 60;i++){
a[i][j][k] = i+ j + k;
printf("a[%d][%d][%d] = %d\t",i,j,k,a[i][j][k]);
}
}
因为i,j,k等都需要声明定义,它们在#ifdef里边,不能保证一定声明。即使你真开了那个选项,也不被允许(编译器的事儿吧)。
那么折中,为了方便,我选择了相对简化解——“ if 配 else ”,并且把条件砍少了。具体应该怎样做,应该衡量自己的情况定。
include<stdio.h>
//排除绝对错误的用法和合并不大的改动,最后焦点就在k的大小,完全体是100,测试体是1000(假设目标是找core dumped的原因)
//#define K_EQ_1000_H_
//#define K_EQ_100_H_
main(){
#ifdef K_EQ_1000_H_
inta[60][250][1000],i,j,k;
for(k = 0;k< 1000;k++)
#else
inta[60][250][100],i,j,k;
for(k = 0;k< 100;k++)
#endif
for(j=0;j < 250;j++)
for(i = 0; i < 60;i++){
a[i][j][k] = i+ j + k;
printf("a[%d][%d][%d] = %d\t",i,j,k,a[i][j][k]);
}
}
这种方案也不是很完美,如注释所说,我简化了很多问题。不过,如果想把所有的语句和差别都弄上,也可以,不过视觉上会很乱~没有建设性,不做了,如果真想做,因为按句找差异进行拆分的思路已经有了(例如数组的声明和for循环的k部分),把其他所有想要的差异都一句一句拆分,然后给各个宏就完了。一句一句的拆分,比注释有点优越性了吧,如果这种一两句的分化特别多,成百上千行,那一句一句改是要死人的。
现在,把宏都注释掉,想要哪个版本的运行结果,只要取消注释就行了~既能运行错误版本去测试,又能运行“终极形态”完成任务,本例中宏的功能,更形象的说,是一个选择开关,不过,debug宏不就是个开关么?
前边的方法还不是最终形态,最终形态其实需要拆分,重构,至少要有main.c debug.c和debug.h。
之前blog有个测试堆栈分配的程序,就是因为没写debug宏,修改频繁,并且读起来越来越困难,现在不是有办法了么——重构,把print语句都拆出去,然后加个debug宏,打开debug宏即可,类似于这种:
#define DEBUG_H_
.......
#ifdef DEBUG_H_
printDetail();
#endif
虽然是比较粗糙的一种使用方式,不是“原装大厂”debug“的用法和教程,但是也算一种思路, 更符合我们循序渐进的学习规律——你遇到困难或者效率低,然后解决这个障碍或者提高效率。其实最重要的还是你要在平时找机会引入各种应该掌握的功能,锻炼技巧,保持好习惯。
关 于学知识的循序渐进和相互联系,既然我碰到了实例,就再说一个吧,我是分类放c文件啊,那个测试程序我放在stack路径下了,那么按归类我现在要把他放 在MacroDebug路径下,总不能复制一份吧?太老土了,关键有些时候不现实,比如慢,占空间。那么应该怎么做呢,这就用到了另一个知识点,就是 linux下的文件链接link(很简单,三分钟学会),至于用硬链接还是软链接,我觉得没必要用软的,用硬的有一个好处——备份~!防删~!万一你那个路径不需要他,在这个路径还是需要的,所以备份最妥当。
root@v:/usr/local/C-test# ln ./stack/stack.c ./macroDebug/stackExample.c
root@v:/usr/local/C-test# ls macroDebug/
eg1 firstDebugMacro.c stackExample.c
不过,这个也看怎么改了,在同一个文件内加宏做修改,硬链接的两个”文件名“可以共用原文件。但是,如果重构——把原文件分成多个.c文件和.h头文件,那么缺失了这些相关文件,原来那个就废掉了(解决方法——可以把这几个文件(再单独创建一个子路径,把子路径)都链接过去~~~)
时间关系,未做过于细致的思考和验证,以及相关资料的查阅,有错误请指正,有兴趣的也可以找我讨论。
谢谢。