OOP 概述
面向對象程序設計的核心思想是數據抽象、繼承和動態綁定。
- 數據抽象:將類的接口與實現分類。
- 繼承:可以定義相似的類型並對其相似關系建模。
- 動態綁定:可以在一定程序上忽略相似類型的區別,而以統一的形式使用它們的對象。
繼承
通過繼承聯系在一起的類構成一種層次關系,通常在層次關系的根部有一個 基類,其他類則直接或間接的從基類繼承而來,這些繼承得到的類稱為 派生類。基類負責定義在層次關系中所有類共同擁有的成員,而每個派生類定義各自特有的成員。
派生類必須通過使用 類派生列表 明確指出從哪個或者哪些基類繼承而來,類派生列表的形式是:冒號,后面緊跟以逗號分隔的基類列表,其中每個基類前面都可以有訪問說明符。
如果基類希望他的派生類各自定義適合自身版本的函數,此時基類就將這些函數聲明成虛函數。C++ 11 新標准允許派生類顯示地注明它將使用哪個成員函數改寫基類的虛函數,具體措施是在函數的形參列表之后再加一個 override 關鍵字。
動態綁定
在 C++ 語言中,使用基類的引用或者指針調用一個虛函數時將發生動態綁定。
定義基類和派生類
定義基類
class Quote{
public:
Quote() = default;
Quote(const std::string &book,double sales_price):
bookNo(book),price(sales_price){}
std::string isbn() const{return bookNo;}
virtual double net_price(std::size_t n) const
{return n * price;}
virtual ~Quote() = default; //對析構函數進行動態綁定
private:
std::string bookNo; //書籍的 ISBN 編號
protected:
double price = 0.0; //表示普通狀態下不打折的價格
};
注意:
基類通常都應該定義一個虛析構函數,即使該函數不執行任何實際的操作也是如此。
成員函數與繼承
派生類可以繼承其基類的成員,但是針對特定的操作,派生類可以提供自己新的定義,以覆蓋從基類繼承而來的舊定義。
C++ 中必須將它的兩種成員函數區分開來:
- 一種是基類希望其派生類進行覆蓋的函數。
- 一種是基類希望派生類直接繼承而不要改變的函數。
對於第一種函數,基類通常將其定義為虛函數,當使用指針或者引用調用虛函數時,該調用將動態綁定,根據引用或指針所綁定的對象類型不同,該調用可能執行基類的版本,也可能執行某個派生類的版本。
基類通過在成員函數前面加上 virtual 函數使得該函數執行動態綁定。
- 任何構造函數之外的非靜態函數都可以是虛函數。
- 關鍵字
virtual只能出現在類內部聲明語句之前,不能用於類外部的函數定義。 - 如果基類把一個函數寫成虛函數,則該函數在派生類中隱式地也是虛函數。
成員函數如果沒有被聲明為虛函數,則其解析過程發生在編譯時期而非運行時。
訪問控制與繼承
派生類可以繼承定義在基類中的成員,但是派生類的成員函數不一定有權訪問從基類繼承而來的成員。派生類可以訪問公有成員,而不能訪問私有成員,protected 成員是基類希望它的派生類能夠訪問該成員,但是同時禁止其他用戶訪問。
定義派生類
派生類需要使用類派生列表明確指出它是從哪個或哪些基類繼承而來的,類派生列表的形式是:冒號后面緊跟逗號分隔的基類列表,每個基類前面都要指明訪問說明符:public,protected,private。
class Bulk_quote : public Quote
{
public:
Bulk_quote() = default;
Bulk_quote(const std::string&,double,std::size_t,double);
double net_price(std::size_t) const override;
private:
std::size_t min_qty = 0;
double discount = 0;
};
繼承自一個類的形式稱為單繼承。
派生類中的虛函數
派生類經常但不總是覆蓋它繼承的虛函數,如果派生類沒有覆蓋其基類中的某個虛函數,則該虛函數的行為類似於其他的普通成員,派生類會直接繼承其在基類中的版本。
派生類對象及派生類向基類的類型轉換
一個派生類對象包含多個組成部分:含有派生類自定義的對象,繼承自基類的對象。
C++ 中沒有明確規定派生類的對象在內存中如何分布,但是可以 認為 Bulk_quote 對象包含如下兩個部分:

因為在派生類對象中含有與其基類對應的組成部分,所以能把派生類的對象當成基類對象使用,而且能將基類的指針或引用綁定到派生類對象中的基類部分:
Quote item; // 基類對象
Bulk_quote bulk; // 派生類對象
Quote *p = &item; // p指向基類 Quote 對象
p = &bulk; // p指向派生類 Bulk_quote 對象的基類部分
Quote &r = bulk; // r綁定到派生類 Bulk_quote 對象的基類部分
上面介紹的轉換通常稱為 派生類到基類的類型轉換。這種類型轉換是編譯器隱式執行的。
派生類構造函數
盡管派生類包含了從基類繼承而來的成員,但是派生類不能直接初始化這些成員,而是需要使用基類的構造函數來初始化它們。也就是說:每個類控制它自己的成員初始化過程。
派生類對象的基類部分與派生類對象自己的數據成員都是在構造函數的初始化階段執行初始化操作的:
Bulk_quote(const std::string& book,double p,std::size_t qty,double disc) : Quote(book,p),min_qty(qty),discount(disc){}
除非指出,否則派生類對象的基類部分數據成員會執行默認初始化。
另外,首先會初始化基類的部分,然后按照聲明的順序依次初始化派生類的成員。
派生類使用基類的成員
派生類可以訪問其基類中的公有成員和受保護的成員。
繼承與靜態成員
如果基類定義了一個靜態成員,則在整個繼承體系中只存在該成員的唯一定義,無論從基類中派生出來多少個派生類,對於每個靜態成員來說都只存在唯一的實例。
靜態成員遵循通用的訪問控制規則。如果某個靜態成員是可以訪問的,那么既可以通過基類也可以通過派生類使用它。
派生類的聲明
派生類的聲明包含類名但是不包含它的派生列表:
class Bulk_quote : public Quote; // 錯誤,派生類列表不能出現在這里
class Bulk_quote; // 正確,聲明派生類的正確方式
被用作基類的類
如果想使用某個類作為基類,則該類必須是已經定義而非僅僅聲明:
class Quote; // 聲明,但未定義
class Bulk_quote : public Quote {...}; // 錯誤,Quote必須先被定義
派生類中包含並且可以使用它從基類繼承而來的成員,為了使用這些成員,派生類當然要先知道它們是什么,因此規定還有一層隱含的意思,即一個類不能派生它本身。
一個類是基類,同時也可以是派生類:
class Base{/*...*/};
calss D1 : public Base {/*...*/};
class D2 : public D1 {/*...*/};
Base 是 D1 的直接基類,同時是 D2 的間接基類。直接基類出現在派生列表中,而間接基類由派生類通過其直接基類繼承而來。
防止繼承的發生
如果想定義一個類並且不希望從它派生出新的類,可以禁止繼承的方式,C++ 11 新標准中在類名后面緊跟一個關鍵字 final 即可實現:
class NoDerived final{/*...*/}; // NoDerived 不能作為基類
類型轉換與繼承
通常情況下引用或指針綁定到一個對象時,引用或指針的類型應與對象的類型一致,或者對象的類型含有一個可接受的 const 類型轉換規則。存在繼承關系的類是一個重要的例外:可以將基類的指針或引用綁定到派生類的對象上。
可以將基類的指針或引用綁定到派生類對象上是一層極為重要的含義:當使用基類的引用或指針時,實際上我們並不清楚該引用或指針所綁定對象的真實類型。該對象可能是基類的對象,也可能是派生類的對象。
智能指針也支持派生類向基類的類型轉換。
靜態類型與動態類型
當使用存在繼承關系的類型時,必須將一個變量或其他表達式的靜態類型與該表達式表示對象的動態類型區分開來,表達式的靜態類型在編譯時總是已知的,它是變量聲明時的類型或表達式生成的類型。動態類型則是變量或表達式表示的內存中的對象類型,動態類型直到運行時才可以知道。
如果表達式既不是引用也不是指針,則它的動態類型永遠與靜態一致。
不存在從基類向派生類的隱式轉換類型
之所以存在派生類向基類的類型轉換是因為每個派生類對象都包含一個基類部分,基類的引用或指針可以綁定到這部分上。
因為一個基類的對象可能是派生類對象的一部分,也可能不是,所以不存在從基類向派生類的自動類型轉換:
Quote base;
Bulk_quote bulkP = &base; // 錯誤,不能將基類轉換成派生類
Bulk_quote& bulkRef = base; // 錯誤,不能將基類轉換成派生類
如果上述轉換合法,則我們有可能會使用 bulkP 或 bulkRef 來訪問 base 中根本不存在的數據成員。
除此之外還有一種情況顯得有點特別,即使一個基類指針或引用綁定在一個派生類對象上,也不能執行從基類向派生類的轉換:
Bulk_quote bulk;
Quote *itemP = &bulk; //正確,動態類型是 Bulk_quote
Bulk_quote *bulkP = itemP; //錯誤,不能將基類轉換成派生類
如果在基類中含有一個或多個虛函數,可以使用 dynamic_cast 來請求一個類型轉換,該轉換的安全檢查將在運行時執行,同樣。如果已知某個基類向派生類轉換是安全的,則可以使用 static_cast 來強制覆蓋掉編譯器的檢查工作。
對象之間不存在類型轉換
派生類向基類的自動類型轉換只針對指針或引用類型有效,在派生類類型和基類類型之間不存在裝的。
