C語言小祕密之斷言

每次寫摘要我都覺得是一件很頭疼的事兒,因爲我知道摘要真的很重要,它幾乎直接就決定了讀者的數量。可能花了九六二虎之力寫出來的東西,因爲摘要的失敗而前功盡棄,因爲絕大多數的讀者看文章之前都會瀏覽下摘要,如果他們發現摘要“不對口”,沒有什麼特色和吸引人的地方,那麼輕則採用一目十行的方法看完全文,重則對文章判“死刑”,一篇文章的好壞雖然不能用摘要來衡量,但是它卻常常被讀者用來衡量一篇文章的好壞,從而成爲了文章讀者數量多少的一個關鍵因素。下面言歸正傳來說說斷言,如果出於一般性的學習C語言,應付考試的話,我想很少有人會在代碼中使用斷言,可能有的人在此之前從來沒有使用過斷言。那麼斷言的使用到底能給我們的代碼帶來什麼呢?我儘可能的把我所理解的斷言的使用講解清楚,希望我在此所講的斷言能夠對你有所幫助,讓你以後能夠在代碼中靈活使用斷言。

在講解之前,我們先來對斷言做一個基本的介紹,讓大家對斷言有一個大致的瞭解。在使用C語言編寫工程代碼時,我們總會對某種假設條件進行檢查,斷言就是用於在代碼中捕捉這些假設,可以將斷言看作是異常處理的一種高級形式。斷言表示爲一些布爾表達式,程序員相信在程序中的某個特定點該表達式值爲真。可以在任何時候啓用和禁用斷言驗證,因此可以在測試時啓用斷言,而在部署時禁用斷言。同樣,程序投入運行後,最終用戶在遇到問題時可以重新起用斷言。它可以快速發現並定位軟件問題,同時對系統錯誤進行自動報警。斷言可以對在系統中隱藏很深,用其它手段極難發現的問題可以用斷言來進行定位,從而縮短軟件問題定位時間,提高系統的可測性。實際應用時,可根據具體情況靈活地設計斷言。

通過上面的講解我們對於斷言算是有了一個大概的瞭解,那麼接下來我們就來看看C語言中assert宏在代碼中的使用。

原型定義:

void assert( int expression );

assert宏的原型定義在<assert.h>中,其作用是先計算表達式 expression ,如果expression的值爲假(即爲0),那麼它先向stderr打印一條出錯信息,然後通過調用abort 來終止程序運行。

下面來看看一段代碼:

#include <stdio.h>
#include <assert.h>

int main( void )
{
      int i;
   i=1;
   assert(i++);


   printf("%d\n",i);

       return 0;
}

 運行結果爲:

看看運行結果,因爲我們給定的i初始值爲1,所以使用assert(i++);語句的時候不會出現錯誤,進而執行了i++,所以其後的打印語句輸出值爲2。如果我們把i的初始值改爲0,那麼就回出現如下錯誤。
Assertion failed: i++, file E:\fdsa\assert2.cpp, line 8
Press any key to continue

是不是發現根據提示很快就能定位出錯點呢?!既然assert這麼便於定位出錯點,看來的確我們有必要熟練的在代碼中使用它,但是什麼東西的使用都是有規則的,assert的使用也不例外。

斷言語句不是永遠會執行,可以屏蔽也可以啓用,這就要求assert不管是在屏蔽還是啓用的情況下都不能對我們本身代碼的功能有所影響,這樣的話剛纔我們在代碼中使用了一句assert(i++);是不妥的,因爲我們一旦禁用了assert,i++的語句就得不到執行,對於接下來i值的使用就會出現問題了,所以對於這樣的語句我們應該是要分開來實現,寫出如下兩句來替代, assert(i); i++;,所以這就對於斷言的使用有了相應的要求,那麼我們一般在什麼情況下使用斷言呢?主要體現在一下幾個方面:

1.可以在預計正常情況下程序不會到達的地方放置斷言。(如assert (0);)

2.使用斷言測試方法執行的前置條件和後置條件 。

3.使用斷言檢查類的不變狀態,確保任何情況下,某個變量的狀態必須滿足。(如某個變量的變化範圍)

對於上面的前置條件和後置條件可能有的讀者還不是很瞭解,那麼看看下面的解釋你就明白了。

前置條件斷言:代碼執行之前必須具備的特性

後置條件斷言:代碼執行之後必須具備的特性

前後不變斷言:代碼執行前後不能變化的特性

當然在使用的斷言的過程中會有一些我們應該注意的事項和養成一些良好的習慣,如:

1.每個assert只檢驗一個條件,因爲同時檢驗多個條件時,如果斷言失敗,我們就無法直觀的判斷是哪個條件失敗

2.不能使用改變環境的語句,就像我們上面的代碼改變了i變量,在實際編寫代碼的過程中是不能這樣做的

3.assert和後面的語句應空一行,以形成邏輯和視覺上的一致感,也算是一種良好的編程習慣吧,讓編寫的代碼有一種視覺上的美感

4.有的地方,assert不能代替條件過濾

5.放在函數參數的入口處檢查傳入參數的合法性

6.斷言語句不可以有任何邊界效應

上面那麼多的文字,似乎很枯燥,但是沒辦法,我們不能急功近利,還是要先堅持看完文字描述部分,這樣在下面我們分析代碼的過程中就能很快知道爲什麼會出現那樣的問題了,也能在自己編寫代碼的時候熟練的使用assert,給自己的代碼調試帶來極大的便利,尤其是你在用C語言做工程項目的時候,如果你能夠在你的代碼中合理的使用assert,能使你創建更穩定、質量更好且不易於出錯的代碼。當需要在一個值爲FALSE時中斷當前操作的話,可以使用斷言。單元測試必須使用斷言,除了類型檢查和單元測試外,斷言還提供了一種確定各種特性是否在程序中得到維護的極好的方法。但凡優秀的程序員都能夠在自己代碼中很好的使用assert,編寫出高質量的代碼來。

說了assert這麼多的有點,當然也要說說它的缺點了。

使用assert的缺點是,頻繁的調用會極大的影響程序的性能,增加額外的開銷。所以在調試結束後,可以通過在包含#include 的語句之前插入 #define NDEBUG 來禁用assert調用。

接下面分析一下下面的一段代碼:

#include <stdio.h>
//#define NDEBUG
#include <assert.h>

int copy_string(char from[],char to[])
{
 int i=0;
 while(to[i++]=from[i]);

 printf("%s\n",to);

 return 1;
}

int main()
{
 char str[]="this is a string!";
 char dec_str[206];

 printf("%s\n",str); 

 assert(copy_string(str,dec_str));

 printf("%s\n",dec_str);

 return 0;
}

運行結果爲:

在以上代碼的開頭部分我們把#define NDEBUG給註釋掉了,所以我們啓用了assert,main函數中使用了assert(copy_string(str,dec_str));來實現copy_string函數的調用,在copy_string函數中我們使用了一句return 1,所以最終的函數調用結果就等價於是assert(1),所以接下來繼續執行assert下面的打印語句,最終成功的打印了三條輸出語句,如果我們把開頭的註釋部分打開,結果就只能成功的輸出起始部分一條打印語句。

以上我們都是在圍繞着assert宏在講解,僅僅是教會大家如何來使用assert宏,那麼接下來看看我們如何來實現自己的斷言呢?

接下來我們看看另外一段代碼:

#include <stdio.h>

//#undef  _EXAM_ASSERT_TEST_    //禁用
#define  _EXAM_ASSERT_TEST_   //啓用
#ifdef _EXAM_ASSERT_TEST_     //啓用斷言測試
 void assert_report( const char * file_name, const char * function_name, unsigned int line_no )
{
 printf( "\n[EXAM]Error Report file_name: %s, function_name: %s, line %u\n",
         file_name, function_name, line_no );

}
 #define  ASSERT_REPORT( condition )       \
 do{       \
 if ( condition )       \
  NULL;        \
 else         \
  assert_report( __FILE__, __func__, __LINE__ ); \
 }while(0)
 #else // 禁用斷言測試
#define ASSERT_REPORT( condition )  NULL
#endif /* end of ASSERT */
 int main( void )
{
    int i;
    i=0;
   // assert(i++);
   ASSERT_REPORT(i);
     printf("%d\n",i);
        return 0;
}

運行結果如下:

[EXAM]Error Report file_name: assert3.c, function_name: main, line 29
0
細心的讀者會發現我們並沒有使用斷言來結束當前程序的執行,所以在斷言下面的printf成功的打印出了i的當前值,當然我們也可以做適當的修改,在斷言出發現錯誤,那麼就調用 abort();來使當前正在執行的程序異常終止,修改如下:

#include <stdio.h>
#include <stdlib.h>

//#undef  _EXAM_ASSERT_TEST_    //禁用
#define  _EXAM_ASSERT_TEST_   //啓用
#ifdef _EXAM_ASSERT_TEST_     //啓用斷言測試
 void assert_report( const char * file_name, const char * function_name, unsigned int line_no )
{
 printf( "\n[EXAM]Error Report file_name: %s, function_name: %s, line %u\n",
         file_name, function_name, line_no );
  abort();
}

#define  ASSERT_REPORT( condition )       \
 do{       \
 if ( condition )       \
  NULL;        \
 else         \
  assert_report( __FILE__, __func__, __LINE__ ); \
 }while(0)

#else // 禁用斷言測試
#define ASSERT_REPORT( condition )  NULL
#endif /* end of ASSERT */
 int main( void )
{
    int i;
    i=0;
   // assert(i++);
   ASSERT_REPORT(i);
    printf("%d\n",i);
    return 0;

}

運行結果如下:

[EXAM]Error Report file_name: assert3.c, function_name: main, line 31
Aborted
此時就不會在執行接下來的打印語句了。看看我們自己的實現方式就知道,我們自己編寫的斷言可以比直接調用assert宏可以得到更多的信息量,主要是由於我們自己編寫的斷言更加的具有靈活性,可以根據自己的需要來打印輸出不同的信息,同時也可以對於不同類型的錯誤或者警告信息使用不同的斷言,這也是在工程代碼中經常使用的做法。如果你在關注代碼運行結果的同時也認真的閱讀了我的代碼,你會發現其中我在宏定義中使用了一個do{}while(0),使用它有什麼好處呢,或許在以上的代碼中並沒有體現出來,那麼我們看看下面的代碼你就知道了。

#include <stdio.h>

void print_1(void)
{
 printf("print_1\n");
}
void print_2(void)
{
 printf("print_2\n");
}
#define  printf_value()    \
   print_1();   \
   print_2();   \

int main( void )
{
 int i=0;
 if(i==1)
 printf_value();

 return 0;
}

 運行結果:

還是備份一下文章描述,以防圖片打開失敗給讀者帶來困擾。

print_2
Press any key to continue

看了上面運行結果可能有的讀者會很疑惑爲什麼會出現以上的錯誤呢?!if語句的條件不滿足,那麼print_value()函數應該不會被調用啊,怎麼會打印呢。如果我們把上面的printf_value()替換爲 print_1();  print_2();,就會很清楚的發現if語句在此的作用僅僅是不調用print_1();,而print_2();在控制之外,所以出現了上面的結果,有的讀者可能會馬上想到我們加上一個{}不就好了嗎,在這裏的確是加一個{}就可以了,因爲這裏是一個特殊情況,沒有else語句,如果我們在以上的宏定義中使用{},加入else語句後再來看看代碼。

#include <stdio.h>

void print_1(void)
{
 printf("print_1\n");
}

void print_2(void)
{
 printf("print_2\n");
}

#define  printf_value()    \
  {     \
  print_1();   \
  print_2();}


int main( void )
{
 int i=0;
 if(i==1)
  printf_value();
 else
  printf("add else word!!!");

 return 0;
}

看似正確的代碼,我們編譯就會出現如下錯誤:

error C2181: illegal else without matching if

爲什麼會出現這樣的錯誤呢?因爲我們編寫C語言代碼時,在每個語句後面加分號是一種約定俗成的習慣,以上代碼中我們在printf_value()語句後面加了一個分號,正是由於這個分號的作用使得else沒有與之相對應的if,所以編譯出錯。但是如果我們使用do{}while(0)就不會出現這些問題,所以我們在編寫代碼的時候應該學會在宏定義中使用do{}while(0)。

C語言斷言內容的講解到此就該結束了,上面內容已給出了在C語言編寫代碼的過程中斷言較爲詳細的使用,其中後面使用我們自己實現的斷言算得上是一個比較經典的斷言設計方法了.

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