1. 程式人生 > >C語言輸出DEBUG除錯資訊的方法

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;
}

不定義除錯函式而直接使用printf

使用全域性變數(不推薦)

這種方式其實就是將原來定義的除錯資訊巨集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;
}

帶除錯資訊的插樁資訊