1. 程式人生 > >C語言assert函式完全攻略

C語言assert函式完全攻略

斷言assert函式,C語言assert函式完全攻略

對於斷言,相信大家都不陌生,大多數程式語言也都有斷言這一特性。簡單地講,斷言就是對某種假設條件進行檢查。在 C 語言中,斷言被定義為巨集的形式(assert(expression)),而不是函式,其原型定義在<assert.h>檔案中。其中,assert 將通過檢查表示式 expression 的值來決定是否需要終止執行程式。也就是說,如果表示式 expression 的值為假(即為 0),那麼它將首先向標準錯誤流 stderr 列印一條出錯資訊,然後再通過呼叫 abort 函式終止程式執行;否則,assert 無任何作用。

預設情況下,assert 巨集只有在 Debug 版本(內部除錯版本)中才能夠起作用,而在 Release 版本(發行版本)中將被忽略。當然,也可以通過定義巨集或設定編譯器引數等形式來在任何時候啟用或者禁用斷言檢查(不建議這麼做)。同樣,在程式投入執行後,終端使用者在遇到問題時也可以重新起用斷言。這樣可以快速發現並定位軟體問題,同時對系統錯誤進行自動報警。對於在系統中隱藏很深,用其他手段極難發現的問題也可以通過斷言進行定位,從而縮短軟體問題定位時間,提高系統的可測性。

儘量利用斷言來提高程式碼的可測試性

在討論如何使用斷言之前,先來看下面一段示例程式碼:

 
  1. void *Memcpy(void *dest, const void *src, size_t len)
  2. {
  3. char *tmp_dest = (char *)dest;
  4. char *tmp_src = (char *)src;
  5. while(len --)
  6. *tmp_dest ++ = *tmp_src ++;
  7. return dest;
  8. }

對於上面的 Memcpy 函式,毋庸置疑,它能夠通過編譯程式的檢查成功編譯。從表面上看,該函式並不存在其他任何問題,並且程式碼也非常乾淨。

但遺憾的是,在呼叫該函式時,如果不小心為 dest 與 src 引數錯誤地傳入了 NULL 指標,那麼問題就嚴重了。輕者在交付之前這個潛在的錯誤導致程式癱瘓,從而暴露出來。否則,如果將該程式打包釋出出去,那麼所造成的後果是無法估計的。

由此可見,不能夠簡單地認為“只要通過編譯程式成功編譯的就都是安全的程式”。當然,編譯程式也很難檢查出類似的潛在錯誤(如所傳遞的引數是否有效、潛在的演算法錯誤等)。面對這類問題,一般首先想到的應該是使用最簡單的if語句進行判斷檢查,如下面的示例程式碼所示:

 
  1. void *Memcpy(void *dest, const void *src, size_t len)
  2. {
  3. if(dest == NULL)
  4. {
  5. fprintf(stderr,"dest is NULL\n");
  6. abort();
  7. }
  8. if(src == NULL)
  9. {
  10. fprintf(stderr,"src is NULL\n");
  11. abort();
  12. }
  13. char *tmp_dest = (char *)dest;
  14. char *tmp_src = (char *)src;
  15. while(len --)
  16. *tmp_dest ++ = *tmp_src ++;
  17. return dest;
  18. }

現在,通過“if(dest==NULL)與if(data-src==NULL)”判斷語句,只要在呼叫該函式的時候為 dest 與 src 引數錯誤地傳入了NULL指標,這個函式就會檢查出來並做出相應的處理,即先向標準錯誤流 stderr 列印一條出錯資訊,然後再呼叫 abort 函式終止程式執行。

從表面看來,上面的解決方案應該堪稱完美。但是,隨著函式引數或需要檢查的表示式不斷增多,這種檢查測試程式碼將佔據整個函式的大部分(這一點從上面的 Memcpy 函式中就不難看出)。這樣程式碼看起來非常不簡潔,甚至可以說很“糟糕”,而且也降低了函式的執行效率。

面對上面的問題,或許可以利用 C 的預處理程式有條件地包含或不包含相應的檢查部分進行解決,如下面的程式碼所示:

 
  1. void *MemCopy(void *dest, const void *src, size_t len)
  2. {
  3. #ifdef DEBUG
  4. if(dest == NULL)
  5. {
  6. fprintf(stderr,"dest is NULL\n");
  7. abort();
  8. }
  9. if(src == NULL)
  10. {
  11. fprintf(stderr,"src is NULL\n");
  12. abort();
  13. }
  14. #endif
  15. char *tmp_dest = (char *)dest;
  16. char *tmp_src = (char *)src;
  17. while(len --)
  18. *tmp_dest ++ = *tmp_src ++;
  19. return dest;
  20. }

這樣,通過條件編譯“#ifdef DEBUG”來同時維護同一程式的兩個版本(內部除錯版本與發行版本),即在程式編寫過程中,編譯其內部除錯版本,利用其提供的測試檢查程式碼為程式自動查錯。而在程式編完之後,再編譯成發行版本。

上面的解決方案儘管通過條件編譯“#ifdef DEBUG”能產生很好的結果,也完全符合我們的程式設計要求,但是仔細觀察會發現,這樣的測試檢查程式碼顯得並不那麼友好,當一個函式裡這種條件編譯語句很多時,程式碼會顯得有些浮腫,甚至有些糟糕。

因此,對於上面的這種情況,多數程式設計師都會選擇將所有的除錯程式碼隱藏在斷言 assert 巨集中。其實,assert 巨集也只不過是使用條件編譯“#ifdef”對部分程式碼進行替換,利用 assert 巨集,將會使程式碼變得更加簡潔,如下面的示例程式碼所示:

 
  1. void *MemCopy(void *dest, const void *src, size_t len)
  2. {
  3. assert(dest != NULL && src !=NULL);
  4. char *tmp_dest = (char *)dest;
  5. char *tmp_src = (char *)src;
  6. while(len --)
  7. *tmp_dest ++ = *tmp_src ++;
  8. return dest;
  9. }

現在,通過“assert(dest !=NULL&&src !=NULL)”語句既完成程式的測試檢查功能(即只要在呼叫該函式的時候為 dest 與 src 引數錯誤傳入 NULL 指標時都會引發 assert),與此同時,對 MemCopy 函式的程式碼量也進行了大幅度瘦身,不得不說這是一個兩全其美的好辦法。

實際上,在程式設計中我們經常會出於某種目的(如把 assert 巨集定義成當發生錯誤時不是中止呼叫程式的執行,而是在發生錯誤的位置轉入除錯程式,又或者是允許使用者選擇讓程式繼續執行等)需要對 assert 巨集進行重新定義。

但值得注意的是,不管斷言巨集最終是用什麼樣的方式進行定義,其所定義巨集的主要目的都是要使用它來對傳遞給相應函式的引數進行確認檢查。如果違背了這條巨集定義原則,那麼所定義的巨集將會偏離方向,失去巨集定義本身的意義。與此同時,為不影響標準 assert 巨集的使用,最好使用其他的名字。例如,下面的示例程式碼就展示了使用者如何重定義自己的巨集 ASSERT:

 
  1. /*使用斷言測試*/
  2. #ifdef DEBUG
  3. /*處理函式原型*/
  4. void Assert(char * filename, unsigned int lineno);
  5. #define ASSERT(condition)\
  6. if(condition)\
  7. NULL; \
  8. else\
  9. Assert(__FILE__ , __LINE__)
  10. /*不使用斷言測試*/
  11. #else
  12. #define ASSERT(condition) NULL
  13. #endif
  14. void Assert(char * filename, unsigned int lineno)
  15. {
  16. fflush(stdout);
  17. fprintf(stderr,"\nAssert failed: %s, line %u\n",filename, lineno);
  18. fflush(stderr);
  19. abort();
  20. }

如果定義了 DEBUG,ASSERT 將被擴充套件為一個if語句,否則執行“#define ASSERT(condition) NULL”替換成 NULL。

這裡需要注意的是,因為在編寫 C 語言程式碼時,在每個語句後面加一個分號“;”已經成為一種約定俗成的習慣,因此很有可能會在“Assert(__FILE__,__LINE__)”呼叫語句之後習慣性地加上一個分號。實際上並不需要這個分號,因為使用者在呼叫 ASSERT 巨集時,已經給出了一個分號。面對這種問題,我們可以使用“do{}while(0)”結構進行處理,如下面的程式碼所示:

 
  1. #define ASSERT(condition)\
  2. do{ \
  3. if(condition)\
  4. NULL; \
  5. else\
  6. Assert(__FILE__ , __LINE__);\
  7. }while(0)
  8.  
  9. 現在,將不再為分號“;”而擔心了,呼叫示例如下:
  10. void Test(unsigned char *str)
  11. {
  12. ASSERT(str != NULL);
  13. /*函式處理程式碼*/
  14. }
  15. int main(void)
  16. {
  17. Test(NULL);
  18. return 0;
  19. }

很顯然,因為呼叫語句“Test(NULL)”為引數 str 錯誤傳入一個 NULL 指標的原因,所以 ASSERT 巨集會自動檢測到這個錯誤,同時根據巨集 __FILE__ 和 __LINE__ 所提供的檔名和行號引數在標準錯誤輸出裝置 stderr 上列印一條錯誤訊息,然後呼叫 abort 函式中止程式的執行。執行結果如圖 1 所示。



圖 1 呼叫自定義 ASSERT 巨集的執行結果


如果這時候將自定義 ASSERT 巨集替換成標準 assert 巨集結果會是怎樣的呢?如下面的示例程式碼所示:

 
  1. void Test(unsigned char *str)
  2. {
  3. assert(str != NULL);
  4. /*函式處理程式碼*/
  5. }

毋庸置疑,標準 assert 巨集同樣會自動檢測到這個 NULL 指標錯誤。與此同時,標準 assert 巨集除給出以上資訊之外,還能夠顯示出已經失敗的測試條件。執行結果如圖 2 所示。



圖 2 呼叫標準 assert 巨集的執行結果


從上面的示例中不難發現,對標準的 assert 巨集來說,自定義的 ASSERT 巨集將具有更大的靈活性,可以根據自己的需要列印輸出不同的資訊,同時也可以對不同型別的錯誤或者警告資訊使用不同的斷言,這也是在工程程式碼中經常使用的做法。當然,如果沒有什麼特殊需求,還是建議使用標準 assert 巨集。

儘量在函式中使用斷言來檢查引數的合法性

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

  1. 在程式碼執行之前或者在函式的入口處,使用斷言來檢查引數的合法性,這稱為前置條件斷言。
  2. 在程式碼執行之後或者在函式的出口處,使用斷言來檢查引數是否被正確地執行,這稱為後置條件斷言。
  3. 在程式碼執行前後或者在函式的入出口處,使用斷言來檢查引數是否發生了變化,這稱為前後不變斷言。


例如,在上面的 Memcpy 函式中,除了可以通過“assert(dest !=NULL&&src!=NULL);”語句在函式的入口處檢查 dest 與 src 引數是否傳入 NULL 指標之外,還可以通過“assert(tmp_dest>=tmp_src+len||tmp_src>=tmp_dest+len);”語句檢查兩個記憶體塊是否發生重疊。如下面的示例程式碼所示:

 
  1. void *Memcpy(void *dest, const void *src, size_t len)
  2. {
  3. assert(dest!=NULL && src!=NULL);
  4. char *tmp_dest = (char *)dest;
  5. char *tmp_src = (char *)src;
  6. /*檢查記憶體塊是否重疊*/
  7. assert(tmp_dest>=tmp_src+len||tmp_src>=tmp_dest+len);
  8. while(len --)
  9. *tmp_dest ++ = *tmp_src ++;
  10. return dest;
  11. }

除此之外,建議每一個 assert 巨集只檢驗一個條件,這樣做的好處就是當斷言失敗時,便於程式排錯。試想一下,如果在一個斷言中同時檢驗多個條件,當斷言失敗時,我們將很難直觀地判斷哪個條件失敗。因此,下面的斷言程式碼應該更好一些,儘管這樣顯得有些多此一舉:

 
  1. assert(dest!=NULL);
  2. assert(src!=NULL);

最後,建議 assert 巨集後面的語句應該空一行,以形成邏輯和視覺上的一致感,讓程式碼有一種視覺上的美感。同時為複雜的斷言新增必要的註釋,可澄清斷言含義並減少不必要的誤用。

避免在斷言表示式中使用改變環境的語句

預設情況下,因為 assert 巨集只有在 Debug 版本中才能起作用,而在 Release 版本中將被忽略。因此,在程式設計中應該避免在斷言表示式中使用改變環境的語句。如下面的示例程式碼所示:

 
  1. int Test(int i)
  2. {
  3. assert(i++);
  4. return i;
  5. }
  6. int main(void)
  7. {
  8. int i=1;
  9. printf("%d\n",Test(i));
  10. return 0;
  11. }

對於上面的示例程式碼,由於“assert(i++)”語句的原因,將導致不同的編譯版本產生不同的結果。如果是在 Debug 版本中,因為這裡向變數 i 所賦的初始值為 1,所以在執行“assert(i++)”語句的時候將通過條件檢查,進而繼續執行“i++”,最後輸出的結果值為 2;如果是在 Release 版本中,函式中的斷言語句“assert(i++)”將被忽略掉,這樣表示式“i++”將得不到執行,從而導致輸出的結果值還是 1。

因此,應該避免在斷言表示式中使用類似“i++”這樣改變環境的語句,使用如下程式碼進行替換:

 
  1. int Test(int i)
  2. {
  3. assert(i);
  4. i++;
  5. return i;
  6. }

現在,無論是 Debug 版本,還是 Release 版本的輸出結果都將為 2。

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

在對斷言的使用中,一定要遵循這樣一條規定:對來自系統內部的可靠的資料使用斷言,對於外部不可靠資料不能夠使用斷言,而應該使用錯誤處理程式碼。換句話說,斷言是用來處理不應該發生的非法情況,而對於可能會發生且必須處理的情況應該使用錯誤處理程式碼,而不是斷言。

在通常情況下,系統外部的資料(如不合法的使用者輸入)都是不可靠的,需要做嚴格的檢查(如某模組在收到其他模組或鏈路上的訊息後,要對訊息的合理性進行檢查,此過程為正常的錯誤檢查,不能用斷言來實現)才能放行到系統內部,這相當於一個守衛。而對於系統內部的互動(如子程式呼叫),如果每次都去處理輸入的資料,也就相當於系統沒有可信的邊界,這樣會讓程式碼變得臃腫複雜。事實上,在系統內部,傳遞給子程式預期的恰當資料應該是呼叫者的責任,系統內的呼叫者應該確保傳遞給子程式的資料是恰當且可以正常工作的。這樣一來,就隔離了不可靠的外部環境和可靠的系統內部環境,降低複雜度。

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

看下面一段示例程式碼:

 
  1. char * Strdup(const char * source)
  2. {
  3. assert(source != NULL);
  4. char * result=NULL;
  5. size_t len = strlen(source) +1;
  6. result = (char *)malloc(len);
  7. assert(result != NULL);
  8. strcpy(result, source);
  9. return result;
  10. }

對於上面的 Strdup 函式,相信大家都不陌生。其中,第一個斷言語句“assert(source!=NULL)”用來檢查該程式正常工作時絕對不應該發生的非法情況。換句話說,在呼叫程式碼正確的情況下傳遞給 source 引數的值必然不為 NULL,如果斷言失敗,說明呼叫程式碼中有錯誤,必須修改。因此,它屬於斷言的正常使用情況。

而第二個斷言語句“assert(result!=NULL)”的用法則不同,它測試的是錯誤情況,是在其最終產品中肯定會出現且必須對其進行處理的錯誤情況。即對 malloc 函式而言,當記憶體不足導致記憶體分配失敗時就會返回 NULL,因此這裡不應該使用 assert 巨集進行處理,而應該使用錯誤處理程式碼。如下面問題將使用 if 判斷語句進行處理:

 
  1. char * Strdup(const char * source)
  2. {
  3. assert(source != NULL);
  4. char * result=NULL;
  5. size_t len = strlen(source)+1;
  6. result = (char *)malloc(len);
  7. if (result != NULL)
  8. {
  9. strcpy(result, source);
  10. }
  11. return result;
  12. }

總之記住一句話:斷言是用來檢查非法情況的,而不是測試和處理錯誤的。因此,不要混淆非法情況與錯誤情況之間的區別,後者是必然存在且一定要處理的。

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

對於防錯性程式設計,相信有經驗的程式設計師並不陌生,大多數教科書也都鼓勵程式設計師進行防錯性程式設計。在程式設計過程中,總會或多或少產生一些錯誤,這些錯誤有些屬於設計階段隱藏下來的,有些則是在編碼中產生的。為了避免和糾正這些錯誤,可在編碼過程中有意識地在程式中加進一些錯誤檢查的措施,這就是防錯性程式設計的基本思想。其中,它又可以分為主動式防錯程式設計和被動式防錯程式設計兩種。

主動式防錯程式設計是指週期性地對整個程式或資料庫進行搜查或在空閒時搜查異常情況。它既可以在處理輸入資訊期間使用,也可以在系統空閒時間或等待下一個輸入時使用。如下面所列出的檢查均適合主動式防錯程式設計。

  • 記憶體檢查:如果在記憶體的某些塊中存放了一些具有某種型別和範圍的資料,則可對它們做經常性檢查。
  • 標誌檢查:如果系統的狀態是由某些標誌指示的,可對這些標誌做單獨檢查。
  • 反向檢查:對於一些從一種程式碼翻譯成另一種程式碼或從一種系統翻譯成另一種系統的資料或變數值,可以採用反向檢查,即利用反向翻譯來檢查原始值的翻譯是否正確。
  • 狀態檢查:對於某些具有多個操作狀態的複雜系統,若用某些特定的儲存值來表示這些狀態,則可通過單獨檢查儲存值來驗證系統的操作狀態。
  • 連線檢查:當使用連結串列結構時,可檢查連結串列的連線情況。
  • 時間檢查:如果已知道完成某項計算所需的最長時間,則可用定時器來監視這個時間。
  • 其他檢查:程式設計人員可經常仔細地對所使用的資料結構、操作序列和定時以及程式的功能加以考慮,從中得到要進行哪些檢查的啟發。


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

  • 來自外部裝置的輸入資料,包括範圍、屬性是否正確。
  • 由其他程式所提供的資料是否正確。
  • 資料庫中的資料,包括陣列、檔案、結構、記錄是否正確。
  • 操作員的輸入,包括輸入的性質、順序是否正確。
  • 棧的深度是否正確。
  • 陣列界限是否正確。
  • 表示式中是否出現零分母情況。
  • 正在執行的程式版本是否是所期望的(包括最後系統重新組合的日期)。
  • 通過其他程式或外部裝置的輸出資料是否正確。


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

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

一個很常見的例子就是無處不在的 for 迴圈,如下面的示例程式碼所示:

 
  1. for(i=0;i<count;i++)
  2. {
  3. /*處理程式碼*/
  4. }

在幾乎所有的 for 迴圈示例中,其行為都是迭代從 0 開始到“count-1”,因此,大家也都自然而然地編寫成了上面這種防錯性版本。但存在的問題是:如果 for 迴圈中的索引 i 值確實大於 count,那麼極有可能意味著程式碼中存在著潛在的缺陷問題。

由於上面的 for 迴圈示例採用了防錯性程式設計方式,因此,就算是在內部測試階段中出現了這種缺陷也很難發現其問題的所在,更加不可能出現系統報警提示。同時,因為這個潛在的程式缺陷,極有可能會在以後讓我們吃盡苦頭,而且非常難以診斷。

那麼,不採用防錯性程式設計會是什麼樣子呢?如下面的示例程式碼所示:

 
  1. for(i=0;i!=count;i++)
  2. {
  3. /*處理程式碼*/
  4. }

很顯然,這種寫法肯定是不行的,當 for 迴圈中的索引 i 值確實大於 count 時,它還是不會停止迴圈。

對於上面的問題,斷言為我們提供了一個非常簡單的解決方法,如下面的示例程式碼所示:

 
  1. for(i=0;i<count;i++)
  2. {
  3. /*處理程式碼*/
  4. }
  5. assert(i==count);

不難發現,通過斷言真正實現了一舉兩得的目的:健壯的產品軟體和脆弱的開發除錯程式,即在該程式的交付版本中,相應的程式防錯程式碼可以保證當程式的缺陷問題出現的時候,使用者可以不受損失;而在該程式的內部除錯版本中,潛在的錯誤仍然可以通過斷言預警報告。

因此,“無論你在哪裡編寫防錯性程式碼,都應該儘量確保使用斷言來保護這段程式碼”。當然,也不必過分拘泥於此。例如,如果每次執行 for 迴圈時索引 i 的值只是簡單地增 1,那麼要使索引i的值超過 count 從而引起問題幾乎是不可能的。在這種情況下,相應的斷言也就沒有任何存在的意義,應該從程式中刪除。但是,如果索引 i 的值有其他處理情況,則必須使用斷言進行預警。由此可見,在防錯性程式設計中是否需要使用斷言進行錯誤報警要視具體情況而定,在編碼之前都要問自己:“在進行防錯性程式設計時,程式中隱瞞錯誤了嗎?”如果答案是肯定的,就必須在程式中加上相應的斷言,以此來對這些錯誤進行報警。否則,就不要多此一舉了。

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

在日常軟體設計中,如果原先規定的一部分功能尚未實現,則應該使用斷言來保證這些沒有被定義的特性或功能不被使用。例如,某通訊模組在設計時,準備提供“無連線”和“連線”這兩種業務。但當前的版本中僅實現了“無連線”業務,且在此版本的正式發行版中,使用者(上層模組)不應產生“連線”業務的請求,那麼在測試時可用斷言來檢查使用者是否使用了“連線”業務。如下面的示例程式碼所示:

 
  1. /*無連線業務*/
  2. #define CONNECTIONLESS 0
  3. /*連線業務*/
  4. #define CONNECTION 1
  5. int MessageProcess(MESSAGE *msg)
  6. {
  7. assert(msg != NULL);
  8. unsigned char service;
  9. service = GetMessageService(msg);
  10. /*使用斷言來檢查使用者是否使用了“連線”業務*/
  11. assert(service != CONNECTION);
  12. /*處理程式碼*/
  13. }

謹慎使用斷言對程式開發環境中的假設進行檢查

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

除此之外,對編譯器提供的功能及特性的假設也可以使用斷言進行檢查,如下面的示例程式碼所示:

 
  1. /*int型別佔用的記憶體空間是否為2*/
  2. assert(sizeof(int)== 2);
  3. /*long型別佔用的記憶體空間是否為4*/
  4. assert(sizeof(long)==4);
  5. /*byte的寬度是否為8*/
  6. assert(CHAR_BIT==8);

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

最後,必須保證軟體的 Debug 與 Release 兩個版本在實現功能上的一致性,同時可以使用調測開關來切換這兩個不同的版本,以便統一維護,切記不要同時存在 Debug 版本與 Release 版本兩個不同的原始檔。

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