每次寫摘要我都覺得是一件很頭疼的事兒,因為我知道摘要真的很重要,它幾乎直接就決定了讀者的數量。可能花了九六二虎之力寫出來的東西,因為摘要的失敗而前功盡棄,因為絕大多數的讀者看文章之前都會瀏覽下摘要,如果他們發現摘要“不對口”,沒有什么特色和吸引人的地方,那么輕則采用一目十行的方法看完全文,重則對文章判“死刑”,一篇文章的好壞雖然不能用摘要來衡量,但是它卻常常被讀者用來衡量一篇文章的好壞,從而成為了文章讀者數量多少的一個關鍵因素。下面言歸正傳來說說斷言,如果出于一般性的學習C語言,應付考試的話,我想很少有人會在代碼中使用斷言,可能有的人在此之前從來沒有使用過斷言。那么斷言的使用到底能給我們的代碼帶來什么呢?我盡可能的把我所理解的斷言的使用講解清楚,希望我在此所講的斷言能夠對你有所幫助,讓你以后能夠在代碼中靈活使用斷言。 在講解之前,我們先來對斷言做一個基本的介紹,讓大家對斷言有一個大致的了解。在使用C語言編寫工程代碼時,我們總會對某種假設條件進行檢查,斷言就是用于在代碼中捕捉這些假設,可以將斷言看作是異常處理的一種高級形式。斷言表示為一些布爾表達式,程序員相信在程序中的某個特定點該表達式值為真。可以在任何時候啟用和禁用斷言驗證,因此可以在測試時啟用斷言,而在部署時禁用斷言。同樣,程序投入運行后,最終用戶在遇到問題時可以重新起用斷言。它可以快速發現并定位軟件問題,同時對系統錯誤進行自動報警。斷言可以對在系統中隱藏很深,用其它手段極難發現的問題可以用斷言來進行定位,從而縮短軟件問題定位時間,提高系統的可測性。實際應用時,可根據具體情況靈活地設計斷言。 通過上面的講解我們對于斷言算是有了一個大概的了解,那么接下來我們就來看看C語言中assert宏在代碼中的使用。 原型定義: void assert( int expression ); assert宏的原型定義在中,其作用是先計算表達式 expression ,如果expression的值為假(即為0),那么它先向stderr打印一條出錯信息,然后通過調用abort 來終止程序運行。 下面來看看一段代碼: #include #include 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 //#define NDEBUG #include int copy_string(char from[],char to[]) { int i=0; while(to[i++]=from); 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 //#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 #include //#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 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 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語言編寫代碼的過程中斷言較為詳細的使用,其中后面使用我們自己實現的斷言算得上是一個比較經典的斷言設計方法了,讀者可以在自己以后編寫C語言代碼的過程中參考下。由于本人水平有限,博客中的不妥或錯誤之處在所難免,殷切希望讀者批評指正。同時也歡迎讀者共同探討相關的內容,如果樂意交流的話請留下你寶貴的意見。 |