【C語言】利用assert高效排查你的C程序

        衆所周知,我們在實際開發C程序的時候,往往是編碼容易——調試困難,修改容易——排查困難。我們在開發過程中,debug佔據了我們很大一部分的時間,而正確地使用各種編碼手段,可以有效地提升排查問題代碼的效率。筆者從自己的實踐經驗出發,給大家分享一個用於編碼/調試階段高效發現問題代碼的利器,這就是大名鼎鼎的assert。通過閱讀本文,你將瞭解到以下內容:

  • 什麼是assert?
  • assert有什麼用?
  • assert怎麼使用?
  • assert的常規操作有哪些?

什麼是assert?


        assert它的中文含義是“斷言”,它被包含在<assert.h>中,往往給使用者呈現的形式爲: assert() 。因此,很多開發者認爲它就是一個函數,可能它的原型就是void assert(int expression); 但研究過assert.h的,一定會發現,其實並不是。

        assert的真身,其實是一個宏定義,只不過是一個帶參數輸入的宏定義,與我們之前一篇八卦Linux內核設計的max宏定義 (【Linux內核】從小小的宏定義窺探Linux內核的精妙設計)類似的。廬山真面目如下所示:

#define assert(e) ((e) ? (void)0 : _assert(#e, __FILE__, __LINE__))

        從它的定義,我們可以很清晰的知道,真正起到打印作用的是_assert,而它纔是真正的一個函數。原型爲:

void _assert(const char *e, const char *file, int line);

assert有什麼用?


        本文的主題是利用assert高效排查問題代碼,自然assert的用途就是排查代碼;但是,具體它的功能是怎麼體現呢?假設有如下代碼,一個測試函數的實現片段:

int test_function(int a, int *b)
{
    assert(a > 1);  /* 斷言:入參a的值一定大於1 */
    assert(b);      /* 斷言: 入參b指針一定不是NULL */

    /* Do other things here ... */
}

        如代碼所示,有一個測試函數test_function,接收2個入參,一個是int型的a變量,一個int *類型的b指針;在函數的開始,我們就用assert分別對a和b做了斷言,確保它們有正確的輸入。假設我們有如下的函數調用的測試代碼:

{
    int a = 7;
    int *b = &a;
    
    test_function(a, b);

    /* Do other things here ... */
}

        很明顯,當如上代碼調用test_fucntion時,內部的兩個assert判斷均爲【真】,那麼什麼事情也不會發生,assert就像一個優雅的淑女,靜靜地站在那裏看着你。

        當我們的測試代碼做如下調整:

{
    int a = 0;
    int *b = &a;
    
    test_function(a, b);

    /* Do other things here ... */
}

        很明顯,test_function的第一個assert語句不爲【真】,那麼它就像山洪一樣要爆發了,終止程序運行的同時,會輸出類似的錯誤提示: Assertion failed: a > 1,file xxx.c, line 128,這段錯誤提示,不僅告訴了我們哪個條件判斷出錯了,並且還告訴了我們出錯的位置在哪個文件的哪一行,這是多麼智能啊!由此可見,它真正的威力在於【代碼出錯】時,即當代碼沒有按照我們的斷言進行時,我們就應該停下來,排查下爲何會有錯誤的參數輸入,這樣我們就可以將bug在出現苗頭的時候就把它消滅掉。


assert怎麼使用?


        其實,上面的示例代碼已經展示瞭如何使用assert,但是我們需要補充的是,一般在使用assert斷言語句的時候,需要在對應的.c文件加上對assert.h的引用,否則編譯會報錯誤。

        assert這麼智能的利器是非常有利於我們寫出高質量不易出錯的代碼的,通常富有經驗的程序員都會很擅長使用assert語句,把assert打在恰當的語句中,可以最大限度地提升我們的代碼質量。但是,很多開發者開始有疑惑了,要是每條語句,每個判斷都加上assert,那麼就算全部assert的情況都是【真】,也夠CPU忙一會了,這樣似乎有些浪費CPU的計算能力,以追求高效的C語言編程,可容不下這樣的事情發生。那,這可怎麼辦呢?

        爲避免以上情況的發生,我們作爲assert的使用者,一般只需要在開發調試階段才使用assert,而在正式發佈的版本是需要去掉assert的。這樣疑惑就更大了,發佈版本一條條刪掉assert調用,萬一刪錯了代碼呢?設計者總是聰明的,他們也早就想到了這一點,這不他們也提供瞭解決方案。開頭的時候,我們介紹了assert是一個宏,但並沒有完全展示它的全貌。現在開始展示它的真容:

#ifdef NDEBUG
#define assert(e) (void)0
#else
#define assert(e) ((e) ? (void)0 : _assert(#e, __FILE__, __LINE__))
#endif

        聰明的你,一定也發現了,我們只需要在.c文件#include <assert.h>之前,加上一句#define NDEBUG 1就可以把相應.c中的assert(e)全部變成((void)0);而((void)0)本身是個無效調用代碼,在實際的編譯過程中是會被優化掉的,這樣我們僅增加對NDEBUG(NO DEBUG的意思)的宏定義,就可以把全部的assert給摒棄了,是不是很智能呢?


assert的常規操作有哪些?


        正如上面所述,assert這麼智能,但是我們也不能濫用,只需要在恰當的位置作爲特定的判斷;通常來說,我們有以下一些情景可以考慮使用assert語句:

  • 函數的入參判斷,對錯誤的入參及時處理
  • 對重點調用的系統函數的返回結果做判斷,使用assert保證系統調用的結果是正確的,避免外部使用不正確的系統調用而出現錯上加錯的情況;
  • switch語句中,如果不允許出現default的情況,可以考慮在default分支中加入assert(0);
  • 執行計算時,做計算的輸入或計算結果的輸出等做下判斷,比如除數不能爲0,比如一個百分比值不能超過100%等等。

        綜述,assert是把雙刃劍,出錯時它能很優秀地暴露問題代碼,非常有利於我們排查代碼,從而以最快的速度找到問題並解決問題;同時,它的頻繁調用,一定程度上加上了CPU的處理,做一些無畏的判斷,“簡直就是在浪費生命”。所以,在實際開發過程中,我們務必要嚴謹細緻地使用assert,讓它更好地爲我們服務。

        只有不自負且思維嚴謹的人才能使用好assert,我們只有做到了不自負,不對自己的代碼打100%的包票,相信是代碼總會有出錯的時候,纔會逐步養成思維嚴謹的習慣,反而對自己的代碼質量有更大的提升。

        本文對assert的介紹和使用做了一番總結,文中難免有紕漏之處,還望讀者誠心指正,感謝。

版權聲明:本文爲博主原創文章,轉載請註明出處! https://blog.csdn.net/szullc/article/details/84844575

原創作者:李路昌

電子郵箱:[email protected]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章