【C++】《C++ Primer 》第十五章
阿新 • • 發佈:2020-08-09
第十五章 面向物件程式設計
一、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)
明確指出它是從哪個基類繼承而來。形式:冒號。後面緊跟以逗號分隔的基類列表,每個基類前面可以是public
、protected
、private
中的一個。 - 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
宣告語句不能指定explicit
或constexpr
。如果基類的建構函式是explicit
或constexpr
,則繼承的建構函式也擁有相同的屬性。 - 當一個基類建構函式含有預設實參時,這些實參並不會被繼承。
- 如果基類含有幾個建構函式,則除了兩個例外情況,大多數時候派生類會繼承所有這些建構函式。
- 第一個例外:派生類可以繼承一部分建構函式,而為其他建構函式定義自己的版本。
- 第二個例外:預設、拷貝和移動建構函式不會被繼承。
- 一個建構函式的
八、容器和繼承
- 當使用容器存放繼承體系中的物件時,通常必須採用間接儲存的方式。
- 當派生類物件被賦值給基類物件時,其中的派生類部分將被"切掉",因此容器和存在繼承關係的型別無法相容。
- 希望在容器中存放具有繼承關係的物件時,實際上存放的通常是基類的指標(更好的選擇是智慧指標)。
- 對於C++面向物件的程式設計來說,一個悖論是我們無法直接使用物件進行面向物件程式設計。相反,必須使用指標和引用。因為指標會增加程式的複雜性,所以經常定義一些輔助的類來處理這些複雜的情況。