C 語言編程 — 使用 assert 斷言進行程序設計

      網友投稿 1120 2025-03-31

      目錄


      文章目錄

      目錄

      斷言

      利用斷言來提高代碼的可測試性

      自定義斷言來滿足需求

      盡量在函數中使用斷言來檢查參數的合法性

      避免在斷言表達式中使用改變環(huán)境的語句

      避免使用斷言去檢查程序錯誤

      在防錯性程序設計中使用斷言來進行錯誤報警

      用斷言保證沒有定義的特性或功能不被使用

      謹慎使用斷言對程序開發(fā)環(huán)境中的假設進行檢查

      assert 使用風格

      參考文檔

      斷言

      在 C 語言中,斷言被定義為宏的形式,而不是函數,其原型定義在 assert.h 文件中。其中,assert 將通過檢查表達式 expression 的值來決定是否需要終止執(zhí)行程序。例如:assert(expression)。也就是說,如果表達式 expression 的值為假(即為 0),那么它將首先向標準錯誤流 stderr 打印一條出錯信息,然后再通過調用 abort 函數終止程序運行;否則,assert 無任何作用。

      使用 assert 的缺點是:頻繁的調用會極大的影響程序的性能,增加額外的開銷。所以,默認情況下,assert 宏只有在 DEBUG 版本(內部調試版本)中才能夠起作用,而在 Release 版本(發(fā)行版本)中將被忽略。當然,也可以通過定義宏或設置編譯器參數等形式來在任何時候啟用或者禁用斷言檢查。同樣,在程序投入運行后,最終用戶在遇到問題時也可以重新起用斷言。這樣可以快速發(fā)現(xiàn)并定位軟件問題,同時對系統(tǒng)錯誤進行自動報警。對于在系統(tǒng)中隱藏很深,用其他手段極難發(fā)現(xiàn)的問題也可以通過斷言進行定位到錯誤點,從而縮短軟件問題定位時間,提高系統(tǒng)的可測性。

      總的來說,使用斷言需要注意兩點:

      使用斷言記錄內部假設:斷言是對某種內部模塊的假設條件進行檢查,如果假設不成立,說明存在編程、設計錯誤。斷言可以對在系統(tǒng)中隱藏很深,用其它手段極難發(fā)現(xiàn)的問題進行定位,從而縮短軟件問題定位時間,提高系統(tǒng)的可測性。

      不能用斷言來檢查運行時錯誤:斷言是用來處理內部編程或設計是否符合假設;不能處理對于可能會發(fā)生的且必須處理的情況要寫防錯程序,而不是斷言。如某模塊收到其它模塊或鏈路上的消息后,要對消息的合理性進行檢查,此過程為正常的錯誤檢查,不能用斷言來實現(xiàn)。

      利用斷言來提高代碼的可測試性

      void *Memcpy(void *dest, const void *src, size_t len) { char *tmp_dest = (char *)dest; char *tmp_src = (char *)src; while(len --) *tmp_dest ++ = *tmp_src ++; return dest; }

      1

      2

      3

      4

      5

      6

      7

      8

      上面的 Memcpy 函數是可以通過編譯程序的,但假設 dest 與 src 實參錯誤地傳入了 NULL 指針就會出現(xiàn)錯誤。面對這類編譯器檢查不出來的問題,最簡單的方式是使用 if 語句進行判斷檢查:

      void *Memcpy(void *dest, const void *src, size_t len) { if(dest == NULL) { fprintf(stderr,"dest is NULL\n"); abort(); } if(src == NULL) { fprintf(stderr,"src is NULL\n"); abort(); } char *tmp_dest = (char *)dest; char *tmp_src = (char *)src; while(len --) *tmp_dest ++ = *tmp_src ++; return dest; }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      但這種方式的缺點是:隨著函數參數或需要檢查的表達式不斷增多,這種檢查測試代碼將占據整個函數的大部分,會降低程序的執(zhí)行效率。對于這個問題,我們可以想到使用 C 語言的預處理器來解決,只有當 DEBUG 開啟的時候執(zhí)行檢查:

      void *MemCopy(void *dest, const void *src, size_t len) { #ifdef DEBUG if(dest == NULL) { fprintf(stderr,"dest is NULL\n"); abort(); } if(src == NULL) { fprintf(stderr,"src is NULL\n"); abort(); } #endif char *tmp_dest = (char *)dest; char *tmp_src = (char *)src; while(len --) *tmp_dest ++ = *tmp_src ++; return dest; }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

      20

      但顯然的,這會讓程序變得臃腫,可讀性差。

      對于上述問題,C 語言提供了斷言機制來滿足需求。利用 assert 宏,將會使代碼變得更加安全且簡潔,如下面的示例代碼所示:

      void *MemCopy(void *dest, const void *src, size_t len) { assert(dest != NULL && src !=NULL); char *tmp_dest = (char *)dest; char *tmp_src = (char *)src; while(len --) *tmp_dest ++ = *tmp_src ++; return dest; }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      自定義斷言來滿足需求

      實際上,我們經常會出于某種目的,例如:把 assert 宏定義成當發(fā)生錯誤時不是中止調用程序的執(zhí)行,而是在發(fā)生錯誤的位置轉入調試程序,又或者是允許用戶選擇讓程序繼續(xù)運行等。需要對 assert 宏進行重新定義。

      但值得注意的是,不管斷言宏最終是用什么樣的方式進行定義,其所定義宏的主要目的都是要使用它來對傳遞給相應函數的參數進行確認檢查。如果違背了這條宏定義原則,那么所定義的斷言宏將會偏離方向,失去斷言宏定義本身的意義。

      其次,為不影響標準 assert 宏的使用,最好使用其他的名字。例如,下面的示例代碼就展示了用戶如何重定義自己的宏 ASSERT。

      需要注意的是,因為在編寫 C 語言代碼時,需要在每個語句后面以分號 ; 結束,所以程序員很可能會在習慣性的在進行宏定義時候使用 ; 號。但實際上這樣是錯誤的,因為用戶在調用 ASSERT 宏時,已經給出了一個分號。面對這種問題,我們可以使用 do{}while(0) 結構進行宏的定義,如下述例子:

      /* 使用斷言 */ #ifdef DEBUG /* 處理函數原型 */ void Assert(char * filename, unsigned int lineno); /* 自定義斷言 */ #define ASSERT(condition)\ do{ \ if(condition)\ NULL; \ else\ Assert(__FILE__ , __LINE__);\ }while(0) /* 不使用斷言 */ #else #define ASSERT(condition) NULL #endif void Test(unsigned char *str) { ASSERT(str != NULL); /* 函數處理代碼,省略... */ } int main(void) { Test(NULL); return 0; }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

      20

      21

      22

      23

      24

      25

      26

      27

      28

      29

      30

      31

      32

      很顯然,因為語句 Test(NULL) 為參數 str 錯誤傳入一個 NULL 指針,所以 ASSERT 宏會自動檢測到這個錯誤,同時根據宏 __FILE__ 和 __LINE__ 所提供的文件名和行號參數在標準錯誤輸出設備 stderr 上打印一條錯誤消息,然后調用 abort 函數中止程序的執(zhí)行。運行結果如下圖所示。

      從上面的示例中不難發(fā)現(xiàn),對標準的 assert 宏來說,自定義的 ASSERT 宏將具有更大的靈活性,可以根據自己的需要打印輸出不同的信息,同時也可以對不同類型的錯誤或者警告信息使用不同的斷言,這也是在工程代碼中經常使用的做法。當然,如果沒有什么特殊需求,還是建議使用標準的 assert 宏。

      盡量在函數中使用斷言來檢查參數的合法性

      在函數中使用斷言來檢查參數的合法性是斷言最主要的應用場景之一,它主要體現(xiàn)在如下 3 個方面:

      在代碼執(zhí)行之前或者在函數的入口處,使用斷言來檢查參數的合法性,這稱為前置條件斷言。

      在代碼執(zhí)行之后或者在函數的出口處,使用斷言來檢查參數是否被正確地執(zhí)行,這稱為后置條件斷言。

      在代碼執(zhí)行前后或者在函數的入出口處,使用斷言來檢查參數是否發(fā)生了變化,這稱為前后不變斷言。

      void *Memcpy(void *dest, const void *src, size_t len) { assert(dest!=NULL && src!=NULL); char *tmp_dest = (char *)dest; char *tmp_src = (char *)src; /* 檢查內存塊是否重疊 */ assert(tmp_dest>=tmp_src+len||tmp_src>=tmp_dest+len); while(len --) *tmp_dest ++ = *tmp_src ++; return dest; }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      在上面的 Memcpy 函數中,除了在函數的入口處檢查 dest 與 src 參數是否傳入 NULL 指針之外,還在函數出口處檢查了兩個內存塊是否發(fā)生重疊。

      除此之外,建議每一個 assert 宏只檢驗一個條件,這樣做的好處就是當斷言失敗時,便于程序排錯。最后,建議 assert 宏后面的語句應該空一行,以形成邏輯和視覺上的一致感,讓代碼有一種視覺上的美感。同時為復雜的斷言添加必要的注釋,可澄清斷言含義并減少不必要的誤用。

      避免在斷言表達式中使用改變環(huán)境的語句

      默認情況下,因為 assert 宏只有在 DEBUG 版本中才能起作用,而在 Release 版本中將被忽略。因此,在程序設計中應該避免在斷言表達式中使用改變環(huán)境的語句。

      避免使用斷言去檢查程序錯誤

      在對斷言的使用中,一定要遵循這樣一條規(guī)定:對來自系統(tǒng)內部的可靠的數據使用斷言,對于外部不可靠數據不能夠使用斷言,而應該使用錯誤處理代碼。換句話說,斷言是用來處理不應該發(fā)生的非法情況,而對于可能會發(fā)生且必須處理的情況應該使用錯誤處理代碼,而不是斷言。

      在通常情況下,系統(tǒng)外部的數據(如:不合法的用戶輸入)都是不可靠的,需要做嚴格的檢查(如:某模塊在收到其他模塊或鏈路上的消息后,要對消息的合理性進行檢查,此過程為正常的錯誤檢查,不能用斷言來實現(xiàn))才能放行到系統(tǒng)內部,這相當于一個守衛(wèi)。而對于系統(tǒng)內部的交互(如:子程序調用),如果每次都去處理輸入的數據,也就相當于系統(tǒng)沒有可信的邊界,這樣會讓代碼變得臃腫復雜。事實上,在系統(tǒng)內部,傳遞給子程序預期的恰當數據應該是調用者的責任,系統(tǒng)內的調用者應該確保傳遞給子程序的數據是恰當且可以正常工作的。這樣一來,就隔離了不可靠的外部環(huán)境和可靠的系統(tǒng)內部環(huán)境,降低復雜度。

      但是在代碼編寫與測試階段,代碼很可能包含一些意想不到的缺陷,也許是處理外部數據的程序考慮得不夠周全,也許是調用系統(tǒng)內部子程序的代碼存在錯誤,造成子程序調用失敗。這個時候,斷言就可以發(fā)揮作用,用來確診到底是哪部分出現(xiàn)了問題而導致子程序調用失敗。在清理所有缺陷之后,就建立了內外有別的信用體系。等到發(fā)行版的時候,這些斷言就沒有存在的必要了。因此,不能用斷言來檢查最終產品肯定會出現(xiàn)且必須處理的錯誤情況。

      char * Strdup(const char * source) { assert(source != NULL); char * result=NULL; size_t len = strlen(source) +1; result = (char *)malloc(len); assert(result != NULL); strcpy(result, source); return result; }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      對于 strdup 函數:

      第一個斷言語句用來檢查該程序正常工作時絕對不應該發(fā)生的非法情況。在調用代碼正確的情況下傳遞給 source 參數的值必然不為 NULL,如果斷言失敗,說明調用代碼中有錯誤,必須修改。因此,它屬于斷言的正常使用情況。

      第二個斷言語句,它測試的是錯誤情況,是在其最終產品中肯定會出現(xiàn)且必須對其進行處理的錯誤情況。即對 malloc 函數而言,當內存不足導致內存分配失敗時就會返回 NULL,因此這里不應該使用 assert 宏進行處理,而應該使用錯誤處理代碼。

      改正如下:

      char * Strdup(const char * source) { assert(source != NULL); char * result=NULL; size_t len = strlen(source)+1; result = (char *)malloc(len); if (result != NULL) { strcpy(result, source); } return result; }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      斷言是用來檢查非法情況的,而不是測試和處理錯誤的。因此,不要混淆非法情況與錯誤情況之間的區(qū)別,后者是必然存在且一定要處理的。

      在防錯性程序設計中使用斷言來進行錯誤報警

      在程序設計過程中,總會或多或少產生一些錯誤,這些錯誤有些屬于設計階段隱藏下來的,有些則是在編碼中產生的。為了避免和糾正這些錯誤,可在編碼過程中有意識地在程序中加進一些錯誤檢查的措施,這就是防錯性程序設計的基本思想。其中,它又可以分為 主動式防錯程序設計 和 被動式防錯程序設計 兩種。

      主動式防錯程序設計:是指周期性地對整個程序或數據庫進行搜查或在空閑時搜查異常情況。它既可以在處理輸入信息期間使用,也可以在系統(tǒng)空閑時間或等待下一個輸入時使用。如下面所列出的檢查均適合主動式防錯程序設計:

      內存檢查:如果在內存的某些塊中存放了一些具有某種類型和范圍的數據,則可對它們做經常性檢查。

      標志檢查:如果系統(tǒng)的狀態(tài)是由某些標志指示的,可對這些標志做單獨檢查。

      反向檢查:對于一些從一種代碼翻譯成另一種代碼或從一種系統(tǒng)翻譯成另一種系統(tǒng)的數據或變量值,可以采用反向檢查,即利用反向翻譯來檢查原始值的翻譯是否正確。

      狀態(tài)檢查:對于某些具有多個操作狀態(tài)的復雜系統(tǒng),若用某些特定的存儲值來表示這些狀態(tài),則可通過單獨檢查存儲值來驗證系統(tǒng)的操作狀態(tài)。

      連接檢查:當使用鏈表結構時,可檢查鏈表的連接情況。

      時間檢查:如果已知道完成某項計算所需的最長時間,則可用定時器來監(jiān)視這個時間。

      其他檢查:程序設計人員可經常仔細地對所使用的數據結構、操作序列和定時以及程序的功能加以考慮,從中得到要進行哪些檢查的啟發(fā)。

      被動式防錯程序設計:則是指必須等到某個輸入之后才能進行檢查,也就是達到檢查點時才能對程序的某些部分進行檢查。一般所要進行的檢查項目如下:

      來自外部設備的輸入數據,包括范圍、屬性是否正確。

      由其他程序所提供的數據是否正確。

      數據庫中的數據,包括數組、文件、結構、記錄是否正確。

      操作員的輸入,包括輸入的性質、順序是否正確。

      棧的深度是否正確。

      數組界限是否正確。

      表達式中是否出現(xiàn)零分母情況。

      正在運行的程序版本是否是所期望的(包括最后系統(tǒng)重新組合的日期)。

      通過其他程序或外部設備的輸出數據是否正確。

      雖然防錯性程序設計被譽為有較好的編碼風格,一直被業(yè)界強烈推薦。但防錯性程序設計也是一把雙刃劍,從調試錯誤的角度來看,它把原來簡單的、顯而易見的缺陷轉變成晦澀的、難以檢測的缺陷,而且診斷起來非常困難。從某種意義上講,防錯性程序設計隱瞞了程序的潛在錯誤。

      當然,對于軟件產品,希望它越健壯越好。但是調試脆弱的程序更容易幫助我們發(fā)現(xiàn)其問題,因為當缺陷出現(xiàn)的時候它就會立即表現(xiàn)出來。因此,在進行防錯性程序設計時,如果 “不可能發(fā)生” 的事情的確發(fā)生了,則需要使用斷言進行報警,這樣,才便于程序員在內部調試階段及時對程序問題進行處理,從而保證發(fā)布的軟件產品具有良好的健壯性。

      for(i=0; i

      1

      2

      3

      4

      上述例子中是一個常見的 for 循環(huán),就是一種非常常見的放錯性程序設計,保證 i 始終是小于 count 的。非放錯性程序設計為:

      for(i=0; i!=count; i++) { /* 處理代碼 */ }

      1

      2

      3

      4

      這是,當 i > count 時,程序也是不會停止的。

      但是,即便采用了放錯性程序設計,也依然存在一種特殊的邊界情況是:如果 for 循環(huán)中的索引 i 值確實大于 count,那么極有可能意味著代碼中存在著潛在的缺陷問題。斷言為我們提供了一個非常簡單的解決方法:

      for(i=0; i

      1

      2

      3

      4

      5

      通過斷言真正實現(xiàn)了一舉兩得的目的:健壯的產品軟件和脆弱的開發(fā)調試程序,即在該程序的交付版本中,相應的程序防錯代碼可以保證當程序的缺陷問題出現(xiàn)的時候,用戶可以不受損失;而在該程序的內部調試版本中,潛在的錯誤仍然可以通過斷言預警報告。

      因此,無論你在哪里編寫防錯性代碼,都應該盡量確保使用斷言來保護這段代碼。

      用斷言保證沒有定義的特性或功能不被使用

      在日常軟件設計中,如果原先規(guī)定的一部分功能尚未實現(xiàn),則應該使用斷言來保證這些沒有被定義的特性或功能不被使用。例如,某通信模塊在設計時,準備提供 “無連接” 和 “連接” 這兩種業(yè)務。但當前的版本中僅實現(xiàn)了 “無連接” 業(yè)務,且在此版本的正式發(fā)行版中,用戶(上層模塊)不應產生 “連接” 業(yè)務的請求,那么在測試時可用斷言來檢查用戶是否使用了 “連接 ”業(yè)務。

      /* 無連接業(yè)務 */ #define CONNECTIONLESS 0 /* 連接業(yè)務 */ #define CONNECTION 1 int MessageProcess(MESSAGE *msg) { assert(msg != NULL); unsigned char service; service = GetMessageService(msg); /* 使用斷言來檢查用戶是否使用了 “連接” 業(yè)務 */ assert(service != CONNECTION); /* 處理代碼 */ }

      1

      C 語言編程 — 使用 assert 斷言進行程序設計

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      謹慎使用斷言對程序開發(fā)環(huán)境中的假設進行檢查

      在程序設計中,不能夠使用斷言來檢查程序運行時所需的軟硬件環(huán)境及配置要求,它們需要由專門的處理代碼進行檢查處理。而斷言僅可對程序開發(fā)環(huán)境(OS/Compiler/Hardware)中的假設及所配置的某版本軟硬件是否具有某種功能的假設進行檢查。例如,某網卡是否在系統(tǒng)運行環(huán)境中配置了,應由程序中正式代碼來檢查;而此網卡是否具有某設想的功能,則可以由斷言來檢查。

      除此之外,對編譯器提供的功能及特性的假設也可以使用斷言進行檢查:

      /* int 類型占用的內存空間是否為 2 */ assert(sizeof(int)== 2); /* long 類型占用的內存空間是否為 4 */ assert(sizeof(long)==4); /* byte 的寬度是否為 8 */ assert(CHAR_BIT==8);

      1

      2

      3

      4

      5

      6

      7

      8

      之所以可以這樣使用斷言,那是因為軟件最終發(fā)行的 Release 版本與編譯器已沒有任何直接關系。

      最后,必須保證軟件的 DEBUG 與 Release 兩個版本在實現(xiàn)功能上的一致性,同時可以使用調測開關來切換這兩個不同的版本,以便統(tǒng)一維護,切記不要同時存在 DEBUG 版本與 Release 版本兩個不同的源文件。

      當然,因為頻繁調用 assert 會極大影響程序的性能,增加額外的開銷。因此,應該在正式軟件產品(即 Release 版本)中將斷言及其他調測代碼關掉(尤其是針對自定義的斷言宏)。

      assert 使用風格

      每個 assert 只檢驗一個條件。如果同時檢驗多個條件時,基表斷言失敗了,我們也無法直觀的判斷哪個條件失敗。

      錯誤:

      assert(nOffset>=0 && nOffset+nSize<=m_nInfomationSize);

      1

      正確:

      assert(nOffset >= 0); assert(nOffset+nSize <= m_nInfomationSize);

      1

      2

      不能使用改變環(huán)境的表達式:斷言僅用于判斷條件是否正確,不應參與任何運算。

      錯誤:

      assert(i++ < 100)

      1

      正確:

      assert(i < 100) i++;

      1

      2

      assert 和后面的語句應該空一行,以形成邏輯和視覺上的一致性。

      斷言用于檢查 “不應該” 發(fā)生的情況,不能代替條件過濾。

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

      int resetBufferSize(int nNewSize) { // 功能: 改變緩沖區(qū)大小 // 參數: nNewSize 緩沖區(qū)新長度 // 返回值: 緩沖區(qū)當前長度 // 說明: 保持原信息內容不變 nNewSize<=0 表示清除緩沖區(qū) assert(nNewSize >= 0); assert(nNewSize <= MAX_BUFFER_SIZE); ... }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      參考文檔

      http://c.biancheng.net/c/assert/

      C 語言

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:您的ERP實施路線圖
      下一篇:wps廣告如何刪除 wps廣告去除圖文教程
      相關文章
      337p日本欧洲亚洲大胆裸体艺术| 亚洲国产婷婷综合在线精品| 亚洲精品国偷自产在线| 亚洲成av人片天堂网老年人| 国产成人精品日本亚洲语音| 亚洲国产精品成人AV在线| 亚洲一本一道一区二区三区| 亚洲国产成人精品激情| 亚洲一卡2卡3卡4卡国产网站| 亚洲最大视频网站| 亚洲另类春色国产精品| 亚洲成在人线中文字幕| 亚洲第一页在线观看| 亚洲精品在线不卡| 91亚洲国产成人久久精品网址| 亚洲女人初试黑人巨高清| 亚洲欧洲春色校园另类小说| 亚洲精品美女久久久久| 亚洲福利电影一区二区?| 亚洲福利视频网址| 亚洲天堂男人影院| 亚洲人成欧美中文字幕| 无码亚洲成a人在线观看| 另类图片亚洲校园小说区| 亚洲高清无码在线观看| 久久夜色精品国产亚洲av| 亚洲精品无码午夜福利中文字幕 | 亚洲熟妇丰满xxxxx| 亚洲色少妇熟女11p| 亚洲va中文字幕| 亚洲男女内射在线播放| 亚洲精品无码不卡在线播放HE| 久久久久久亚洲精品| 亚洲精品亚洲人成在线观看麻豆| 亚洲国产成人超福利久久精品 | 亚洲精品午夜无码电影网| 亚洲人成电影亚洲人成9999网| 亚洲欧洲自拍拍偷午夜色| 亚洲国产日韩视频观看| 精品亚洲成A人在线观看青青| 亚洲精品成a人在线观看|