我们在调试程序时,输出调试信息(又称为”打桩”或者”插桩”)是一种普遍、有效的方法。
我们输出的信息通常包括行号、函数名、程序变量等。
但是我们在程序BUG修复后,又会特别烦我们之间插入的哪些调试语句,客户是不会理解我们那些调试语句曾经又多少汗马功劳,而太多的调试语句也影响我们程序运行时输出的美观和清晰,于是很多情况下我们需要手动将那些调试语句注释掉或者删掉,这对于小项目来说,我们还可以忍受,但是对于大项目,如果我们还是手动删除,我们只能。。。。呵呵,这不是程序猿该干的事。。。
下面我们给出几种调试方式方便大家使用。
/* debug.c */
#include <stdio.h>
#include <stdlib.h>
//#define DEBUG
/* 计算n的阶乘n! */
long Fac(int n);
/* 主函数 * 输入一个n计算n的阶乘 */
int main(void)
{
int n;
long fac;
while(scanf("%d", &n) != EOF)
{
printf("%d! = %ld\n", n, Fac(n));
}
return EXIT_SUCCESS;
}
/* 计算n的阶乘n! */
long Fac(int n)
{
int i;
long fac;
for(i = 1; i <= n; i++)
{
fac *= i;
printf("调试信息 %d! = %ld\n", i, fac);/* 调试信息 */
}
return fac;
}
这个程序是有BUG的,在程序第40行,变量fac未初始化为1。
插入的调试信息
printf("%d! = %ld\n", i, fac);/* 调试信息 */
在不需要时我们只能将此调试信息注释掉,这个是最原始,最人工的一种方式。
优势:
方便简单,易于操作,简单易读
缺点:
非常灵活,单一的调试信息会造成错误输出过于冗余
通过预处理指令将调试信息封闭起来,如下
#ifdef DEBUG
printf("%d! = %ld\n", i, fac);
#endif
这样调试的信息只存在与插桩信息宏DEBUG的预处理指令下,如果需要打开调试信息就定义插桩信息宏DEBUG,否则就将插桩信息宏DEBUG注释掉(也可以undef或者删掉)。
这样我们的代码就变成
/* debug.c */
#include <stdio.h>
#include <stdlib.h>
/* 插桩信息宏 */
#define DEBUG /* 如果需要调试信息请使用该宏,如果想取消调试信息,请注释掉或者*/
//#undef DEBUG /* 取消插桩信息宏DEBUG */
/* 计算n的阶乘n! */
long Fac(int n);
/* 主函数 * 输入一个n计算n的阶乘 */
int main(void)
{
int n;
long fac;
while(scanf("%d", &n) != EOF)
{
printf("%d! = %ld\n", n, Fac(n));
}
return EXIT_SUCCESS;
}
/* 计算n的阶乘n! */
long Fac(int n)
{
int i;
long fac;
for(i = 1; i <= n; i++)
{
fac *= i;
#ifdef DEBUG
printf("调试信息 %d! = %ld\n", i, fac);
#endif
}
return fac;
}
其实我们也可以不在代码中添加插桩信息宏DEBUG,gcc为我们提供了一个更简单的方法,那就是gcc -D编译选项
-DDEBUG 以字符串“1”定义 DEBUG 宏。 -DDEBUG=DEFN 以字符串“DEFN”定义 DEBUG 宏。
因此我们可以直接
gcc -DDEBUG debug.c -o debug
优势:
方便简单,易于操作,简单易读
缺点:
①不灵活,单一的调试宏,对于小项目来说可以,但是对于大项目同样会造成错误输出过于冗余,在大项目中,为了增加灵活性,往往通过定义多个等级的DEBUG(如DEBUG1,DEBUG2,DEBUG3等)或者不同名称的DEBUG(如DEBUG_DATA,DEBUG_COMM,DEBUG_APP等),来为不同的模块,或者错误等级进行调试,但是也会引入其他一些更复杂的问题,如项目难以管理,难以整合等问题。
②每个调试信息都会被成对的预处理指令包含,造成项目代码的过度膨胀,延长预处理时间;同时也不利于代码的阅读。
(编译阶段)能避免使用宏可能带来的副作用,而且方便日后定制debug信息的输出,特别方便维护和修改。我可以随时修改它,比如打印到网络服务器,本地文件,其他终端等,很方便的重定向。这是我最喜欢使用的方法。
#ifdef DEBUG
static int DebugPrintf(const char *format, ...)
{
va_list argPtr;
int count;
va_start(argPtr, format); /* 获取可变参数列表 */
fflush(stdout); /* 强制刷新输出缓冲区 */
count = vfprintf(stderr, format, argPtr); /* 将信息输出到标准出错流设备 */
va_end(argPtr); /* 可变参数列表结束 */
}
#else
static inline int DebugPrintf(const char *format, ...)
{
}
#endif
或者
static int DebugPrintf(const char *format, ...)
{
#ifdef DEBUG
va_list argPtr;
int count;
va_start(argPtr, format); /* 获取可变参数列表 */
fflush(stdout); /* 强制刷新输出缓冲区 */
count = vfprintf(stderr, format, argPtr); /* 将信息输出到标准出错流设备 */
va_end(argPtr); /* 可变参数列表结束 */
#else
/* 未定义插桩调试宏DEBUG,NOP空函数体 */
/* do { }while(0); */
#endif
}
这里我们依旧使用了插桩调试宏DEBUG,但是在宏定义和未定义的时候,分别定义了不同的DebugPrintf调试信息函数。这种方法的本质其实就是重写了一个我们自己的printf函数,在Glibc或者其他C运行库中,printf就是用vfprintf或者vprintf来实现的。
在定义了插桩调试宏DEBUG时,DebugPrintf被定义为一个向标准出错流输出信息的输出函数。但是在未定义插桩调试宏DEBUG时,DebugPrintf被定义为一个内联的空函数(当然也可以不使用内联,但是空函数为增加额外开销,C语言本身是不支持内联函数的,在C标准C99中C语言支持了内联函数)。
其中的空函数体不是很清晰,如果别人看我们代码的时候,可能会很疑惑为什么,我们可以加上注释或者采用如下代码代替
do
{
}while(0);
这样我们同样通过插桩调试宏DEBUG的定义与否来实现调试信息的开启和关闭。
这样我们的程序就变为
//debugprintf.c
#include <stdio.h>
#include <stdlib.h>
//#define DEBUG
//#undef DEBUG
#ifdef DEBUG
#include <stdarg.h>
static int DebugPrintf(const char *format, ...)
{
va_list argPtr;
int count;
va_start(argPtr, format); /* 获取可变参数列表 */
fflush(stdout); /* 强制刷新输出缓冲区 */
count = vfprintf(stderr, format, argPtr); /* 将信息输出到标准出错流设备 */
va_end(argPtr); /* 可变参数列表结束 */
}
#else
static inline int DebugPrintf(const char *format, ...)
{
}
#endif
/* 计算n的阶乘n! */
long Fac(int n);
/* 主函数 * 输入一个n计算n的阶乘 */
int main(void)
{
int n;
long fac;
while(scanf("%d", &n) != EOF)
{
printf("%d! = %ld\n", n, Fac(n));
}
return EXIT_SUCCESS;
}
/* 计算n的阶乘n! */
long Fac(int n)
{
int i;
long fac = 1;
for(i = 1; i <= n; i++)
{
fac *= i;
DebugPrintf("调试信息 %d! = %ld\n", i, fac);
}
return fac;
}
这种方式跟上一种方式有点区别,但是本质上是一样的,上面我们看到,我们通过插桩调试宏来控制调试函数的不同实现,未定义插桩信息宏时,调试函数被定义会空函数,但是这种方式有个缺点,就是会造成目标代码的膨胀。
下面这种方式,我们首先实现一个调试函数,然后通过宏定义来指向
#include <stdarg.h>
static int MyDebugPrintf(const char *format, ...)
{
va_list argPtr;
int count;
va_start(argPtr, format); /* 获取可变参数列表 */
fflush(stdout); /* 强制刷新输出缓冲区 */
count = vfprintf(stderr, format, argPtr); /* 将信息输出到标准出错流设备 */
va_end(argPtr); /* 可变参数列表结束 */
}
#ifdef DEBUG /* 如果定义了插桩信息宏,就将调试信息指向调试函数 */
#define DebugPrintf MyDebugPrintf
#else /* 如果未定义插桩信息宏,那么就将调试信息指向空NOP */
#define DebugPrintf
#endif
这样我们的程序变为
#include <stdio.h>
#include <stdlib.h>
//#define DEBUG
//#undef DEBUG
#include <stdarg.h>
static int MyDebugPrintf(const char *format, ...)
{
va_list argPtr;
int count;
va_start(argPtr, format); /* 获取可变参数列表 */
fflush(stdout); /* 强制刷新输出缓冲区 */
count = vfprintf(stderr, format, argPtr); /* 将信息输出到标准出错流设备 */
va_end(argPtr); /* 可变参数列表结束 */
}
#ifdef DEBUG /* 如果定义了插桩信息宏,就将调试信息指向调试函数 */
#define DebugPrintf MyDebugPrintf
#else /* 如果未定义插桩信息宏,那么就将调试信息指向空NOP */
#define DebugPrintf
#endif
/* 计算n的阶乘n! */
long Fac(int n);
/* 主函数 * 输入一个n计算n的阶乘 */
int main(void)
{
int n;
long fac;
while(scanf("%d", &n) != EOF)
{
printf("%d! = %ld\n", n, Fac(n));
}
return EXIT_SUCCESS;
}
/* 计算n的阶乘n! */
long Fac(int n)
{
int i;
long fac = 1;
for(i = 1; i <= n; i++)
{
fac *= i;
DebugPrintf("调试信息 %d! = %ld\n", i, fac);
}
return fac;
}
前面的两种方法,我们都是用vfprintf或者vprintf自己重新实现了一个输出函数,但是我们要想了我们是否可以使用printf函数呢,当然可以了
#ifdef DEBUG
#define DebugPrintf(format, arg...) \
printf(format, ## arg)
#else
#define DebugPrintf(format, arg...) do { } while (0)
#endif
代码如下
#include <stdio.h>
#include <stdlib.h>
//#define DEBUG
//#undef DEBUG
#ifdef DEBUG
#define DebugPrintf(format, arg...) \
printf(format, ## arg)
#else
#define DebugPrintf(format, arg...) do { } while (0)
#endif
/* 计算n的阶乘n! */
long Fac(int n);
/* 主函数 * 输入一个n计算n的阶乘 */
int main(void)
{
int n;
long fac;
while(scanf("%d", &n) != EOF)
{
printf("%d! = %ld\n", n, Fac(n));
}
return EXIT_SUCCESS;
}
/* 计算n的阶乘n! */
long Fac(int n)
{
int i;
long fac = 1;
for(i = 1; i <= n; i++)
{
fac *= i;
DebugPrintf("调试信息 %d! = %ld\n", i, fac);
}
return fac;
}
这种方式其实就是将原来定义的调试信息宏DEBUG更换未全局变量isDebug
static int isDebug = 0;
#define DebugPrintf(format, arg...) \
do
{ \
if (isDebug) \
printf(format , ## arg); \
} while (0)
前面的方法,如果进行调试或者取消调试,都需要重新编译,这样我们就可以使用调试等级来确定。
我们可以根据调试信息的细节程度,将调试信息分成不同的等级。调试信息的等级必须大于0,若调试信息细节程度越高,则等级越高。在输出调试信息时,若调试等级高于调试信息等级才输出调试信息,否则忽略该调试信息,如程序5。当调试等级为0时,则不输出任何调试信息。
下面我们以通过预处理指令定义调试函数的不同实现为例子,说明以下带调试等级的插桩调试信息
//debugprintf.c
#include <stdio.h>
#include <stdlib.h>
static int debugLevel = 0;
#include <stdarg.h>
static int DebugPrintf(const char *format, ...)
{
if (debugLevel >= 1)
{
va_list argPtr;
int count;
va_start(argPtr, format); /* 获取可变参数列表 */
fflush(stdout); /* 强制刷新输出缓冲区 */
count = vfprintf(stderr, format, argPtr); /* 将信息输出到标准出错流设备 */
va_end(argPtr); /* 可变参数列表结束 */
}
}
/* 计算n的阶乘n! */
long Fac(int n);
/* 主函数 * 输入一个n计算n的阶乘 */
int main(int argc, char *argv[])
{
if(argc < 2)
{
debugLevel = 0;
}
else
{
debugLevel = atoi(argv[1]);
}
int n;
long fac;
while(scanf("%d", &n) != EOF)
{
printf("%d! = %ld\n", n, Fac(n));
}
return EXIT_SUCCESS;
}
/* 计算n的阶乘n! */
long Fac(int n)
{
int i;
long fac = 1;
for(i = 1; i <= n; i++)
{
fac *= i;
DebugPrintf("调试信息 %d! = %ld\n", i, fac);
}
return fac;
}