1. 程式人生 > 實用技巧 >【C++】《C++ Primer 》第十五章

【C++】《C++ Primer 》第十五章

第十五章 面向物件程式設計

一、OOP:概述

  • 面向物件程式設計(OOP)的核心思想是資料抽象繼承動態繫結
    • 通過使用資料抽象,可以將類的介面和實現分離。
    • 使用繼承,可以定義相似的型別並對其相似關係建模。
    • 使用動態繫結,可以在一定程度上忽略相似型別的區別,而以統一的方式使用它們的物件。
  • 繼承(inheritance)
    • 通過繼承聯絡在一起的類構成一種層次關係。通常在層次關係的根部有一個基類(base class)
    • 其他類直接或者間接從基類繼承而來,這些繼承得到的類成為派生類(derived class)
    • 基類負責定義在層次關係中所有類共同擁有的成員,而每個派生類定義各自特有的成員。
    • 對於某些函式,基類希望它的派生類各自定義適合自身的版本,此時基類就將這些函式宣告為虛擬函式(virtual function)
class Quote {
public:
    std::string isbn() const;
    virtual double net_price(std::size_t n) const;
};

class BulkQuote : public Quote {
public:
    double net_price(std::size_t n) const override; // C++11
};
  • 動態繫結(dynamic binding),即執行時繫結
    • 使用同一段程式碼可以分別處理基類和派生類的物件。
    • 函式的允許版本由實參決定,即在執行時選擇函式的版本。
// 計算並列印銷售給定數量的某種書籍所得的費用
double print_total(ostream &os, const Quote &item, size_t n) {
    double ret = item.net_price();
    os << "ISBN: " << item.isbn() << " # sold: " << n << " total due: " << ret << endl;
    return ret;
}

print_total(cout, basic, 20);   // 呼叫Quote的net_price
print_total(cout, bluk, 20); // 呼叫BulkQuote的net_price

二、定義基類和派生類

1. 定義基類

  • 基類通常都應該定義一個虛解構函式,即使該函式不執行任何實際操作也是如此。
  • 基類通過在其成員函式的宣告語句之前加上關鍵字virtual使得該函式執行動態繫結。任何建構函式之外的非靜態函式都可以是虛擬函式
  • 關鍵字virtual只能出現在類內部的宣告語句之前而不能用於類外部的函式定義。如果基類把一個函式宣告為虛擬函式,則該函式在派生類隱式地也是隱函式。
  • 如果成員函式沒有被宣告為虛擬函式,則解析過程發生在編譯時而非執行時。
  • 訪問控制
    • protected: 基類和其派生類還有友元可以訪問
    • private: 只有基類本身和友元可以訪問

2. 定義派生類

  • 派生類必須通過派生列表(class derivation list)明確指出它是從哪個基類繼承而來。形式:冒號。後面緊跟以逗號分隔的基類列表,每個基類前面可以是publicprotectedprivate中的一個。
  • C++11允許派生類顯式地註明它將使用哪個成員函式改寫基類地虛擬函式,即在函式地形參列表之後加上一個override關鍵字。
  • 派生類鉤建構函式:派生類必須使用基類的建構函式去初始化它地基類部分。
  • 靜態成員:如果基類定義了一個靜態成員,則在整個繼承體系中只存在該成員的唯一定義。
  • 派生類的宣告:宣告中不包含它的派生列表。
  • C++11提供了一種防止繼承的方法,在類後面跟一個關鍵字final

3. 型別轉換和繼承

  • 理解基類和派生類之間的型別轉換是理解C++語言OOP的關鍵所在。
  • 可以將基類的指標或引用繫結到派生類物件上。基類的指標或引用的靜態型別可能與其動態型別不一致。
  • 不存在從基類向派生類的隱式型別轉換。
  • 派生類向基類的自動型別轉換隻對指標或引用型別有效,物件之間不存在型別轉換。
  • 當我們用一個派生類物件為一個基類物件初始化或賦值時,只有該派生類物件中的基類部分會被拷貝、移動或者賦值,它的派生類部分將被忽略掉。

三、虛擬函式

  • 使用虛擬函式可以執行動態繫結,但是動態繫結只有當我們通過指標或引用呼叫虛擬函式時才會發生,因為只有這種情況下物件的動態型別才有可能與靜態型別不同。
  • OOP的核心思想是多型性(polymorphism)。把具有繼承關係的多個型別稱為多型型別。引用或指標的靜態型別與動態型別不同這一事實正是C++語言支援多型性的根本所在。
  • 基類中的虛擬函式在派生類中隱含地也是一個虛擬函式。當派生類覆蓋了某個虛擬函式時,該函式在基類中的形參必須與派生類中的形參嚴格匹配。
  • C++11允許派生類顯式地註明它將使用哪個成員函式改寫基類的虛擬函式,即在函式的形參列表之後加一個override關鍵字。
  • 如果我們想覆蓋某個虛擬函式,但不小心把形參列表弄錯了,這個時候就不會覆蓋基類中的虛擬函式。加上override可以明確程式設計師的意圖,讓編譯器幫忙確認引數列表是否有出錯。
  • 如果虛擬函式使用預設實參,則基類和派生類中定義的預設實參最好一致。
  • 某些情況下,如果希望對虛擬函式的呼叫不要進行動態繫結,而是強迫其執行虛擬函式的某個特定版本。使用作用域運算子可以實現這一目的。
  • 通常情況下,只有成員函式(或友元)中的程式碼才需要使用作用域運算子來回避虛擬函式的機制。使用場景:當一個派生類的虛擬函式呼叫它覆蓋的基類的虛擬函式版本時。

四、抽象基類

  • 純虛擬函式(pure virtual):清晰地告訴使用者當前地函式是沒有實際意義的。純虛擬函式無需定義,只用在函式體的位置前書寫 =0 就可以將一個虛擬函式說明為純虛擬函式。
  • 含有純虛擬函式的類就是抽象基類(abstract base class)。它不能建立抽象基類的物件。

五、訪問控制與繼承

  • 受保護的成員
    • protected說明符可以看做是public和private中和後的產物。
    • 和私有成員類似,受保護的成員對於類的使用者來說是不可訪問的。
    • 和公有成員類似,受保護的成員對於派生類和友元來說是可訪問的。
    • 派生類的成員和友元只能通過派生類物件來訪問基類的受保護成員。派生類對於一個基類物件中的受保護成員沒有任何訪問特權。
class Base {
protected:
    int prot_mem;   // 
};

class Sneaky: public: Base {
    friend void clobber(SneaKy&);   // 能訪問Sneaky::port_mem
    friend void clobber(Base&); // 不能訪問Base::port_mem
    int j;
};

// 正確:clobber能訪問Sneaky物件的private和protected成員
void clobber(Sneaky &s) { s.j = s.prot_mem = 0; }

// 錯誤:clobber不能訪問Base的protected成員
void clobber(Base &b) { b.prot_mem = 0;}
  • 某個類對其繼承而來的成員的訪問許可權受到兩個因素影響:在基類中該成員的訪問說明符;在派生類的派生列表中的訪問說明符;
  • 派生訪問說明符
    • 對於派生類的成員(及其友元)能否訪問其直接基類的成員沒有說明影響。
    • 它的目的是控制派生類使用者對於基類成員的訪問許可權。比如struct Priv_Drev: private Base{}意味著在派生類Priv_Drev中,從Base繼承而來的部分都是private的。
  • 友元關係不能繼承。每個類負責控制各自成員的訪問許可權。
  • 改變個別成員的可訪問性:使用using
  • 區別:預設情況下,使用class關鍵字定義的派生類是私有繼承的;使用struct關鍵字定義的派生類是公有繼承的。

六、繼承中的類作用域

  • 每個類定義自己的作用域,在這個作用域內我們使用定義類的成員。當存在繼承關係時,派生類的作用域巢狀在其基類的作用域之內。
  • 派生類的成員將隱藏同名的基類成員。
  • 除了覆蓋繼承而來的虛擬函式之外,派生類最好不要重用其他定義在基類中的名字。

七、建構函式和拷貝函式

1. 虛解構函式

  • 基類通常應該定義一個虛解構函式,這樣就能動態分配繼承體系中的物件了。
  • 如果基類的解構函式不是虛擬函式,則delete一個指向派生類物件的基類指標將產生未定義的行為。
  • 一個例外:通常如果一個類需要解構函式,那麼它同樣需要拷貝和賦值操作。但是基類的解構函式是一個例外,應該該解構函式為了成為虛擬函式而令其內容為空,顯然無法推斷該基類還需要賦值運算子或拷貝建構函式。
  • 虛解構函式將阻止合成移動操作。

2. 合成拷貝控制與繼承

  • 基類或派生類的合成拷貝控制成員的行為和其他合成的建構函式、賦值運算子或解構函式類似;它們對類本身的成員依次進行初始化、賦值或銷燬的操作。

3. 派生類的拷貝控制成員

  • 當派生類定義了拷貝或移動操作時,該操作負責拷貝或移動包括基類部分成員在內的整個物件。
  • 派生類解構函式:派生類解構函式先執行,然後執行基類的解構函式。

4. 繼承的建構函式

  • C++11中,派生類可以重用其直接基類定義的建構函式。
  • 語法:
class A: public B {
public:
    using A::A; // 繼承B的建構函式
    double get(int) const;
};
  • 繼承的建構函式的特點:
    • 一個建構函式的using宣告不會改變該建構函式的訪問級別。
    • 一個using宣告語句不能指定explicitconstexpr。如果基類的建構函式是explicitconstexpr,則繼承的建構函式也擁有相同的屬性。
    • 當一個基類建構函式含有預設實參時,這些實參並不會被繼承。
    • 如果基類含有幾個建構函式,則除了兩個例外情況,大多數時候派生類會繼承所有這些建構函式。
      • 第一個例外:派生類可以繼承一部分建構函式,而為其他建構函式定義自己的版本。
      • 第二個例外:預設、拷貝和移動建構函式不會被繼承。

八、容器和繼承

  • 當使用容器存放繼承體系中的物件時,通常必須採用間接儲存的方式。
  • 當派生類物件被賦值給基類物件時,其中的派生類部分將被"切掉",因此容器和存在繼承關係的型別無法相容。
  • 希望在容器中存放具有繼承關係的物件時,實際上存放的通常是基類的指標(更好的選擇是智慧指標)。
  • 對於C++面向物件的程式設計來說,一個悖論是我們無法直接使用物件進行面向物件程式設計。相反,必須使用指標和引用。因為指標會增加程式的複雜性,所以經常定義一些輔助的類來處理這些複雜的情況。

九、文字查詢程式再探