C語言輸出DEBUG除錯資訊的方法
問題提出
我們在除錯程式時,輸出除錯資訊(又稱為”打樁”或者”插樁”)是一種普遍、有效的方法。
我們輸出的資訊通常包括行號、函式名、程式變數等。
但是我們在程式BUG修復後,又會特別煩我們之間插入的哪些除錯語句,客戶是不會理解我們那些除錯語句曾經又多少汗馬功勞,而太多的除錯語句也影響我們程式執行時輸出的美觀和清晰,於是很多情況下我們需要手動將那些除錯語句註釋掉或者刪掉,這對於小專案來說,我們還可以忍受,但是對於大專案,如果我們還是手動刪除,我們只能。。。。呵呵,這不是程式猿該乾的事。。。
下面我們給出幾種除錯方式方便大家使用。
手工環境下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;
}
不定義除錯函式而直接使用printf
前面的兩種方法,我們都是用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;
}