許久以後,你會感謝自己寫的異常處理代碼~

很多時候,我們因爲關注最終的結果,而總是忽略其它的情況。所以我們寫的代碼並不是那麼的健壯。

這篇文章屬於程序員內功修煉,值得一看。

寫代碼的時候,有幾個階段可以參考一下(魚鷹經歷並總結):

階段一:只要最終的結果

處於這個階段的一般都是初學者,眼裏只有一個目標,那就是程序運行成功,從不考慮其他因素。

比如一個簡單的 SPI 驅動程序,最終的目標只要一個,通信成功。所以當需要延時時就會採用死等方式等待結果,而不會考慮其他可能出現的結果,比如因爲某種原因導致引腳電平被持續一個電平不變,導致死循環;比如因爲你的延時,導致其他人功能無法及時處理。

這個時候的思考總是過於理想化,不是因爲自己太理想,太樂觀,而是因爲沒有足夠的經歷告訴你,這樣寫是有問題的。

因爲沒有經歷過,所以初學者也就考慮不了那麼多,所以你看初學者的代碼會很簡單,是一條直線,邏輯很清晰,沒有岔路。

階段二:做一些常見的異常處理

隨着經歷越多(不管是網上看到的還是自己經歷的),漸漸地,自己的代碼變得多了一些,功能還是那個功能,只是這時候的你考慮的更多,更全面,你漸漸的增加了各種異常處理。

比如你不再使用死等方式延時,而是增加了一個等待時間的超時處理;又比如你寫的程序不再只有if裏面的內容(條件爲真),還有else(條件爲假)。

這個時候的你眼裏不再只有最終的結果,還有在運行過程中可能出現的其他情況,並且會對這些情況做處理。

階段三:懷疑一切

這個階段的你不再相信任何東西了,即使它是那麼的可靠。

你總是在函數開始處檢查傳入的參數(如果有的話),判斷指針是否爲空,判斷數據是否在需要的範圍內,等等。

總是在使用指針、數組的時候小心翼翼,深怕一不小心就越界了,而這種BUG只有經歷的人才懂到底有多難查。

總是在異常的地方做出一些動作,如返回錯誤代號、如打印錯誤消息等。

不管怎樣,在出現問題後,你總是能夠快速的定位問題,而這,得益於你對異常的處理。

階段四:做好善後工作

階段三可能讓你很快的定位問題,但是一旦出現問題,程序還是無法正常執行下去,比如申請的資源(內存、信號量等)沒釋放,又比如關閉的中斷在異常後未重新打開等等。

所以異常處理代碼除了能很快定位BUG外,還要做好善後處理,這樣才能讓程序健壯的一直運行下去。

魚鷹曾在《代碼寫完了,你要花多少時間測試?》一文中介紹了一些調試方法,今天,繼續更深入的探討。

(uCOS II代碼片段)

很多人其實不明白,爲什麼一定要在函數開頭檢查參數,這不是很浪費時間的嗎(從上面可以看到,參數檢查有時候比真正需要執行的代碼還要多)?不說指針,就說一些普通變量,爲什麼要檢查?檢查的意義又在哪裏?

浪費時間?

首先說說浪費時間的問題,確實,因爲總是在開頭檢查參數,會浪費CPU的時間(魚鷹一開始也非常不喜歡),但是當你經歷了各種難查的BUG之後,你會發現,這點時間還是浪費的起。

而事實上,軟件開發一般都會有兩個版本,一個是Debug版本,一個是Release版本,只要通過宏進行控制,那麼就可以在穩定之後,不再檢查這些參數了。

但是有些重要的數據,即使穩定了,也不能放棄對它的檢查,否則一旦出現問題,就是災難。

所以在檢查時,還要考慮這些檢查是否在Release版本也是需要的,即按重要性分開檢查。

爲什麼要檢查?檢查的意義又在哪裏?

這些檢查就像是函數的護城河,保證即使參數錯誤,也不會導致異常問題,比如數組越界,計算出錯等。

那麼我們要問了,參數怎麼會錯誤,代碼都是固定死的,有經驗的都知道,參數是保存在棧或寄存器的,怎麼會錯呢?

你說內存數據保存時有問題?保存時爲1,讀出時爲0?

別逗了,如果真是這樣,那還怎麼玩?程序根本沒辦法跑好吧(遇到強幹擾可能會出現這種情況,甚至可能CPU執行流程都是亂的,最終只能重啓,這個魚鷹倒還沒遇到過)。

我們可以認爲存在RAM和FLASH的數據在存儲和讀取方面沒有問題,那麼又是什麼導致了參數出錯呢?

棧溢出

前面說過,參數有可能保存在棧裏面,如果有些棧溢出了,參數被破壞也就可以理解了。(關於棧,可參考筆記《今天,你的棧溢出了嗎?》)

數組越界、野指針等指針問題

一旦越界,那麼產生的破壞力不可想象。

所謂越界,就是修改了不屬於你的變量。比如一個數組,你操作了數組外的數據(不管是數組前面的還是數組後面的)。

而越界根據位置又可以分爲三種情況。

第一種,棧(stack),比如你在棧裏面申請了一個數組,越界了,那麼修改的就是棧內容。

第二種:堆(Heap)。你通過malloc申請的內存,如果操作失誤,那麼就會修改不屬於你的空間。

第三種。全局變量(data)。如果操作失誤,也會出現問題。

其實,越界這種問題不一定就只會修改這些單獨的區域,可能是兩個區、三個區一起修改了,畢竟指針可不管修改的地址到底屬於哪個區,還有一種是野指針導致的異常操作,那麼它修改的位置只有鬼知道了(鬼好像不懂程序)。

比如你申請了一個數組,通過傳入的參數修改數據,如果不限制參數大小,你確定它不會把你數組後所有的內存都給清零?!

如果硬要爲上面三種情況劃分處理難度等級,那麼最容易也最快解決的就是全局變量的修改,爲什麼?因爲地址比較固定,而ARM內核有神器處理這種情況,如果出現概率高的話,一查一個準,所以魚鷹都不怎麼苦惱這種問題。

最難解決的是堆的修改,這種問題比棧更難找。原因就在於,內存動態申請和釋放,可能這次修改的是這個位置,沒出現問題,下次修改另一個地址,就出現了問題,這種是最難查的。而棧的空間一般不會太大,而且他的存取都是有規律的,要稍微好查一些。

可能你會問,參數會保存在寄存器裏面,那麼寄存器的數據有可能被異常修改嗎?

異常修改的可能性很小,因爲對於C語言而言,寄存器是透明的,用戶很難操作這個。

但是,雖然說參數傳入之後被修改的可能性很小,但是傳入前修改的可能性還是很大的,比如你傳入的參數是一個全局變量,那麼這個全局變量是可能被異常修改的啊!

所以,參數檢查,是一個健壯程序必須要有的,這是防止產生重大問題最重要的護城河。而越早檢查出問題,那麼越容易定位問題

蝴蝶效應大家都知道,千里之堤毀於蟻穴大家也知道,用在程序裏面也是很合適的。

可能你會說,我對自己有信心,我的技術槓槓的,絕對不會出問題。

真的是這樣嗎?

第一、時間久了自己都忘了。

工作時,常常完成了一個項目,下一個項目馬上來了,如果老項目需要維護,不需要一兩年,只要一兩個月,如果你沒有參數檢查、異常處理的好習慣,一旦你修改了代碼,那麼很可能因爲某些疏忽,導致難以發現的BUG,而解決這些BUG的時間,比你寫這些異常處理代碼更多。

但是在你剛開始寫這份代碼的時候,因爲思路清晰,考慮的比較多,有哪些異常很清楚,那麼很容易寫出那些異常處理代碼。

就比如魚鷹去年寫了一份通過位綁定地址,批量配置引腳的時鐘、寄存器信息的代碼,那麼今年再複用代碼的時候,因爲自己的疏忽,很可能需要大量的時間解決BUG,那我寫這份代碼的意義就不存在了(寫這份代碼就是爲了在標準庫中通過端口和引腳號快速配置引腳),而如果說,一旦因爲疏忽導致的問題,程序會自動幫你檢查,那麼解決問題就很快了。

第二、維護。

不管是別人維護,還是自己維護,當項目需要更改需求時,如果因爲某些疏忽,導致了BUG,那麼解決起來費時費力,而且即使你這次解決了,難道下次還要重蹈覆轍嗎?

第三、合作開發

一個項目可能不是一個人完成的,而是多人合作。而每個人的水平有高有低,你敢說別人不會寫出有問題的代碼?你敢說自己一定不會寫出有問題的代碼?

而且即使別人沒有寫出BUG,但因爲某些原因,需要修改或屏蔽你的代碼,如果你的代碼能自動提醒出這些異常,那麼定位問題也就不難了。

而魚鷹爲什麼寫代碼的時間會比測試長,除了掌握大量的調試技巧外,就是因爲在寫的時候,會考慮很多,並且這些考慮,大部分會以代碼的形式存在,少量的會以註釋或者#warning、#error 形式存在,而其中,最好的方式是代碼形式,因爲它能保證程序正常運行,即使不能正常運行,也應該打印消息以提醒用戶問題在何處。

所以,魚鷹總是很慶幸自己當初花了不少時間去寫異常處理代碼,而這些代碼,如果需要事後彌補的話,相信花的時間會更多(定位問題、回想當初自己如何思考、補充異常處理代碼,這些都要時間)。

1.美商務部又將33家中國公司/機構列入“實體清單”,包括360、雲從科技

2.嵌入式軟件測試的10條祕訣

3.一個單片機ADC的挖坑填坑之旅

4.熱了好多年,其實物聯網剛剛邁過谷底~

5.如何讓STM32優雅地“說”hello world?

6.常見類型ADC選型必知!

免責聲明:本文系網絡轉載,版權歸原作者所有。如涉及作品版權問題,請與我們聯繫,我們將根據您提供的版權證明材料確認版權並支付稿酬或者刪除內容。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章