天天看點

《c++語言導學》——3.4 錯誤處理

本節書摘來自華章計算機《c++語言導學》一書中的第3章,第3.4節,[美] 本賈尼·斯特勞斯特盧普 更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視。

錯誤處理是一個略顯繁雜的主題,它的内容和影響都遠遠超越了語言特性的層面,而應歸結為程式設計技術和工具的範疇。不過c++還是提供了一些有益的功能,其中最主要的一個工具就是類型系統本身。在建構應用程式時,通常的做法不是僅僅依靠内置類型(如char、int和double)和語句(如if、while和for),而是建立更多适合應用的新類型(如string、map和regex)和算法(如sort()、find_if()和draw_all())。這些進階成分簡化了程式設計,減少了産生錯誤的機會(例如你大概不會把周遊樹的算法應用在對話框上),同時也增加了編譯器捕獲錯誤的機率。大多數c++的成分都緻力于設計并實作優雅而高效的抽象模型(例如使用者自定義類型以及基于這些自定義類型的算法)。這種子產品化和抽象機制(特别是庫的使用)的一個重要影響就是運作時錯誤的捕獲位置與錯誤處理的位置被分離開來。随着程式規模不斷增長,特别是庫的應用越來越廣泛,處理錯誤的規範和标準變得愈加重要。程式員應該在開始開發程式後,盡早地設計和描述錯誤處理的政策。

3.4.1 異常

讓我們重新考慮vector的例子。對2.3節中的向量,當我們試圖通路某個越界的元素時,應該做什麼呢?

vector的作者并不知道使用者在面臨這種情況時希望如何處理(通常情況下,vector的作者甚至不知道向量被用在何種程式場景中)。

vector的使用者不能保證每次都檢測到問題(如果他們能做到的話,越界通路也就不會發生了)。

是以最佳的解決方案是由vector的實作者負責檢測可能的越界通路并通知使用者,然後vector的使用者可以采取适當的應對措施。例如,vector::operator[]()能夠檢測到潛在的越界通路錯誤并抛出一個out_of_range異常:

《c++語言導學》——3.4 錯誤處理

throw負責把程式的控制權從某個直接或間接調用了vector::operator[]()的函數轉移到out_of_range異常處理代碼。為了實作這一目标,實作部分需要解開(unwind)函數調用棧以便傳回主調函數的上下文。換句話說,異常處理機制把程式的控制權從目前作用域轉移到處理該類型錯誤的代碼,在必要的時候調用析構函數(見4.2.2節)。例如:

《c++語言導學》——3.4 錯誤處理

https://yqfile.alicdn.com/ae5b94d5c920074124778908706c63fc44918221.png" >

我們把可能發生異常的可疑程式放在一個try塊當中。顯然,對v[v.size()]的指派操作将會出錯。是以,程式進入到提供了out_of_range錯誤處理代碼的catch從句中。out_of_range類型定義在标準庫中(在中),事實上,一些标準庫容器通路函數也使用它。

通過使用異常處理機制,錯誤處理變得更簡單,條理性和可讀性也得到了加強。但是也要注意不能過度使用try語句。4.2.2節進一步介紹一些技術——稱為資源請求即初始化(resource aquisition is initialization),這些技術使得錯誤處理簡單易用,具有較好的系統性。

我們可以把一個永遠不會抛出異常的函數聲明成noexcept。例如:

《c++語言導學》——3.4 錯誤處理

https://yqfile.alicdn.com/05db7dec45743d10ea77c228e4532785c109640d.png" >

一旦真的發生了錯誤,函數user()還是會抛出異常,此時标準庫函數terminate()立即終止目前程式的執行。

3.4.2 不變式

使用異常機制通報越界通路錯誤是函數檢查實參的一個示例,此時,因為基本假設,即所謂的前置條件(precondition)沒有滿足,是以函數将拒絕執行。在正式地說明vector的下标運算符時,我們應該規定類似于“索引值必須在[0:size())範圍内”的規則,這一規則在operator[]()内被檢查。記号[a:b)指定了一個半開區間,其中a位于區間内而b不在。無論什麼時候隻要我們試圖定義一個函數,就應該考慮它的前置條件是什麼,以及檢驗該條件的過程是否足夠簡潔。

然而在上面的定義中,operator[]()作用于vector的對象并且隻在vector的成員有“合理”的值時才有意義。特别是,我們說過“elem指向一個含有sz個double型元素的數組”,但這隻是注釋中的說明而已。對于類來說,這樣一條假定某事為真的聲明稱為類的不變式(class invariant),簡稱為不變式(invariant)。建立類的不變式是構造函數的任務(進而成員函數可以依賴于該不變式),它的另一個作用是確定當成員函數退出時不變式仍然成立。不幸的是,我們的vector構造函數隻履行了一部分職責。它正确地初始化了vector成員,但是沒有檢驗傳入的實參是否有效。考慮如下情況:

《c++語言導學》——3.4 錯誤處理

https://yqfile.alicdn.com/adeeb52c46b6c8e012ae4ff32f94e197547784e1.png" >

這條語句很可能會引起混亂。

與原來的版本相比,下面的定義更好:

《c++語言導學》——3.4 錯誤處理

本書使用标準庫異常length_error報告元素數目為非正數的錯誤,因為一些标準庫操作也是這麼做的。如果new運算符找不到可配置設定的記憶體,就會抛出std::bad_alloc。我們可以接着書寫:

《c++語言導學》——3.4 錯誤處理

你可以自定義異常類,然後讓它們把任意資訊從檢測異常的點傳遞到處理異常的點(見3.4.1節)。

通常情況下,當遭遇異常問題之後函數就無法繼續完成工作了。此時,“處理”異常的含義僅僅是做一些簡單的局部資源清理,然後重新抛出異常。要想在異常處理子產品中抛出(重新抛出)異常,隻需書寫throw;例如:

《c++語言導學》——3.4 錯誤處理

不變式的概念是設計類的關鍵,而前置條件也在設計函數的過程中起到類似的作用。不變式能夠:

幫助我們準确地了解想要什麼;

強制我們具體而明确地描述設計,而這有助于確定代碼正确(在調試和測試之後)。

不變式的概念是c++中由構造函數(見第4章)和析構函數(見4.2.2節,11.2節)支撐的資源管理概念的基礎。

3.4.3 靜态斷言

程式異常負責報告運作時發生的錯誤。如果我們能在編譯時發現錯誤,顯然效果更好。這是大多數類型系統以及自定義類型接口說明的主要目的。不過,我們也能對其他一些編譯時可知的屬性做一些簡單檢查,并以編譯器錯誤消息的形式報告所發現的問題。例如:

《c++語言導學》——3.4 錯誤處理

如果4<=sizeof(int)不滿足,輸出資訊integers are too small。也就是說,如果目前系統一個int占有的空間不足4個位元組,就會報錯。我們把這種表達某種期望的語句稱為斷言(assertion)。

static_assert機制能用于任何可以表達為常量表達式(見1.7節)的東西。例如:

《c++語言導學》——3.4 錯誤處理

通常情況下,static_asser t(a,s)的作用是當a不為true時把s作為一條編譯器錯誤資訊輸出。

static_assert最重要的用途是為泛型程式設計中作為形參的類型設定斷言(見5.4節,11.6節)。

對于運作時檢查的斷言,使用異常。

繼續閱讀