用C++寫代碼的時候總是避免不了處理錯誤,通常來說有兩種方式,通過函數的返回值或者拋出異常。C語言的錯誤處理一概是通過函數的返回值來判斷的,通常是返回0
、NULL
或者-1
表示錯誤,或者直接返回錯誤代碼,具體是哪種方式沒有統一的規定,各種API也各有各的偏好。譬如fopen
函數,當成功時返回文件指針,失敗時返回NULL
,而POSIX標準的open
函數則在成功時返回0
或者正數,失敗時返回-1
,然後須要再通過全局變量errno
來判斷具體錯誤是什麼,配套的還有一系列perror
、strerror
這樣的函數。性能
C++號稱向下兼容C語言,於是就將C語言通過返回值的錯誤處理方式也搬了進來。但C++最大的不一樣是引入了異常機制,能夠用throw
產生一個異常,並通過try
和catch
來捕獲。於是就混亂了,究竟是什麼時候使用返回值表示錯誤,什麼時候使用異常呢?首先簡單談論一下異常和返回值的特點。spa
錯誤信息豐富,便於獲得錯誤現場code
代碼相對簡短,不須要判斷每個函數的返回值orm
使控制流變得複雜,難以追蹤接口
開銷相對較大作用域
性能開銷相對小字符串
避免定義異常類it
程序員經常「忘記」處理錯誤返回值class
每個可能產生錯誤的函數在調用後都須要判斷是否有錯誤stream
與「真正的」返回值混用,須要規定一個錯誤代碼(一般是0
、-1
或NULL
)
個人觀點是,用異常來表示真正的、並且不太可能發生的錯誤。所謂不太可能發生的錯誤,指的是真正難以預料,但發生了卻又不得不單獨處理的,譬如內存耗盡、讀文件發生故障。而在一個字符串中查找一個子串,若是沒有找到顯然應該是用一個特殊的返回值(如-1
),而不應該拋出一個異常。
一句話來概況就是不要用異常代替正常的控制流,只有當程序真的「不正常」的時候,纔使用異常。反過來說,當程序真正發生錯誤了,必定要使用異常而不是返回一個錯誤代碼,因爲錯誤代碼總是傾向於被忽略。若是要保證一個以返回值來表示錯誤代碼的函數的錯誤正確地向上傳遞,須要在每個調用了可能產生錯誤的函數後面都判斷一下是否發生了錯誤,一旦發生了不可解決的錯誤,就要終止當前函數(並釋放當前函數申請的資源),然後向上傳遞錯誤。這樣一來錯誤處理代碼會被重複地寫好幾遍,十分冗雜,譬以下面代碼:
int func(int n) { int fd = open("path/to/file", O_RDONLY); if (fd == -1) { return ERROR_OPEN; } int* array = new[n]; int err; err = do_something(fd, array); if (err != SUCCESS) { delete[] array; return err; } err = do_other_thing(); if (err != SUCCESS) { delete[] array; return err; } err = do_more_thing(); if (err != SUCCESS) { delete[] array; return err; } delete[] array; return SUCCESS; }
對使用異常容易增長函數出口的指控其實是不成立的,因爲即便使用返回值,這些出口也是免不了的,除非程序員有意或無意忽略掉,但異常是不可忽略的。若是你認爲能夠把判斷錯誤的if
語句縮寫到一行使代碼變得「更清晰」,那麼我只能說是自欺欺人。
有些錯誤幾乎總是能夠被當即恢復(譬如前面所說的查找一個字符串不存在的子串,甚至都不能說這是一個「錯誤」),並且返回值自己就傳遞必定信息,就不須要使用異常了。
鑑於C++沒有統一的ABI,並不建議在模塊的接口上使用異常。若是要使用,就要把可能曝露給用戶的異常所有聲明出來,不要把其餘類型的異常丟給用戶去處理,尤爲是內部狀態——模塊的使用者一般也不會關心模塊內部具體是哪條語句發生錯誤了。
有一個相當實際的問題是,如何處理構造函數的錯誤?我們都知道構造函數是沒有返回值的,怎麼辦呢?一般有三種常見的處理方法,標記錯誤狀態、使用一個額外的initialize
函數來初始化,或者直接拋出異常。
合格的C++程序員都知道C++的析構函數中不應該拋出異常,一旦析構函數中的異常沒有被捕獲,整個程序都要被停止掉。於是許多人就對在構造函數中拋出異常也產生了對等的恐懼,寧可以使用一個額外的初始化函數在裏面初始化對象的狀態並拋出異常(或者返回錯誤代碼)。這樣作違背了對象產生和初始化要在一塊兒的原則,強迫用戶記住調用一個額外的初始化函數,一旦沒有調用直接使用了其餘函數,其行爲極可能是未定義的。
使用初始化函數的唯一好處多是避免了手動釋放資源(釋放資源的操做交給析構函數來作),因爲C++的一個特點是構造函數拋出異常以後析構函數是不會被調用的,因此若是你在構造函數裏面申請了內存或者打開了資源,須要在異常產生時關閉。但想一想看其實並不能徹底避免,因爲有些資源多是要在可能產生錯誤的函數調用過後纔被申請的,還是無法徹底避免手工的釋放。
標記錯誤狀態也是一種常見的形式,譬如STL中的ifstream
類,當構造時傳入一個無法訪問的文件做爲參數,它不會返回任何錯誤,而是標記的內部狀態爲不可用,用戶須要手工通過is_open()
函數來判斷是否打開成功了。同時它還有good()
、fail()
兩個函數,同時也重載了bool
類型轉換運算符用於在if
語句中判斷。標記狀態的方法在實踐中相當醜陋,因爲在使用前總是須要判斷它是否「真的創建成功了」。
最直接的方法還是在構造函數中拋出異常,它並不會向析構函數中拋出異常那樣有嚴重的後果,只是須要注意的是拋出異常以後對象沒有被創建成功,析構函數也不會被調用,因此應該自行把申請的資源所有都釋放掉。
構造函數與普通函數有一個很不一樣特性,就是構造函數能夠有初始化列表,例以下面的代碼:
class B { public: B(int val) : val_(val * val) { } private: int val_; };class A { public: A(int val) : b_(val) { a_ = val; } private: int a_; B b_; };
以上的代碼中A
的構造函數的函數體的語句在執行以前會先調用B
的構造函數,這時候問題在於,若是B
的構造函數拋出了異常,A
該如何捕獲呢?一個迂迴的作法是在A
中把B
的實例聲明爲指針,在構造函數和析構函數中分別創建和刪除,這樣就能捕獲到異常了。不過,實際上是有更簡單的作法的。下面我要介紹一個C++的很不常見的語法:函數做用域級別的異常捕獲。
class B { public: B(int val) : val_(val * val) { throw runtime_error("wtf from B"); } private: int val_; };class A { public: A(int val) try : b_(val) { a_ = val; } catch (runtime_error& e) { cerr << e.what() << endl; throw runtime_error("wtf from A"); } private: int a_; B b_; };
注意上面A
的構造函數,在參數列表後和初始化列表前增長了try
關鍵字,然後構造函數就被分割爲了兩部分,前面是初始化,後面是初始化時的錯誤處理。須要指出的是,catch
塊裏面捕獲到的異常不能被忽略,即catch
塊中必須有一個throw
語句從新拋出異常,若是沒有,則默認會將原來捕獲到的異常從新拋出,這和通常的行爲是不一樣的。例以下面代碼運行能夠發現A
會將捕獲到的異常原封不動拋出:
class A { public: A(int val) try : b_(val) { a_ = val; } catch (runtime_error& e) { cerr << e.what() << endl; } private: int a_; B b_; };
這種語法是C++的標準,並且目前已經被全部的主流C++編譯器支持(VS20十、g++ 4.二、clang 3.1),因此幾乎不存在兼容性問題,大可放心使用。