1. 程式人生 > >c++ primer第五版----學習筆記(十八)Ⅰ

c++ primer第五版----學習筆記(十八)Ⅰ

用於大型程式的工具異常處理、名稱空間和多重繼承

特殊要求:

  1. 在獨立開發的子系統之間協同處理錯誤的能力
  2. 使用各種庫(可能包含獨立開發的庫)進行協同開發的能力
  3. 對比較複雜的應用概念建模的能力

1.異常處理

  • 異常處理機制允許程式中獨立開發的部分能夠在執行時就出現的問題進行通訊並作出相應的處理
  • 異常使得我們能夠將問題的檢測與解決過程分離開來

1.1 丟擲異常

  1. 通過丟擲(throw)一條表示式來引發一個異常,被丟擲的表示式的型別以及當前的呼叫鏈共同決定了哪段處理程式碼將被用來處理該異常
  2. 執行throw時,跟在throw後面的語句將不再被執行,程式控制權由throw轉移到與之匹配的catch
    模組
  3. 當throw出現在一個try語句塊內時,檢查與try關聯的catch子句,如果找到匹配的catch,就用該catch處理異常;如果該步沒找到,則找外層try匹配的catch子句,如果還是找不到,則到外層函式中查詢;上述過程稱為棧展開
  4. 一個異常如果沒有被捕獲,則程式將呼叫標準庫函式terminate終止當前程式

     解構函式與異常

  1. 如果在棧展開過程中退出了某個塊,則編譯器負責確保構造或初始化的元素被正確銷燬(類則採用解構函式進行析構)
  2. 一旦在棧展開的過程中解構函式丟擲了異常,並且解構函式自身沒能捕獲到該異常,則程式將被終止
  3. 如果解構函式需要執行某個可能丟擲異常的操作,則該操作應該被放置在一個try語句塊中,並且在解構函式內部得到處理

 


     異常物件

  1. 一種特殊物件,編譯器使用異常丟擲表示式來對異常物件進行拷貝初始化
  2. 丟擲指標要求在任何對應的處理程式碼存在的地方,指標所指的物件必須存在(不能返回區域性物件的指標
  3. 當我們丟擲一條表示式時,該表示式的靜態編譯型別決定了異常物件的型別(即不發生動態繫結

 1.2 捕獲異常

  1. catch子句的異常宣告的型別決定了所能捕獲的異常型別(該型別必須是完全型別,可以是左值引用,但不能是右值引用)
  2. 如果catch的引數型別是非引用型別,則該引數是異常物件的副本;如果是引用,則該引數是異常物件的一個別名
  3. 如果catch的引數是基類型別,則我們可以使用其派生類型別的異常物件對其進行初始化(catch無法使用派生類特有的任何成員);如果catch的引數是非引用型別,則異常物件將被切掉一部分
  4. 如果catch接受的異常與某個繼承體系有關,則最好將catch的引數定義成引用型別

查詢匹配的處理程式碼

  1. 在搜尋catch時,首先挑選出來的應該是第一個與異常匹配的catch語句,越是特例化的catch越應該置於整個catch列表的前端(catch語句按照其出現的順序逐一進行匹配的)
  2. 如果多個catch語句的型別之間存在著繼承關係,則我們應該把繼承鏈最底端的類放在前面,而將繼承鏈最頂端的類放在前面

重新丟擲

  1. 一條catch語句通過重新丟擲的操作將異常傳遞給另外一個catch語句(重新丟擲的語句throw;)
  2. 如果在處理程式碼之外的區域遇到了空throw語句,編譯器將呼叫terminate

捕捉所有異常

catch(...) {
    //處理異常的某些特殊操作
    throw;
}

 

  • 要想處理建構函式初始值丟擲的異常,必須將建構函式寫成函式try語句塊的形式:
//例
template <typename T>
Blob<T>::Blob(std::initializer_list<T> il) try : 
    data(std::make_shared<std::vector<T>>(il)) {
    /*...*/
} catch(xonst std::bad_alloc &e)  { handle_out_of_memory(e); }

1.3 noexcept異常說明

  1. 提供noexcept說明指定某個函式不會丟擲異常,關鍵字noexcept緊跟在函式的引數列表後面
void recoup(int) noexcept;  //不會丟擲異常
void alloc(int);    //可能丟擲異常

違反異常宣告

  1. 一個noexcept函式丟擲了異常,程式就會呼叫terminate以確保遵守不在執行時丟擲異常的承諾
  2. noexcept可以用在兩種情況:一種是確認函式不會丟擲異常,二是我們根本不知道該如何處理異常

異常說明的實參/\noexcept運算子

  1. noexcept說明符接受一個可選的實參,如果該實參是true則函式不會丟擲異常;否則可能丟擲異常
void recoup(int) noexcept(true);     //不會丟擲異常
void alloc(int) noexcept(false);       //可能丟擲異常

     2.noexcept運算子:

noexcept(e);       //當e不丟擲異常時為true

異常說明與指標、虛擬函式和拷貝控制

  1. 函式指標及該指標所指函式必須具有相同的異常宣告
  2. 若隱式或顯示說明某個指標可能丟擲異常,則該指標可以指向任何函式,即使是承諾了不丟擲異常的函式也可以
  3. 如果虛擬函式承若了它不會丟擲異常,則後續派生出來的虛擬函式也必須做出同樣的承諾;如果基類的虛擬函式允許丟擲異常,則派生類的對應函式既可以允許丟擲異常,也可以不允許丟擲異常
  •  使用自定義異常類的方式與使用標準異常類的方式一樣,一處丟擲在另一處捕獲並處理

2.名稱空間

2.1 定義名稱空間

  • 多個庫將名字放置在全域性名稱空間中將引發名稱空間汙染;名稱空間為防止名字衝突提供了更加可控的機制
  • ~名稱空間由關鍵字namespace+名字構成;~能出現在全域性作用域中的宣告就能置於名稱空間內;~名稱空間作用域後面無須分號
//例
namespace cplusplus_primer {
    class Sales_data { /*...*/};
    Sales_data operator+(const Sales_data&, const Sales_data&);
    class Query  {/*...*/};
    class Query_base {/*...*/};
}
  • 定義在名稱空間內部的名字可以被該名稱空間內的其他成員直接訪問,位於名稱空間外的程式碼必須明確指出所用的屬於哪個名稱空間:
cplusplus_primer::Query q = cplusplus_primer::Query("hello");
  • 名稱空間可以不連續:使得可以將幾個獨立的介面和實現檔案組成一個名稱空間
  • 模板特例化必須定義在原始模板所屬的名稱空間中,如果在名稱空間中聲明瞭特例化,就能在外部定義它了:
//例
namespace std {
    template <> struct hash<Sales_data>;  //在模板原始空間宣告特例化
}

template <> struct std::hash<Sales_data>
{
    /*...*/
}

 

全域性名稱空間

  • 全域性名稱空間以隱式的方式宣告,並且在所有程式中都存在:
::member_name;     //訪問全域性作用域成員

巢狀的名稱空間

  • 定義在其他名稱空間中的名稱空間
  • 訪問其中的成員需加上多層限定符

內聯的名稱空間

  • 在關鍵字namespace前新增關鍵字inline
  • 內聯名稱空間中的名字可以被外層名稱空間直接使用

未命名的名稱空間

  • 關鍵字namespace+{}
namespace {
    int i;
}
  • 未命名的名稱空間中定義的變數擁有靜態生命週期,即第一次使用前建立,直到程式結束才銷燬
  • 定義在未命名的名稱空間中的名字可以直接使用
  • 未命名的名稱空間定義在最外層作用域中,其中的名字要與全域性作用域中的名字有所區別

2.2 使用名稱空間成員:

  • 為名稱空間設定一個短的別名: 
namespace cplusplus_primer {/*...*/}
namespace primer = cplusplus_primer;   //primer是當前名稱空間的別名

using宣告/using指示

  1. 一條using宣告語句一次只引入名稱空間的一個成員,有效範圍從using宣告的地方開始,一直using宣告所在的作用域結束為止
  2. using指示以關鍵字using開始,後面是關鍵字namespace以及名稱空間的名字,將名稱空間的所有成員引入
namespace blip {
    int ival;
    double dval;
}

void manip()
{
    /*...*/
    using blip::dval;         //using宣告
    using namespace blip;     //using指示
}

 

2.3 類、名稱空間與作用域

  1.  對名稱空間內部名字的查詢遵循常規的查詢規則:由內向外依次查詢每個外層作用域
  2. 可以從函式的限定名推斷出查詢名字時檢查作用域的次序,限定名以相反次序指出被查詢的作用域
  3. 當我們給函式傳遞一個類型別的物件時,除了常規的作用域查詢還會查詢實參類所屬的名稱空間

  • 標準庫move和forward函式,都接受一個右值引用的函式形參,該形參可以匹配任何型別,容易與其他同名的函式產生名字衝突
  • 一個另外的未宣告的類或函式如果第一次出現在友元宣告中,則我們認為它時最近的外層名稱空間的成員 

 


 

2.4 過載與名稱空間

  1.  using宣告與using指示能將某些函式新增到候選函式集(函式匹配過程)
  2. using宣告引入的函式將過載該宣告語句所屬作用域中已有的其他同名函式;如果using宣告出現在區域性作用域中,則引入的名字將隱藏外層作用域的相關宣告
  3. using指示將名稱空間的成員提升到外層作用域中,如果名稱空間的某個函式與該名稱空間所屬作用域的函式同名,則名稱空間的函式將被新增到過載集合中:
namespace libs_R_us {
    ertern void print(int);
    ertern void print(double);
}

void print(const string &);
using namespace libs_R_us; 
//print呼叫此時的候選函式包括:
//libs_R_us的print(int)
//libs_R_us的print(double)
//顯示宣告的print(const string &)
void fooBar(int ival)
{
    print("Value:");
    print(ival);
}

 

3.多繼承與虛繼承

3.1多重繼承

  •  多重繼承,顧名思義,是指從多個直接基類中產生派生類的能力
  • 每個基類包含一個可選的訪問說明符,如果訪問說明符被忽略掉了,則關鍵字class對應的預設訪問說明符是private,關鍵字struct對應的是public
class CADVehicle : public CAD, Vehicle {...} //公有繼承CAD,私有繼承Vehicle
  • ~~構造一個派生類的物件將同時構造並初始化它的所有基類子物件,~~多重派生的派生類的建構函式初始值也只能初始化它的直接基類,~~如不是顯示初始化,則會使用基類的預設建構函式進行初始化
  • 如果一個類從它的多個基類中繼承了相同的建構函式,則這個類必須為該建構函式定義它自己的版本
  • 派生類的解構函式只負責清除派生類本身分配的資源,派生類的成員及基類都是自動銷燬的

型別轉換與多個基類

  • 在只有一個基類的情況下,派生類的指標或引用能自動轉換成一個可訪問基類的指標或引用
  • 可以令某個可訪問基類的指標或引用直接指向一個派生類物件,但該指標只能訪問其對應的基類部分或者基類的基類部分

多重繼承下的類作用域

  • 當一個類擁有多個基類時,有可能出現派生類從兩個或更多基類中繼承了同名成員的情況。此時,不加字首限定符直接使用該名字將引發二義性
  • 要想避免潛在的二義性,最好的方法是在派生類中為該函式定義一個新版本

 3.2虛繼承

  • 通過虛繼承解決派生類中包含多個間接基類的子物件問題
  • 共享的基類子物件稱為虛基類,定義虛基類的方式是在派生列表中新增關鍵字virtual
  • 虛派生隻影響從指定了虛基類的派生類中進一步派生出來的類,它不會影響派生類本身
  • 不管基類是不是虛基類,派生類物件都能被可訪問基類的指標或引用操作
  • 成員被多於一個基類覆蓋,需進行分類討論

建構函式和虛繼承

  • 在虛派生中,虛基類是由最低層的派生類初始化的,否則虛基類將會在多條繼承路徑上被重複初始化
  • 含有虛基類的物件的構造順序與一般的順序有區別:先使用提供給最低層派生類建構函式的初始值初始化該物件的虛基類子部份,接下來按照直接基類在派生列表中出現的次序對其進行初始化
  • 物件的銷燬順序與構造順序相反