什么是防御性編程?
防御性編程是一種細致、謹慎的編程方法。為了開發(fā)可靠的軟件,我們要設(shè)計系統(tǒng)中的每個組件,以使其盡可能地“保護”自己。
我們通過明確地在代碼中對設(shè)想進行檢查,擊碎了未記錄下來的設(shè)想。這是一種努力,防止(或至少是觀察)我們的代碼以將會展現(xiàn)錯誤行為的方式被調(diào)用。
防御性編程是一種編程習慣,是指預(yù)見在什么地方可能會出現(xiàn)問題,然后創(chuàng)建一個環(huán)境來測試錯誤,當預(yù)見的問題出現(xiàn)的時候通知你,并執(zhí)行一個你指定的損害控制動作,如停止程序執(zhí)行,將用戶重指向到一個備份的服務(wù)器,或者開啟一個你可以用來診斷問題的調(diào)試信息。
這些防御性編程環(huán)境通常的構(gòu)造方法有:添加聲明到代碼中,執(zhí)行按契約進行設(shè)計,開發(fā)軟件防御防火墻,或者簡單添加用來驗證用戶輸入的代碼。
防御性編程使我們可以盡早發(fā)現(xiàn)較小的問題,而不是等到它們發(fā)展成大的災(zāi)難的時候才發(fā)現(xiàn)。
你常??梢钥吹健奥殬I(yè)”的開發(fā)人員不假思索飛快地編寫著代碼。他們開發(fā)軟件的過程可能是這樣的:
他們不斷地受到那些從未有時間驗證的錯誤的打擊。這很難說是現(xiàn)代軟件工程的進步,但它卻不斷地發(fā)生著。防御性編程幫助我們從一開始就編寫正確的軟件,而不再需要經(jīng)歷“編寫-嘗試-編寫-嘗試……”的循環(huán)過程。在采用了防御性編程之后,開發(fā)軟件的過程將變成:
當然,防御性編程并不能排除所有的程序錯誤。但是問題所帶來的麻煩將會減少,并易于修改。防御性程序員只是抓住飄落的雪花,而不是被埋葬在錯誤的雪崩中。
防御性編程是一種防衛(wèi)方式,而不是一種補救形式。我們可以將其與在錯誤發(fā)生之后再來改正錯誤的調(diào)試比較一下。調(diào)試就是如何來找到補救的辦法。
對防御性編程的誤解
關(guān)于防御性編程,有一些常見的誤解。防御性編程并不是:
檢查錯誤
如果代碼中存在可能出現(xiàn)錯誤的情況,無論如何你都應(yīng)該檢查這些錯誤。這并不是防御性編碼。它只是一種好的做法,是編寫正確代碼的一部分。
測試
測試你的代碼并不是防御,而只是開發(fā)工作的另一個典型部分。測試工作不是防御性的,這項工作可以驗證代碼現(xiàn)在是正確的,但不能保證代碼在經(jīng)歷將來的修改之后不會出錯。即便是擁有了世界上最好的測試工具,也還是會有人對代碼進行更改,并使代碼進入過去未測試的狀態(tài)。
調(diào)試
在調(diào)試期間,你可以添加一些防御性代碼,不過調(diào)試是在程序出錯之后進行的。防御性編程首先是“防止”程序出錯的措施(或在錯誤以不可理解的方式出現(xiàn)之前發(fā)現(xiàn)它們,不然就需要整夜的調(diào)試)。
防御性編程真的值得我們來討論嗎?下面是一些支持和反對的意見:
反對意見
防御性編程消耗了程序員和計算機的資源。
-
它降低了代碼的效率;即使是很少的額外代碼也需要一些額外的執(zhí)行時間。對于一個函數(shù)或一個類,這也許還不要緊,但是如果一個系統(tǒng)由10萬個函數(shù)組成,問題就變得嚴重了。
-
每種防御性的做法都需要一些額外的工作。為什么要做這些工作呢?你需要做的已經(jīng)夠多的了,不是嗎?只要確保人們正確地使用你的代碼就可以了。如果他們使用的方式不正確,那么任何問題也都是他們自己造成的。
支持意見
反駁很有說服力。
-
防御性編程可以節(jié)省大量的調(diào)試時間,使你可以去做更有意義的事情。還記得墨菲嗎:凡是可能會被錯誤地使用的代碼,一定會被錯誤地使用。
-
編寫可以正確運行、只是速度有些慢的代碼,要遠遠好過大多數(shù)時間都正常運行、但是有時候會崩潰的代碼(顯示器閃爍高亮彩色火花)。
-
我們可以設(shè)計一些在版本構(gòu)建中物理移除的防御性代碼,以解決性能問題。總之,我們這里所考慮的大部分防御性措施,并不具有任何明顯的開銷。
-
防御性編程避免了大量的安全性問題,這在現(xiàn)代軟件開發(fā)中是一個重大的問題。避免這些問題可以帶來很多好處。
由于市場要求軟件的開發(fā)更加快速和廉價,我們就需要致力于實現(xiàn)這一目標的技術(shù)。不要跳過眼前的這些額外工作,它們可以防止將來的痛苦和項目延遲。
防御性編程有助于程序的安全性,可以防范諸如此類惡意的濫用。黑客和病毒制造者常常會利用那些不嚴謹?shù)拇a,以控制某個應(yīng)用程序,然后實施他們蓄意的破壞計劃。這對軟件開發(fā)的現(xiàn)代世界而言,無疑是個嚴重的威脅;這個問題涉及到諸如生產(chǎn)效率、金錢和個人隱私等方方面面。
軟件濫用者形形色色,從利用程序小缺陷的不守規(guī)則的用戶,到想盡辦法非法進入他人系統(tǒng)的職業(yè)黑客。有太多的程序員在不經(jīng)意間為這些人留下了可隨意通過的后門。隨著網(wǎng)絡(luò)化計算機的興起,粗心大意所帶來的后果變得愈來愈顯著了。
許多大型軟件開發(fā)公司終于意識到了這種威脅,開始認真思考這個問題,將時間和資源投入到嚴謹?shù)姆烙跃幋a工作中。事實上,在受到惡意進攻之后才亡羊補牢是很困難的。
在防御性編程的大框架之下,有許多常識性的規(guī)則。
人們在想到防御性編程的時候,通常都會想到“斷言”,這沒有錯。
我們將在后面對此進行討論。但是,還是有一些簡單的編程習慣可以極大地提高代碼的安全性。
盡管看上去像是常識,但是這些規(guī)則卻往往被人們忽視,這就是為什么世界上并不缺少低質(zhì)量軟件的原因。只要程序員們警惕起來,受到足夠的督促,更高的安全性和可靠的開發(fā)很容易就能夠?qū)崿F(xiàn)。
在下面的幾頁中,將列出防御性編程的一些規(guī)則。我們將先從粗略的概覽開始,整體地描述防御的技巧、過程和步驟。隨著討論的深入,我們會加入更多的細節(jié),進一步地逐條分析每條代碼語句。在這些防御性技巧中,有一些是與具體的編程語言相關(guān)的。這很自然——如果你的編程語言會讓你射傷到自己的腳,那么你一定要穿上防彈靴。
在閱讀這些規(guī)則時,請對你自己進行一個評估。
-
在這些規(guī)則中,現(xiàn)在你遵循的有幾條?
-
你打算采納那些規(guī)則?
1 使用好的編碼風格和合理的設(shè)計
我們可以通過采用良好的編程風格,來防范大多數(shù)編碼錯誤。這與本篇的其他章節(jié)自然地吻合。很多簡單的事,如選用有意義的變量名,或者審慎地使用括號,都可以使編碼變得更加清晰明了,并減少缺陷出現(xiàn)的可能性。
同樣地,在投入到編碼工作中之前,先考慮大體的設(shè)計方案,這也非常關(guān)鍵?!白詈玫挠嬎銠C程序的文本是結(jié)構(gòu)清晰的。”(見參考書目Kernighan Plaugher 78)從實現(xiàn)一套清晰的API、一個邏輯系統(tǒng)結(jié)構(gòu)以及一些定義良好的組件角色與責任開始入手,將使你避免以后處處頭疼的局面。
2 不要倉促地編寫代碼
閃電式的編程太常見了。使用這種編程方式的程序員會很快地開發(fā)出一個函數(shù),馬上把這個函數(shù)交給編譯器來檢查語法,接著運行一遍看看能不能用,然后就進入下一個任務(wù)。這種方式充滿了危險。
相反,在寫每一行時都三思而后行??赡軙霈F(xiàn)什么樣的錯誤?你是否已經(jīng)考慮了所有可能出現(xiàn)的邏輯分支?放慢速度,有條不紊的編程雖然看上去很平凡,但這的確是減少缺陷的好辦法。
關(guān)鍵概念 欲速則不達。每敲一個字,都要想清楚你要輸入的是什么。
在C語言中,有一個會使追求速度的程序員犯錯的陷阱,即將“==”錯誤地輸入為“=”。前者為相等關(guān)系測試,而后者則是變量賦值。如果你的編譯器功能不全(或者關(guān)閉了警告功能),你就不會得到相關(guān)提示,也就無從得知自己輸入了不該輸入的東西。
一定要在完成與一個代碼段相關(guān)的所有任務(wù)之后,再進入下一個環(huán)節(jié)。例如,如果你決定先編寫主體部分,再加入錯誤檢查和處理,那么一定要確保這兩項工作的完成都遵循章法。如果你要推遲錯誤檢查的編寫,而直接開始編寫超過三個代碼
段的主體部分,你一定要慎之又慎。你也許真的想隨后再回來編寫錯誤檢查,但卻一而再再而三地向后推遲,這期間你可能會忘記很多上下文,使得接下來的工作更加耗時和瑣碎。(當然,到時候你還要面臨一些人為設(shè)置的最后截止日期。)
遵循章法是一種習慣,需要牢記于心并切實貫徹。如果你不立即做正確的事,那么將來你很可能也不會再去做正確的事?,F(xiàn)在就行動,不要等到撒哈拉沙漠下雨了才行動。晚做不如早做,因為將來再做將需要遵循更多的章法。
3 不要相信任何人
媽媽曾告訴過你,不要和陌生人說話。不幸的是,要想開發(fā)一個好的軟件,就需要更加憤世嫉俗,對人的天性更加不信任。即便是沒有惡意的代碼用戶,也可能會給你的程序帶來麻煩。防御意味著不能相信任何人。
下面這些情況可能是給你帶來麻煩的原因:
-
真正的用戶 意外地提供了假的輸入,或者錯誤地操作了程序;
-
惡意的用戶 故意造成不好的程序行為;
-
客戶端代碼 使用錯誤的參數(shù)調(diào)用了你的函數(shù),或者提供了不一致的輸入;
-
運行環(huán)境 沒有為程序提供足夠的服務(wù);
-
外部程序庫 運行失誤,不遵從你所依賴的接口協(xié)議。
你甚至可能會在編寫一個函數(shù)時犯下愚蠢的錯誤,或者錯誤地使用三年前編寫的代碼,因為你忘記了這些代碼究竟是怎樣運行的。不要設(shè)想所有的一切都運行良好,或者所有的代碼都會正確地運行。在你的程序各處都添加安全檢查。時刻注意弱點,用更多的防御性代碼防止弱點的出現(xiàn)。
關(guān)鍵概念 不要相信任何人毫無疑問,任何人(包括你自己)都可能把缺陷引入你的程序邏輯當中。用懷疑的眼光審視所有的輸入和所有的結(jié)果,直到你能證明它們是正確的時為止。
4 編碼的目標是清晰,而不是簡潔
如果要你從簡潔(但是有可能讓人困惑)的代碼和清晰(但是有可能比較冗長)的代碼中選擇,一定要選那些看上去和預(yù)期相符合的代碼,即使它不太優(yōu)雅。例如,將復(fù)雜的代數(shù)運算拆分為一系列單獨的語句,使邏輯更清晰。
想一想,誰會是你的代碼的讀者。這些代碼也許需要一位初級程序員來進行維護,如果他不能理解代碼的邏輯,那么他肯定會犯一些錯誤。復(fù)雜的結(jié)構(gòu)或不常用的語言技巧可以證明你在運算符優(yōu)先級方面淵博的知識,但是這些實際上會扼殺代碼的可維護性。請保持代碼簡單。
不能維護的代碼是不安全的。舉一個極端的例子,過于復(fù)雜的表達式會使編譯器生成錯誤的代碼,許多編譯器優(yōu)化的錯誤就是因此而造成的。
關(guān)鍵概念 簡單就是一種美。不要讓你的代碼過于復(fù)雜。
5 不要讓任何人做他們不該做的修補工作
內(nèi)部的事情就應(yīng)該留在內(nèi)部。私人的東西就應(yīng)該用鎖和鑰匙保管起來。不要把你的代碼初稿示于眾人。不管你多么禮貌地懇求,只要你稍不注意,別人就會篡改你的數(shù)據(jù),然后自以為是地試著調(diào)用“僅用于執(zhí)行”的例行程序。不要讓他們這樣做。
— 在面向?qū)ο蟮恼Z言中,通過將屬性設(shè)為專用(private)來防止對內(nèi)部類數(shù)據(jù)的訪問。在C 中,可以考慮使用Cheshire cat/pimpl idiom。(見參考書目Meyers 97)
— 在過程語言中,你仍然可以使用面向?qū)ο螅╫o)的打包概念,將private數(shù)據(jù)打包在不透明的類型背后,并提供可以操作它們的定義良好的公共函數(shù)。
— 將所有變量保持在盡可能小的范圍內(nèi)。不到萬不得已,不要聲明全局變量。如果變量可以聲明為函數(shù)內(nèi)的局部變量,就不要在文件范圍上聲明。如果變量可以聲明為循環(huán)體內(nèi)的局部變量,就不要在函數(shù)范圍上聲明。
說說“何時”
何時進行防御性編程?你是否在事情不順利時才開始這樣做?或者在整理一些你不理解的代碼時才開始?
不,這是不對的,你應(yīng)該從始到終地使用這些防御性編程的技巧。它們應(yīng)該成為你的第二天性。成熟的程序員已經(jīng)從經(jīng)驗中得到教訓(xùn),在吃過不止一遍的苦頭之后,他們才明白了增加預(yù)防措施是明智的。
在開始編寫代碼時就應(yīng)用防御性策略,比改進代碼時才應(yīng)用要容易得多。如果你很晚才試著將這些策略強加進去,就不可能做到萬無一失。如果你在問題出現(xiàn)后才開始添加防御性代碼,實際上你是在調(diào)試,被動地做出反應(yīng),而不是積極地防患于未然。
然而,在調(diào)試的過程中,甚至在添加新的功能時,你將發(fā)現(xiàn)一些你希望驗證的情況。這常常是添加防御性代碼的好時機。
6 編譯時打開所有警告開關(guān)
大多數(shù)語言的編譯器都會在你“傷了它們感情的時候”給出一大堆錯誤信息。當這些編譯器碰到潛在的有缺陷代碼時(如在賦值之前使用C或C 變量)[3],它們也會給出各種各樣的警告。通常情況下,這些警告可以有選擇地啟用或禁用。
如果你的代碼中充滿了危險的構(gòu)造,你將會得到數(shù)頁的警告信息。糟糕的是,通常的反應(yīng)是禁用編譯器的警告功能,或者干脆不理會這些信息。這兩種做法都不可取。
在任何情況下都要打開你的編譯器的警告功能。如果你的代碼產(chǎn)生了任何的警告信息,立即修正代碼,讓編譯器的報錯聲停下來。在啟用了警告功能之后,不要對不能安靜地完成編譯的代碼感到滿意。警告的出現(xiàn)總是有原因的。即使你認為某個警告無關(guān)緊要,也不要置之不理。否則,總有一天這個警告會隱藏一個確實重要的警告。
關(guān)鍵概念 編譯器的警告可以捕捉到許多愚蠢的編碼錯誤。在任何情況下都啟用它們。確保你的代碼可以安安靜靜地完成編譯。
7 使用靜態(tài)分析工具
編輯器警告是對代碼的一次有限的靜態(tài)分析(即在程序運行之前執(zhí)行的代碼檢查)的結(jié)果。
還有許多獨立的靜態(tài)分析工具可供使用,如用于C語言的lint(以及更多新出的衍生工具)和用于.NET匯編程序的FxCop。你的日常編程工作,應(yīng)該包括使用這些工具來檢查你的代碼。它們會比你的編譯器挑出更多的錯誤。
8 使用安全的數(shù)據(jù)結(jié)構(gòu)
如果你做不到,那么就安全地使用危險的數(shù)據(jù)結(jié)構(gòu)。
最常見的安全隱患大概是由緩沖溢出引起的。緩沖溢出是由于不正確地使用固定大小的數(shù)據(jù)結(jié)構(gòu)而造成的。如果你的代碼在沒有檢查一個緩沖的大小之前就寫入這個緩沖,那么寫入的內(nèi)容總是有可能會超過緩沖的末尾的。
這種情況很容易出現(xiàn),如下面這一小段C語言代碼所示:
char *unsafe_copy(const char *source)
{
char *buffer =
new char[
10];
strcpy(buffer, source);
return buffer;
}
如果source中數(shù)據(jù)的長度超過10個字符,它的副本就會超出buffer所保留內(nèi)存的末尾。隨后,任何事都可能會發(fā)生。數(shù)據(jù)出錯是最好情況下的結(jié)果——一些其他數(shù)據(jù)結(jié)構(gòu)的內(nèi)容會被覆蓋。而在最壞的情況下,惡意用戶會利用這個簡單的錯誤,把可執(zhí)行代碼加入到程序堆棧中,并使用它來任意運行他自己的程序,從而劫持了計算機。這類缺陷常常被系統(tǒng)黑客所利用,后果極其嚴重。
避免由于這些隱患而受到攻擊其實很簡單:不要編寫這樣的糟糕代碼!使用更安全的、不允許破壞程序的數(shù)據(jù)結(jié)構(gòu)——使用類似C 的string類的托管緩沖。或者
對不安全的數(shù)據(jù)類型系統(tǒng)地使用安全的操作。通過把strcpy更換為有大小限制的字符串復(fù)制操作strncpy,就可以使上面的C代碼段得到保護。
char *safer_copy(const char *source)
{
char *buffer =
new char[
10];
strncpy(buffer, source,
10);
return buffer;
}
9 檢查所有的返回值
如果一個函數(shù)返回一個值,它這樣做肯定是有理由的。檢查這個返回值。如果返回值是一個錯誤代碼,你就必須辨別這個代碼并處理所有的錯誤。不要讓錯誤悄無聲息地侵入你的程序;忍受錯誤會導(dǎo)致不可預(yù)知的行為。
這既適用于用戶自定義的函數(shù),也適用于標準庫函數(shù)。你會發(fā)現(xiàn):大多數(shù)難以察覺的錯誤都是因為程序員沒有檢查返回值而出現(xiàn)的。不要忘記,某些函數(shù)會通過不同的機制(例如,標準C庫的errno)返回錯誤。不論何時,都要在適當?shù)募墑e上捕獲和處理相應(yīng)的異常。
10 審慎地處理內(nèi)存(和其他寶貴的資源)
對于在執(zhí)行期間所獲取的任何資源,必須徹底釋放。內(nèi)存是這類資源最常提到的一個例子,但并不是唯一的一個。文件和線程鎖也是我們必須小心使用的寶貴資源。做一個好的“管家”。
不要因為覺得操作系統(tǒng)會在你的程序退出時清除程序,就不注意關(guān)閉文件或釋放內(nèi)存。對于你的代碼還會執(zhí)行多長時間,是否會耗盡所有的文件句柄或占用所有的內(nèi)存,其實你一無所知。你甚至不能肯定操作系統(tǒng)是否會完全釋放你的資源,有的操作系統(tǒng)就不是這樣的。
有一個學(xué)派說:“在確定你的程序可以運行之前,不要擔心內(nèi)存的釋放;只有在能夠確定之后再添加所有相關(guān)的釋放操作?!边@種觀點大錯特錯,是一種荒謬而且危險的做法。它會使你在使用內(nèi)存時出現(xiàn)許許多多的錯誤;你將不可避免地在某些地方忘記釋放內(nèi)存。
關(guān)鍵概念** 重視所有稀有的資源**。審慎地管理它們的獲取和釋放。
Java和.NET使用垃圾回收器來執(zhí)行這些繁重的清潔工作,所以你可以“忘記”釋放資源。讓它們進入工作狀態(tài),這樣在運行時將會不時地進行清掃。這真是一種享受,不過,不要因此而對安全性抱有錯誤的想法。
你仍然需要思考。你必須顯式地終止對那些不再需要,或不會被自動清除的對象的引用;不要意外地保留對對象的引用。不太先進的垃圾回收器也很容易會被循環(huán)引用蒙蔽(例如,A引用B,B又引用A,除此之外沒有對A和B的引用)。這會導(dǎo)致對象永遠不會被清除;這是一種難以發(fā)現(xiàn)的內(nèi)存泄漏形式。
11 在聲明位置初始化所有變量
這是一個顯而易見的問題。如果你初始化了每個變量,它們的用途就會是明確的。依靠像“如果我不初始化它,我就不關(guān)心初始值”的經(jīng)驗主義是不安全的。
代碼將會發(fā)展。未初始化的值以后可能隨時都會變成問題。
C和C 使這個問題更加復(fù)雜化。如果你意外地使用了一個沒有初始化的變量,那么你的程序在每次運行的時候都將得到不同的結(jié)果,這取決于當時內(nèi)存中的垃圾信息是什么。在一個地方聲明一個變量,隨后再對它進行賦值,在這之后再使用它,這樣會為錯誤打開一個窗口。
如果賦值的語句被跳過,你就會花費大量的時間來尋找程序隨機出現(xiàn)各種行為的原因。在聲明每個變量的時候就對它進行初始化,就可以把這個窗口關(guān)上,因為即使初始化時賦的值是錯誤的,至少出現(xiàn)的錯誤行為也是可以預(yù)知的。
比較安全的語言(如Java和C#)通過為所有變量定義初始值,回避了這個易犯的錯誤。在聲明變量的時候?qū)λM行初始化仍然是一種好的做法,這樣可以提高代碼的明確性。
12 盡可能推遲一些聲明變量
盡可能推遲一些聲明變量,可以使變量的聲明位置與使用它的位置盡量接近,從而防止它干擾代碼的其他部分。這樣做也使得使用變量的代碼更加清晰。你不再需要到處尋找變量的類型和初始化,在附近聲明使這些都變得非常明顯。
不要在多個地方重用同一個臨時變量,即使每次使用都是在邏輯上相互分離的區(qū)域中進行的。變量重用會使以后對代碼重新完善的工作變得異常復(fù)雜。每次都創(chuàng)建一個新的變量——編譯器會解決任何有關(guān)效率的問題。
13 使用標準語言工具
在這方面,C和C 都是一場噩夢。它們的規(guī)范有許多不同的版本,使得許多情況成為了其他實現(xiàn)的未定義行為。現(xiàn)如今有很多種編譯器,每個編譯器都有一些與其他編譯器稍有不同的行為。這些編譯器大部分是相互兼容的,但是仍然存在大量的繩索會套住你的脖子。
明確地定義你正在使用的是哪個語言版本。除非你的項目要求你(最好是有一個好的理由),否則不要將命運交給編譯器,或者對該語言的任何非標準的擴展。如果該語言的某個領(lǐng)域還沒有定義,就不要依賴你所使用的特定編譯器的行為(例如,不要依賴你的C編譯器將char作為有符號的值對待,因為其他的編譯器并不是這樣的)。
這樣做會產(chǎn)生非常脆弱的代碼。當你更新了編譯器之后,會發(fā)生什么?
一位新的程序員加入到開發(fā)團隊中,如果他不理解那些擴展,會發(fā)生什么?依賴于特定編譯器的個別行為,將導(dǎo)致以后難以發(fā)現(xiàn)的錯誤。
14 使用好的診斷信息日志工具
當你編寫新的代碼時,常常會加入很多診斷信息,以確定程序的運行情況。在調(diào)試結(jié)束后是否應(yīng)該刪除這些診斷信息呢?保留這些信息對以后再次訪問代碼會帶來很多方便,特別是如果在此期間可以有選擇地禁用這些信息。
有很多診斷信息日志系統(tǒng)可以幫助實現(xiàn)這種功能。這些系統(tǒng)中很多都可以使診斷信息在不需要的時候不帶來任何開銷;可以有選擇地使它們不參加編譯。
15 審慎地進行強制轉(zhuǎn)換
大多數(shù)語言都允許你將數(shù)據(jù)從一種類型強制轉(zhuǎn)換(或轉(zhuǎn)換)為另一種類型。這種操作有時比其他操作更成功。如果試著將一個64位的整數(shù)轉(zhuǎn)換為較小的8位數(shù)據(jù)類型,那么其他的56位會怎么樣呢?你的執(zhí)行環(huán)境可能會突然拋出異常,或者悄悄地使你數(shù)據(jù)的完整性降級。很多程序員并不考慮這類事情,所以他們的程序就會表現(xiàn)出不正常的行為。
如果你真的想使用強制轉(zhuǎn)換,就必須對之深思熟慮。
你所告訴編譯器的是:“忘記類型檢查吧:我知道這個變量是什么,而你并不知道?!蹦阍陬愋拖到y(tǒng)中撕開了一個大洞,并直接穿越過去。這樣做很不可靠。
如果你犯了任何一種錯誤,編譯器將只會靜靜地坐在那里小聲嘀咕道:“我告訴過你的?!比绻愫苄疫\(例如使用Java或C#),運行時可能會拋出異常以讓你了解發(fā)生了錯誤,但這完全依賴于你要進行的是什么轉(zhuǎn)換。
C和C 對于數(shù)據(jù)類型的精度并不明確,所以對于數(shù)據(jù)類型的可互換性不要做任何假設(shè)。不要假設(shè)int和long的大小相同并且可以相互賦值,即使你在你的平臺上僥幸可以這樣做。代碼可以在平臺之間移植,但是糟糕的代碼可移植性很差。
16 細則
低級別防御性代碼的編寫技巧有很多。這些技巧是日常
編程工作的組成部分,包含在對現(xiàn)實世界的一種健康的懷疑當中。下面的幾條細則值得考慮:
提供默認的行為 大多數(shù)語言都提供了一條switch語句;這些語言都將碰到default case的執(zhí)行情況。如果default case是錯誤的,在代碼中將錯誤情況明示出來。如果一切都正常,也要在代碼中明示順利執(zhí)行的情況,只有這樣維護代碼的程序員才會理解程序的執(zhí)行情況。
同樣地,如果你要編寫一條不帶else子句的if語句,停下來想一想,你是否應(yīng)該處理這個邏輯上的默認情況。
遵從語言習慣
這條簡單的建議將確保你的讀者可以明白你所編寫的所有代碼。他們做出的錯誤設(shè)想會更少。
檢查數(shù)值的上下限
即使是最基本的計算,也會使數(shù)值型變量上溢或下溢。對此要非常注意。語言規(guī)范或核心庫提供了一些機制,用來確定各個標準類型的大小——別忘了使用這些機制。確保你了解所有可用的數(shù)值類型,以及每種類型最適合的情況。
檢查并確保每一次運算都是可靠穩(wěn)定的。例如,確保自己一定不要使用可能會造成除0錯誤的值。
正確設(shè)置常量
C或C 語言的程序員真的應(yīng)該對常量的設(shè)置保持高度警惕,這會讓日子好過很多。盡可能將所有可以設(shè)置成常量的都設(shè)為常量。這樣做有兩個好處:首先,常量的限制條件可以充當代碼記錄;其次,常量使編譯器可以找到你所犯下的愚蠢錯誤。這樣,你就可以避免修改超出上下限的數(shù)據(jù)了。
原文鏈接:
http://www.uml.org.cn/codeNorms/201007165.asp
https://blog.csdn.net/everpenny/article/details/6316698
—— The End ——