http://c.biancheng.net/c/assert/程序員
對於斷言,相信你們都不陌生,大多數編程語言也都有斷言這一特性。簡單地講,斷言就是對某種假設條件進行檢查。在 C 語言中,斷言被定義爲宏的形式(assert(expression)),而不是函數,其原型定義在<assert.h>文件中。其中,assert 將經過檢查表達式 expression 的值來決定是否須要終止執行程序。也就是說,若是表達式 expression 的值爲假(即爲 0),那麼它將首先向標準錯誤流 stderr 打印一條出錯信息,而後再經過調用 abort 函數終止程序運行;不然,assert 無任何做用。
默認狀況下,assert 宏只有在 Debug 版本(內部調試版本)中才可以起做用,而在 Release 版本(發行版本)中將被忽略。固然,也能夠經過定義宏或設置編譯器參數等形式來在任什麼時候候啓用或者禁用斷言檢查(不建議這麼作)。一樣,在程序投入運行後,最終用戶在遇到問題時也能夠從新起用斷言。這樣能夠快速發現並定位軟件問題,同時對系統錯誤進行自動報警。對於在系統中隱藏很深,用其餘手段極難發現的問題也能夠經過斷言進行定位,從而縮短軟件問題定位時間,提升系統的可測性。算法
在討論如何使用斷言以前,先來看下面一段示例代碼:數據庫
對於上面的 Memcpy 函數,毋庸置疑,它可以經過編譯程序的檢查成功編譯。從表面上看,該函數並不存在其餘任何問題,而且代碼也很是乾淨。
但遺憾的是,在調用該函數時,若是不當心爲 dest 與 src 參數錯誤地傳入了 NULL 指針,那麼問題就嚴重了。輕者在交付以前這個潛在的錯誤致使程序癱瘓,從而暴露出來。不然,若是將該程序打包發佈出去,那麼所形成的後果是沒法估計的。
因而可知,不可以簡單地認爲「只要經過編譯程序成功編譯的就都是安全的程序」。固然,編譯程序也很難檢查出相似的潛在錯誤(如所傳遞的參數是否有效、潛在的算法錯誤等)。面對這類問題,通常首先想到的應該是使用最簡單的if語句進行判斷檢查,以下面的示例代碼所示:express
如今,經過「if(dest==NULL)與if(data-src==NULL)」判斷語句,只要在調用該函數的時候爲 dest 與 src 參數錯誤地傳入了NULL指針,這個函數就會檢查出來並作出相應的處理,即先向標準錯誤流 stderr 打印一條出錯信息,而後再調用 abort 函數終止程序運行。
從表面看來,上面的解決方案應該堪稱完美。可是,隨着函數參數或須要檢查的表達式不斷增多,這種檢查測試代碼將佔據整個函數的大部分(這一點從上面的 Memcpy 函數中就不難看出)。這樣代碼看起來很是不簡潔,甚至能夠說很「糟糕」,並且也下降了函數的執行效率。
面對上面的問題,或許能夠利用 C 的預處理程序有條件地包含或不包含相應的檢查部分進行解決,以下面的代碼所示:編程
這樣,經過條件編譯「#ifdef DEBUG」來同時維護同一程序的兩個版本(內部調試版本與發行版本),即在程序編寫過程當中,編譯其內部調試版本,利用其提供的測試檢查代碼爲程序自動查錯。而在程序編完以後,再編譯成發行版本。
上面的解決方案儘管經過條件編譯「#ifdef DEBUG」能產生很好的結果,也徹底符合咱們的程序設計要求,可是仔細觀察會發現,這樣的測試檢查代碼顯得並不那麼友好,當一個函數裏這種條件編譯語句不少時,代碼會顯得有些浮腫,甚至有些糟糕。
所以,對於上面的這種狀況,多數程序員都會選擇將全部的調試代碼隱藏在斷言 assert 宏中。其實,assert 宏也只不過是使用條件編譯「#ifdef」對部分代碼進行替換,利用 assert 宏,將會使代碼變得更加簡潔,以下面的示例代碼所示:數組
如今,經過「assert(dest !=NULL&&src !=NULL)」語句既完成程序的測試檢查功能(即只要在調用該函數的時候爲 dest 與 src 參數錯誤傳入 NULL 指針時都會引起 assert),與此同時,對 MemCopy 函數的代碼量也進行了大幅度瘦身,不得不說這是一個一箭雙鵰的好辦法。
實際上,在編程中咱們常常會出於某種目的(如把 assert 宏定義成當發生錯誤時不是停止調用程序的執行,而是在發生錯誤的位置轉入調試程序,又或者是容許用戶選擇讓程序繼續運行等)須要對 assert 宏進行從新定義。
但值得注意的是,無論斷言宏最終是用什麼樣的方式進行定義,其所定義宏的主要目的都是要使用它來對傳遞給相應函數的參數進行確認檢查。若是違背了這條宏定義原則,那麼所定義的宏將會偏離方向,失去宏定義自己的意義。與此同時,爲不影響標準 assert 宏的使用,最好使用其餘的名字。例如,下面的示例代碼就展現了用戶如何重定義本身的宏 ASSERT:安全
若是定義了 DEBUG,ASSERT 將被擴展爲一個if語句,不然執行「#define ASSERT(condition) NULL」替換成 NULL。
這裏須要注意的是,由於在編寫 C 語言代碼時,在每一個語句後面加一個分號「;」已經成爲一種約定俗成的習慣,所以頗有可能會在「Assert(__FILE__,__LINE__)」調用語句以後習慣性地加上一個分號。實際上並不須要這個分號,由於用戶在調用 ASSERT 宏時,已經給出了一個分號。面對這種問題,咱們可使用「do{}while(0)」結構進行處理,以下面的代碼所示:數據結構
很顯然,由於調用語句「Test(NULL)」爲參數 str 錯誤傳入一個 NULL 指針的緣由,因此 ASSERT 宏會自動檢測到這個錯誤,同時根據宏 __FILE__ 和 __LINE__ 所提供的文件名和行號參數在標準錯誤輸出設備 stderr 上打印一條錯誤消息,而後調用 abort 函數停止程序的執行。運行結果如圖 1 所示。編程語言
若是這時候將自定義 ASSERT 宏替換成標準 assert 宏結果會是怎樣的呢?以下面的示例代碼所示:函數
毋庸置疑,標準 assert 宏一樣會自動檢測到這個 NULL 指針錯誤。與此同時,標準 assert 宏除給出以上信息以外,還可以顯示出已經失敗的測試條件。運行結果如圖 2 所示。
從上面的示例中不難發現,對標準的 assert 宏來講,自定義的 ASSERT 宏將具備更大的靈活性,能夠根據本身的須要打印輸出不一樣的信息,同時也能夠對不一樣類型的錯誤或者警告信息使用不一樣的斷言,這也是在工程代碼中常用的作法。固然,若是沒有什麼特殊需求,仍是建議使用標準 assert 宏。
在函數中使用斷言來檢查參數的合法性是斷言最主要的應用場景之一,它主要體如今以下 3 個方面:
例如,在上面的 Memcpy 函數中,除了能夠經過「assert(dest !=NULL&&src!=NULL);」語句在函數的入口處檢查 dest 與 src 參數是否傳入 NULL 指針以外,還能夠經過「assert(tmp_dest>=tmp_src+len||tmp_src>=tmp_dest+len);」語句檢查兩個內存塊是否發生重疊。以下面的示例代碼所示:
除此以外,建議每個 assert 宏只檢驗一個條件,這樣作的好處就是當斷言失敗時,便於程序排錯。試想一下,若是在一個斷言中同時檢驗多個條件,當斷言失敗時,咱們將很難直觀地判斷哪一個條件失敗。所以,下面的斷言代碼應該更好一些,儘管這樣顯得有些畫蛇添足:
最後,建議 assert 宏後面的語句應該空一行,以造成邏輯和視覺上的一致感,讓代碼有一種視覺上的美感。同時爲複雜的斷言添加必要的註釋,可澄清斷言含義並減小沒必要要的誤用。
默認狀況下,由於 assert 宏只有在 Debug 版本中才能起做用,而在 Release 版本中將被忽略。所以,在程序設計中應該避免在斷言表達式中使用改變環境的語句。以下面的示例代碼所示:
對於上面的示例代碼,因爲「assert(i++)」語句的緣由,將致使不一樣的編譯版本產生不一樣的結果。若是是在 Debug 版本中,由於這裏向變量 i 所賦的初始值爲 1,因此在執行「assert(i++)」語句的時候將經過條件檢查,進而繼續執行「i++」,最後輸出的結果值爲 2;若是是在 Release 版本中,函數中的斷言語句「assert(i++)」將被忽略掉,這樣表達式「i++」將得不到執行,從而致使輸出的結果值仍是 1。
所以,應該避免在斷言表達式中使用相似「i++」這樣改變環境的語句,使用以下代碼進行替換:
如今,不管是 Debug 版本,仍是 Release 版本的輸出結果都將爲 2。
在對斷言的使用中,必定要遵循這樣一條規定:對來自系統內部的可靠的數據使用斷言,對於外部不可靠數據不可以使用斷言,而應該使用錯誤處理代碼。換句話說,斷言是用來處理不該該發生的非法狀況,而對於可能會發生且必須處理的狀況應該使用錯誤處理代碼,而不是斷言。
在一般狀況下,系統外部的數據(如不合法的用戶輸入)都是不可靠的,須要作嚴格的檢查(如某模塊在收到其餘模塊或鏈路上的消息後,要對消息的合理性進行檢查,此過程爲正常的錯誤檢查,不能用斷言來實現)才能放行到系統內部,這至關於一個守衛。而對於系統內部的交互(如子程序調用),若是每次都去處理輸入的數據,也就至關於系統沒有可信的邊界,這樣會讓代碼變得臃腫複雜。事實上,在系統內部,傳遞給子程序預期的恰當數據應該是調用者的責任,系統內的調用者應該確保傳遞給子程序的數據是恰當且能夠正常工做的。這樣一來,就隔離了不可靠的外部環境和可靠的系統內部環境,下降複雜度。
可是在代碼編寫與測試階段,代碼極可能包含一些意想不到的缺陷,也許是處理外部數據的程序考慮得不夠周全,也許是調用系統內部子程序的代碼存在錯誤,形成子程序調用失敗。這個時候,斷言就能夠發揮做用,用來確診究竟是哪部分出現了問題而致使子程序調用失敗。在清理全部缺陷以後,就創建了內外有別的信用體系。等到發行版的時候,這些斷言就沒有存在的必要了。所以,不能用斷言來檢查最終產品確定會出現且必須處理的錯誤狀況。
看下面一段示例代碼:
對於上面的 Strdup 函數,相信你們都不陌生。其中,第一個斷言語句「assert(source!=NULL)」用來檢查該程序正常工做時絕對不該該發生的非法狀況。換句話說,在調用代碼正確的狀況下傳遞給 source 參數的值必然不爲 NULL,若是斷言失敗,說明調用代碼中有錯誤,必須修改。所以,它屬於斷言的正常使用狀況。
而第二個斷言語句「assert(result!=NULL)」的用法則不一樣,它測試的是錯誤狀況,是在其最終產品中確定會出現且必須對其進行處理的錯誤狀況。即對 malloc 函數而言,當內存不足致使內存分配失敗時就會返回 NULL,所以這裏不該該使用 assert 宏進行處理,而應該使用錯誤處理代碼。以下面問題將使用 if 判斷語句進行處理:
總之記住一句話:斷言是用來檢查非法狀況的,而不是測試和處理錯誤的。所以,不要混淆非法狀況與錯誤狀況之間的區別,後者是必然存在且必定要處理的。
對於防錯性程序設計,相信有經驗的程序員並不陌生,大多數教科書也都鼓勵程序員進行防錯性程序設計。在程序設計過程當中,總會或多或少產生一些錯誤,這些錯誤有些屬於設計階段隱藏下來的,有些則是在編碼中產生的。爲了不和糾正這些錯誤,可在編碼過程當中有意識地在程序中加進一些錯誤檢查的措施,這就是防錯性程序設計的基本思想。其中,它又能夠分爲主動式防錯程序設計和被動式防錯程序設計兩種。
主動式防錯程序設計是指週期性地對整個程序或數據庫進行搜查或在空閒時搜查異常狀況。它既能夠在處理輸入信息期間使用,也能夠在系統空閒時間或等待下一個輸入時使用。以下面所列出的檢查均適合主動式防錯程序設計。
被動式防錯程序設計則是指必須等到某個輸入以後才能進行檢查,也就是達到檢查點時才能對程序的某些部分進行檢查。通常所要進行的檢查項目以下:
雖然防錯性程序設計被譽爲有較好的編碼風格,一直被業界強烈推薦。但防錯性程序設計也是一把雙刃劍,從調試錯誤的角度來看,它把原來簡單的、顯而易見的缺陷轉變成晦澀的、難以檢測的缺陷,並且診斷起來很是困難。從某種意義上講,防錯性程序設計隱瞞了程序的潛在錯誤。
固然,對於軟件產品,但願它越健壯越好。可是調試脆弱的程序更容易幫助咱們發現其問題,由於當缺陷出現的時候它就會當即表現出來。所以,在進行防錯性程序設計時,若是「不可能發生」的事情的確發生了,則須要使用斷言進行報警,這樣,才便於程序員在內部調試階段及時對程序問題進行處理,從而保證發佈的軟件產品具備良好的健壯性。
一個很常見的例子就是無處不在的 for 循環,以下面的示例代碼所示:
在幾乎全部的 for 循環示例中,其行爲都是迭代從 0 開始到「count-1」,所以,你們也都天然而然地編寫成了上面這種防錯性版本。但存在的問題是:若是 for 循環中的索引 i 值確實大於 count,那麼極有可能意味着代碼中存在着潛在的缺陷問題。
因爲上面的 for 循環示例採用了防錯性程序設計方式,所以,就算是在內部測試階段中出現了這種缺陷也很難發現其問題的所在,更加不可能出現系統報警提示。同時,由於這個潛在的程序缺陷,極有可能會在之後讓咱們吃盡苦頭,並且很是難以診斷。
那麼,不採用防錯性程序設計會是什麼樣子呢?以下面的示例代碼所示:
很顯然,這種寫法確定是不行的,當 for 循環中的索引 i 值確實大於 count 時,它仍是不會中止循環。
對於上面的問題,斷言爲咱們提供了一個很是簡單的解決方法,以下面的示例代碼所示:
不難發現,經過斷言真正實現了一箭雙鵰的目的:健壯的產品軟件和脆弱的開發調試程序,即在該程序的交付版本中,相應的程序防錯代碼能夠保證當程序的缺陷問題出現的時候,用戶能夠不受損失;而在該程序的內部調試版本中,潛在的錯誤仍然能夠經過斷言預警報告。
所以,「不管你在哪裏編寫防錯性代碼,都應該儘可能確保使用斷言來保護這段代碼」。固然,也沒必要過度拘泥於此。例如,若是每次執行 for 循環時索引 i 的值只是簡單地增 1,那麼要使索引i的值超過 count 從而引發問題幾乎是不可能的。在這種狀況下,相應的斷言也就沒有任何存在的意義,應該從程序中刪除。可是,若是索引 i 的值有其餘處理狀況,則必須使用斷言進行預警。因而可知,在防錯性程序設計中是否須要使用斷言進行錯誤報警要視具體狀況而定,在編碼以前都要問本身:「在進行防錯性程序設計時,程序中隱瞞錯誤了嗎?」若是答案是確定的,就必須在程序中加上相應的斷言,以此來對這些錯誤進行報警。不然,就不要畫蛇添足了。
在平常軟件設計中,若是原先規定的一部分功能還沒有實現,則應該使用斷言來保證這些沒有被定義的特性或功能不被使用。例如,某通訊模塊在設計時,準備提供「無鏈接」和「鏈接」這兩種業務。但當前的版本中僅實現了「無鏈接」業務,且在此版本的正式發行版中,用戶(上層模塊)不該產生「鏈接」業務的請求,那麼在測試時可用斷言來檢查用戶是否使用了「鏈接」業務。以下面的示例代碼所示:
在程序設計中,不可以使用斷言來檢查程序運行時所需的軟硬件環境及配置要求,它們須要由專門的處理代碼進行檢查處理。而斷言僅可對程序開發環境(OS/Compiler/Hardware)中的假設及所配置的某版本軟硬件是否具備某種功能的假設進行檢查。例如,某網卡是否在系統運行環境中配置了,應由程序中正式代碼來檢查;而此網卡是否具備某設想的功能,則能夠由斷言來檢查。
除此以外,對編譯器提供的功能及特性的假設也可使用斷言進行檢查,以下面的示例代碼所示:
之因此能夠這樣使用斷言,那是由於軟件最終發行的 Release 版本與編譯器已沒有任何直接關係。最後,必須保證軟件的 Debug 與 Release 兩個版本在實現功能上的一致性,同時可使用調測開關來切換這兩個不一樣的版本,以便統一維護,切記不要同時存在 Debug 版本與 Release 版本兩個不一樣的源文件。固然,由於頻繁調用 assert 會極大影響程序的性能,增長額外的開銷。所以,應該在正式軟件產品(即 Release 版本)中將斷言及其餘調測代碼關掉(尤爲是針對自定義的斷言宏)。