我們本系列的上篇文章說了自己分配內存的重要性,因為那很容易引進程序的崩潰。在原始的 C/C++ 中,這是經常發生的一件事情。除此之外引起崩潰的原因非常多,常見的有:訪問指針異常、除0錯、訪問 NULL 指針、文件訪問錯誤以及作業系統異常等等。對於一個剛出校門走上工作崗位的 C 語言程式設計師來說,在默認的情況下 C 語言程序只要遇到以上情況就一定會崩潰這種事實是非常打擊人的。如果你用的是 vc、bcb 這樣著名的 ide 還是好的,它們會默認處理一些常見錯誤。如果這位初學者的崗位是在 linux 下的,那基本上就是災難了:他一定會經常碰到 linux 下最著名的問題 -- 段錯誤。段錯誤的表現為,寫得好好的程序總會在某個時刻彈出這個信息然後就退出了。然後你就找啊找,到底哪裡出了問題。你會在網上搜索各個解決辦法,估計大部分找傳過來後都會在各個可能出錯的地方打上 printf。如果你的程序是單線程的,那倒也罷了,printf 會很有效的提示出錯誤的位置。如果你的程序是多線程的,而且還是伺服器 ... 我個人覺得,除非您所在的公司很有錢,要不這一定是個坑,幾年內是出不來的,工作之餘多存點錢以便被炒的時候 ... 真的,一個剛工作的初學者能維護好一個 C/C++ 伺服器程序的可能性無限接近於0!但在我接觸過使用 delphi 的公司中,居然有很多伺服器是初學者寫的,雖然其中有不少錯誤,但是他們寫的程序是不會動不動就崩潰的。同樣的道理也適用於 java 和 C#。有一定工作經驗的同學一定會同意我的這個觀點。
那麼這種情況是怎樣造成的呢?原因就在於默認的情況下,C語言的錯誤處理就是直接退出!說實在的我覺得這真是一個腦殘的設計思想,因為年代久遠已經無法追溯這是怎樣形成的了。但現狀就是如此,我們只能自己想辦法去處理和適應。我們先來看看其他語言是怎麼處理的,就先以受眾最多的 java 為例吧。學過 java 的同學應該都學過它的歷史都知道 java 本來就是為了解決 C 語言的很多問題而產生的,既然錯誤的處理方式很爛,那 java 當然是要重點處理的了。java 的處理方式就是加上異常處理,當出錯時說清楚怎樣做就可以了,不需要退出(具體代碼我們就貼了),而且還要求在所有可能出錯的地方都要寫上異常處理,就是那個著名的 try 代碼塊。不過我個人覺得 java 的處理方式太深究化,實際工作中太麻煩,我個人覺得最好的方式是 delphi 的,它象 java 一樣可能以顯示存在,不存在的情況下則會默認只跳出一層,而不是整個程序(估計初學者也看不懂什麼是跳出一層,這個一時也解釋不清楚)。因為 try 的代碼塊處理方式非常有效,所以現在的語言大多這樣處理。當然 golang 是例外的,因為 try 的概念已經根深蒂固,所以我花了近兩周才學會 golang 的異常處理,總的來說 golang 的處理介於 java 和 delphi 之間,印象中它是唯一不使用 try 代碼塊的異常處理語言。
好了,既然業界都有處理方法了,C/C++ 裡不可能沒有這個 try 吧。先說好消息,是的,有的。等等,先別高興得太早 C++ 中的異常處理基本上只針對類的異常,對於系統的異常是一樣崩潰的。而 C 語言裡呢,各個廠商也擴展了 try 語言塊,它的調用方式大概是這樣:
try{各種操作 } catch(...) { 錯誤處理 }這裡又能正常運行了
你一定以為萬事大吉了。如果您用的是 bcb,那差不多吧。如果是 vc 情況很複雜,我們先說 gcc 的情況吧:gcc 下完全沒有用,而且默認下根本不支持 try。得了,我們都用 vc ... 好,我們來看下 vc 的情況,測試代碼如下圖:
源碼如下(因為可能會被過濾某些符號,所以請參考上面的圖片):
int _tmain(int argc, _TCHAR* argv[]){ //char * s = "1234567890"; char * s = NULL; //char s[] = "1234567890"; //換成這個數組聲明的方式內存就不會報錯了 //char * s = (char *)malloc(11); //C++ 裡還要加強制轉換 try{ memset(s, 0, 11); //一定要清空 strcpy(s, "abc"); //printf("Hello World!\r\n"); printf("Hello World!%s\r\n", s); //free(s); } catch(...){} printf("正常退出\r\n"); //C中的 try 是無用的,到不了這裡 return 0;}
如果是 bcb 那麼這段代碼會輸出 "正常退出" 的。如果是 java 或者 delphi C# 等現代語言也會,但 VC 的情況很複雜,以下是其 2010 版本的發布模式的編譯運行結果:
可以看到,這裡的 try 根本沒起使用。不過 VC 下另有 __try 語言塊支持,並且還有 debug 模式,在各種選項的組合下 VC 是可以支持的,不過時間關係我們就不展示了。總之 VC 是可以做到的,不過不是用默認的 C/C++ 異常處理方式。
正因為以上的 C/C++ 異常處理的複雜性,特別是 gcc 下沒有好的處理方法(暫時)。所以現在業界的做法大多其實是不做異常處理!是的,您沒看錯,至少是沒有做太多的異常處理,開源界就更是如此了,開源界基本上不處理或者是用長跳轉的方式(具體我們就不介紹了)。還有一種常見的就是加退出處理函數,在程序就要崩潰退出前顯示一個對話框,例如常見的迅雷的錯誤退出對話框和 firefox 的錯誤對話框想必大家都見過吧。你會說這麼恐怖!那麼伺服器程序怎麼辦?是這樣的,通常伺服器會弄成兩個程序,一個是主業務,跑邏輯,另外一個專門監控這個主業務程序,如果前面的那個崩潰了,那麼監控的這個會把它再啟動 ... 雖然不可思議,但事實就是如此,比如大家常用的 php就是這樣的。另外一些單機程序也是,比如早期的 google 瀏覽器的部分功能也是獨立的 exe 在後臺跑的。遺憾的是,很多國內的公司並不知道這樣做。
要說的是,雖然所說 gcc 不久會有"真正的"異常支持,VC也可以實現 java、C# 那種級別的異常支持。但是異常不是萬能的,有一些錯誤是無法用異常跳過的,正確的方式還是寫好程序,考慮好可能發生的錯誤情況。比如我從業生涯中就見過好多次整個 java 程序都崩潰的情況,delphi 也是;golang 中的 cgo 異常也是無法完全處理的(所以我現在基本不用 cgo,也希望大家發行量不要用)。有個真實的故事:過去手機程序 kjava 流行時有一個同時面對公司 kjava 程序莫名其妙的運行結果時抱怨還不如象 C 語言一樣直接退出呢。所以異常這個東西也是雙刃劍,既然現在 C 對它的支持還不完善那還不如不要用它!
最後我想說,我個人覺得使用監控程序的方式目前來說是最好的解決方案,當然這可能和我主要從事伺服器端的開發有關係。