C++ 學習筆記之(18)-大型工程工具(異常處理、命名空間和多重繼承與虛繼承)
異常處理
異常處理(exception handling)機制能夠對程序在運行時就出現的問題進行通信並作出相應的處理。
拋出異常
C++語言通過 拋出(throwing)表達式來 引發(raised)異常。throw
後,程序控制權交給對應的catch
模塊,即throw
後的語句將不再被執行。
- 棧展開(stack unwinding):沿着嵌套函數的調用鏈不斷查找,直到找到與異常匹配的
catch
子句繼續執行。或者最終沒有找到,調用標準庫函數terminate
, 終止程序的執行過程 - 析構函數中,對於某個可能拋出異常的操作,應放置在
try
語句塊中,並在析構函數內部處理。否則可能無法正確釋放資源 - 異常對象(exception object):特殊的對象,編譯器使用異常拋出表達式對異常對象進行拷貝初始化
- 若表達式是類類型,則類必須含有可一個可訪問的析構函數和一個可訪問的拷貝或移動構造函數
- 若表達式是數組類型或函數類型,則表達式將被轉換成與之對應的指針類型
- 異常對象位於編譯器管理的空間中,處理完畢後,異常對象被銷燬
- 拋出指針要求在任何對應的處理代碼存在的地方,指針所指的對象都必須存在
捕獲異常
- 通常,如果
catch
接受的異常與某個繼承體系有關,則最好將該catch
的參數定義成引用類型 - 異常的類型與
catch
聲明的類型是精確匹配的
- 允許從非常量向常量的類型轉換
- 允許從派生類向基類的類型轉換
- 數組/函數被轉換成指向數組(元素)/函數類型的指針
- 包括標準算數類型轉換和類類型轉換在內的其他所有轉換不能在匹配
catch
的過程中使用
- 重新拋出(rethrowing):將異常傳遞給另一個
catch
語句,只需throw
語句,不包含表達式 - 捕獲所有異常(catch-all):形如
catch(...)
, 若catch(...)
與其他catch
語句一起出現,則catch()
必須在最後的位置。否則出現在捕獲所有異常語句後面的catch
語句將永遠不會被匹配
函數try
語句塊與構造函數
若想處理構造函數初始值拋出的異常,必須將構造函數寫成 函數
try
語句塊也稱爲 函數測試塊(function try block)的形式初始化構造函數參數時也可能發生異常,但這種情況不屬於函數
try
語句塊template <typename T> Blob<T>::Blob(std::initializer_list<T> il) try: data(std::mnake_shared<std::vector<T>>(il)) {} catch(const std::bad_alloc &e) { handle_out_of_memory(e); }
noexcept
異常說明
若預先知道函數不會拋出異常有助於簡化調用該函數的代碼,且編譯器可執行某些特殊的優化操作
C++11新標準,可通過提供noexcept說明(noexcept specification)指定某個函數不會拋出異常。其形式是關鍵字
noexcept
緊跟在函數參數列表後若函數說明了
noexcept
的同事又含有throw
語句或者調用了可能拋出異常的其它函數,編譯器可能會通過。一旦拋出異常,程序會調用terminate
以確保遵守不在運行時拋出異常的承諾noexcept
說明符接受一個可選的實參,該市餐必須能轉換爲bool
類型noexcept運算符:一元運算符,返回一個
bool
類型的右值常量表達式,表示給定的表達式是否會拋出異常void recoup(int) noexcept; // 不會拋出異常 void recoup(int) throw(); // 等價聲明 void recoup(int) noexcept(true); // recoup 不會拋出異常 void alloc(int) noexcept(false); // alloc 可能拋出異常 noexcept(recoup(i)); // 如果 recoup 不拋出異常則結果爲 true, 否則爲 false noexcept(e); // 判斷 e 調用的所有函數是否都做了不拋出異常說明且 e 本身不含 throw 語句時
函數指針與該指針所指的函數必須具有一致的異常說明。若函數指針聲明不拋出異常,則只能指向不拋出異常的函數;若函數指針可能拋出異常,則可以指向任何函數
void (*pf1)(int) noexcept = recoup; // recoup 和 pf1 都不會拋出異常 void (*pf2)(int) = recoup; // 正確:recoup 不會拋出異常, pf2 可能拋出異常,二者互不干擾 pf1 = alloc; // 錯誤:alloc 可能拋出異常,但是 pf1 已聲明不會拋出異常 pf2 = alloc; // 正確:pf2 和 alloc 都可能拋出異常
若虛函數聲明不拋出異常,則派生類的虛函數必須聲明
異常類層次
標準庫異常類的繼承體系如下
- 運行時錯誤:程序運行時才能檢測到的錯誤
- 邏輯錯誤:程序代碼中發現的錯誤
命名空間
命名空間污染(namespace pollution)由多個庫將名字放置在全局命名空間中引發。 命名空間(namespace)爲防止名字衝突提供了可控機制,分割了全局命名空間,其中每個命名空間是一個作用域。
命名空間定義
命名空間的名字在定義它的作用域中需保持唯一,命名空間可定義在全局作用域內,也可定義在其他命名空間中,但不能定義在函數或類的內部
每個命名空間都是一個作用域, 且命名空間可以不連續,
全局命名空間(global namespace):全局作用域中定義的名字(即在所有類、函數及命名空間之外定義的名字)定義在全局命名空間,隱式聲明,且在所有程序都存在,
::member_name
命名空間可嵌套,內層命名空間作用域中隱藏外層命名空間同名成員
內聯命名空間(inline namespace):C++11定義,成員可被外層命名空間直接使用
namespace A{ namespace B { int x; } inline namespace C { int y; } } A::B::x; A::j; // 可直接使用內聯空間成員
未命名的命名空間:關鍵字
namespace
後緊跟花括號,其內變量擁有靜態生命週期,第一次使用前創建,程序結束時銷燬,僅可在文件內不連續,不能跨越多個文件靜態聲明
static
已被取消,現在的做法是使用未命名的命名空間
使用命名空間成員
命名空間別名
namespace primer = cplusplus_primer
using聲明(using declaration)
:一次只引入命名空間的一個成員,using std::endl;
using指示(using directive)
:無法控制那些名字課件,因爲都可見using namespace std;
對於命名空間中名字的隱藏規則有個例外:即給函數傳遞一個類類型的對象時,除了在常規作用域查找,還會查找實參類所屬的命名空間
std::string s; std::cin >> s; operator>>(std::cin, s); // 等價於上式。由於`形參爲類類型,故對 operator>> 的查找會包括 cin 和類所屬的命名空間,即查找定義了 istream 和 string 的命名空間 std
對標準庫模板函數
std::move
和std::forward
來說,儘量書寫完整
重載與命名空間
using
聲明引入的是名字,而非特定函數,即如果爲函數書寫using
聲明,會將函數的所有版本引入。若using
聲明作用域已出現同名函數,則會重載;若出現在局部作用域,則會隱藏外層作用域相關聲明;若所在作用域有同名且形參列表相同的函數,則引發錯誤
多重繼承與虛繼承
多重繼承(multiple inheritance):指從多個直接基類中產生派生類的能力。派生類繼承了所有父類的屬性
多重繼承
- 構造派生類對象會同事構造並初始化所有基類子對象,且順序與派生列表中基類的出現順序一致
- C++11允許派生類從一個或多個基類中繼承構造函數,但若構造函數相同,則錯誤。需要自定義
- 多個基類的情況下,任意一種基類的指針或引用都可直接指向一個派生類對象
- 單基類繼承,派生類作用域嵌套在直接基類和間接基類的作用域下。而多重繼承,有可能出現派生類從兩個或多個基類中繼承同名成員的情況,此時不加前綴限定符直接使用該名字會引發二義性
虛繼承
派生類會多次或直接或間接地繼承同一基類,會導致基類的多份拷貝。故通過 虛繼承(virtual inheritance)的機制解決,共享的基類子對象稱爲 虛基類(virtual base class)。 在這種機制下,不論虛基類在集成體系出現多少次,派生類中只會包含唯一一個共享的虛基類子對象
// 關鍵字 public 和 virual 順序隨意, 下列代碼將 A 定義爲 B 和 C 的虛基類
class B: public virtual A { /* ... */ };
class C: public virtual A { /* ... */ };
- 虛基類總是先於非虛基類構造,與它們在繼承體系中的次序和位置無關,其後再按聲明順序逐一構造其他非虛基類
結語
C++的異常處理、命名空間以及多重繼承或虛繼承適合處理大規模問題。
- 異常處理將程序的錯誤檢測部分與錯誤處理部分分割開
- 命名空間用啦管理大規模複雜應用程序,一個命名空間是一個作用域,可在其中定義對象、類型、函數、模板以及其他命名空間。標準庫定義在名爲
std
的命名空間中 - 多重繼承即一個派生類可從多個直接基類繼承而來。派生類對象中既包含派生類部分,也包含每個基類的基類部分,可能會引入新的名字衝突並造成來自於基類部分的二義性問題
- 使用虛繼承,可使繼承同一基類的多個類共享虛基類,派生類中只會有一個共享虛基類的副本