文章目錄
OOP 概述
很多程序中都存在着一些相互關聯但有細微差別的概念。例如,書店中不同書籍的定價策略可能不同:有的書按照原價銷售,而有的打折;又或是,當顧客購買的書超過一定數量時打折;又或者只對前多少本書打折。面向對象程序設計適用於這類應用。
面向對象程序設計(object-oriented programming) 的核心思想是數據抽象、繼承和動態綁定。通過使用數據抽象,我們可以將類的接口與實現分離;使用繼承,可以定義相似的類型並對其相似的關係建模;使用動態綁定,可以在一定程度上忽略相似類型的區別,而以統一的方式使用它們的對象。
繼承
通過繼承聯繫在一起的類構成一種層次關係。通常在層次關係的根部有一個基類,其他類則直接或間接地從基類繼承而來,這些繼承得到的類稱爲派生類。基類負責定義在層次關係中所有類共同擁有的成員,而每個派生類定義各自特有的成員。
對上面提到的不同定價策略建模,我們首先定義一個名爲 Quote 的類,並將它作爲層次關係中的基類。Quote 對象表示按原價銷售的書籍。Quote 派生出另一個名爲 Bulk_quote 的類,它表示可以打折銷售的書籍。
這些類將包含下面兩個成員函數:
- isbn(),返回書的 isbn 編號。該操作不涉及派生類的特殊性,因此只定義在 Quote 中
- net_price(size_t),返回書籍的實際銷售價格,前提是用於購買該書的數量達到一定標準。這個操作顯然是類型相關的,Quote 和 Bulk_quote 都應該包含該函數
在 C++ 中,基類將某些類型相關的函數與派生類不做改變直接繼承的函數區別對待。對於與類型相關的函數,基類希望它們的派生類各自定義適合自己的版本,此時基類就將這些函數聲明成虛函數。因此,我們可以將 Quote 類這樣編寫:
class Quote {
public:
std::string isbn() const;
virtual double net_price(std::size_t n) const;
};
派生類必須通過使用類派生列表明確指出它是從哪個(哪些)基類繼承而來的。類派生列表的形式是:首先一個冒號,後面緊跟以逗號分隔的基類列表,其中每個基類前面可以有訪問說明符。
class Bulk_quote : public Quote { // Bulk_quote 繼承 Quote
public:
double net_price(std::size_t n) const override;
}
我們可以發現,Bulk_quote 派生列表中使用了 public,所以我們完全可以把 Bulk_quote 的對象當成 Quote 的對象來使用。
派生類必須在其內部對所有重新定義的虛函數進行聲明。派生類可以在這樣的函數之前加上 virtual,但非必須。C++11 允許顯式地註明它將使用哪個成員函數改寫基類的虛函數,具體做法是在參數列表後加上 override 關鍵字。
動態綁定
使用動態綁定,我們能用同一段代碼分別處理 Quote 和 Bulk_quote 對象。例如,當要購買的書籍和購買的數量已知時,下面的函數負責打印總費用:
double print_total(ostream &os,const Quote &item,size_t n) {
// 根據傳入 item 形參的對象類型,調用 Quote::net_price
// 或者 Bulk_quote::net_price
double ret = item.net_price(n);
os << "ISBN: " << item.isbn()
<< " # sold: " << n << " total due: " << ret << endl;
return ret;
}
關於上面的函數有兩個有意思的結論:因爲函數 print_total 的 item 形參是 Quote 的一個引用,我們既能使用基類 Quote 對象調用該函數,也能用派生類 Bulk_quote 調用該函數;又因爲 print_total 是使用引用類型調用 net_price,所以實際傳入 print_total 的對象類型將決定到底指向哪個 net_price:
// basic 的類型是 Quote,bulk 是 Bulk_quote
print_total(cout, basic, 20); // 調用 Quote 的
print_total(cout, bulk, 20); // 調用 Bulk_quote 的
在 C++ 中,當我們使用基類的引用 (或指針) 調用一個虛函數時將發生動態綁定。
定義基類和派生類
定義基類
首先有 Quote 類的定義:
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;
};
記住,作爲繼承關係中的根節點的類通常都會定義一個虛析構函數。即使該函數不進行任何實際操作。
成員函數與繼承
派生類可以繼承基類的成員,但對於 net_price 這樣與類型相關的操作時,派生類需要對這些操作提供自己的新定義以覆蓋 (override) 從基類繼承而來的舊定義。
C++ 中,基類必須將它的兩種成員函數區分開來:一種是基類希望其派生類進行覆蓋的函數;另一種是基類希望派生類直接繼承而不改變的函數。對於前者,基類通常將其定義爲虛函數,以便該調用被動態綁定。
任何構造函數之外的非靜態函數都可以是虛函數。關鍵字 virtual 只能出現在類內部的聲明語句中,不能用於類外部的函數定義。派生類繼承而來的虛函數隱式地也是虛函數。
成員函數如果沒有被聲明成虛函數,其解析過程發生在編譯時而非運算時。如果 isbn() 函數一樣,無論是基類還是派生類,都不會出現執行哪個版本的問題。
訪問控制與繼承
派生類可以繼承定義在基類中的成員,但是派生類的成員函數不一定能夠訪問從基類繼承而來的成員。派生類能訪問公有成員,不能訪問私有成員。同時,protected 訪問說明符指明這些成員能夠被派生類訪問,但不能被其他用戶訪問。
定義派生類
派生類必須通過使用類派生列表明確指出它是從哪個(哪些)基類繼承而來的。**類派生列表的形式是:首先一個冒號,後面緊跟以逗號分隔的基類列表,其中每個基類前面可以有訪問說明符。**訪問說明符有三種:public、private 和 protected。
派生類必須將其繼承而來需要覆蓋的成員函數重新聲明,因此我們的 Bulk_quote 類必須包含一個 net_price 成員:
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_qy = 0; // 使用折扣政策的最低購買量
double discount = 0; // 以小數表示的折扣額
};
如果一個派生是公有的,則基類的公有成員也是派生類接口的組成部分。此外,我們能將公有派生類型的對象綁定到基類的引用或指針上。
所以我們的 Bulk_quote 的接口隱式包含 isbn 函數,同時,在任何需要 Quote 的引用或指針的地方我們都能使用 Bulk_quote 的對象。
派生類中的虛函數
派生類經常 (但不總是)覆蓋它繼承的虛函數。如果派生類沒有覆蓋其基類中的某個虛函數,則該虛函數的行爲類似其他普通成員,派生類會直接繼承其在類中的版本。
派生類可以在它覆蓋的函數前使用 virtual 關鍵字。C++11 允許派生類顯式地註明它使用某個成員函數覆蓋了它繼承的虛函數,具體做法如上。
派生類對象及派生類向基類的類型轉換
一個派生類對象包含多個組成部分:一個含有派生類自己定義的 (非靜態) 成員的子對象,以及一個與該派生類繼承的基類對應的子對象,如果有多個基類,那麼這樣的子對象也有多個。
因此,一個 Bulk_quote 對象將包含四個數據元素:它從 Quote 繼承而來的 bookNo 和 price 數據成員,以及 Bulk_quote 自己定義的 min_qty 和 discount 成員。
C++ 並沒有明確規定派生類的對象在內存中如何分佈,以及在一個對象中,繼承自基類的部分和派生類自定義的部分不一定是連續存儲的。
派生類對象中含有與其基類對應的組成部分,所以我們能把派生類對象當成基類對象來使用,而且我們也能將基類的指針或引用綁定到派生類對象中的基類部分上。
Quote item;
Bulk_quote bulk;
Quote *p = &item;
p = &bulk; // p 指向 bulk 的 Quote 部分
Quote &r = bulk; // r 綁定到 bulk 的 Quote 部分
這種轉換通常稱爲派生類到基類類型轉換。編譯器會隱式執行派生類到基類的轉換。
派生類構造函數
儘管派生類對象中含有從基類繼承而來的成員,但是派生類不能直接初始化這些成員。派生類必須通過基類的構造函數來初始化它的基類部分。
每個類控制它自己的成員初始化過程
派生類對象的基類部分與派生類對象自己的數據成員都是在構造函數的初始化階段執行初始化的。派生類構造函數通過構造函數初始化列表來將實參傳遞給基類構造函數。
例如,接受四個參數的 Bulk_quote 構造函數如下:
Bulk_quote::Bulk_quote(const std::string &book, double p, std::size_t qty, double disc):
Quote(book,p), min_qy(qty), discount(disc) { }
除非我們特別指出,否則派生類對象的基類部分就會像數據成員一樣執行默認初始化。
派生類的成員初始化順序:先初始化基類部分,然後按照聲明的順序依次初始化派生類的成員。
派生類使用基類的成員
派生類可以訪問基類的公有成員和受保護成員:
double Bulk_quote::net_price(std::size_t cnt) const {
if(cnt >= min_qy)
return cnt * (1 - discount) * price;
return cnt * price;
}
繼承與靜態成員
如果基類定義了一個靜態成員,則在整個繼承體系中只存在該成員的唯一定義。不論從基類中派生出來多個派生類,對於每個靜態成員來說都只存在唯一的實例。
class Base {
public:
static void statmem();
};
class Derived : public Base {
void f(const Derived&);
};
靜態成員遵循通用的訪問控制規則,如果基類中的成員是 private 的,則派生類無權訪問它。假設某靜態成員是可訪問的,則我們可以通過基類使用它也能通過派生類使用它。
void Derived::f(const Derived &derived_obj) {
Base::statmem(); // ok,Base 定義了 statmem
Derived::statmem(); // ok,Derived 繼承了 statmem
// ok,派生類的對象能訪問基類的靜態成員
derived_obj.statmem(); // 通過 dervied_obj 對象訪問
statmem(); // 通過 this 訪問
}
派生類的聲明
派生類的聲明與普通類一樣,不用包含它的派生列表:
class Bulk_quote : public Quote; // 錯誤,派生列表不能出現在這裏
class Bulk_quote; // ok,聲明派生類的正確方式
被用作基類的類
如果我們想將某個類用作基類,則該類必須已經定義而非僅僅聲明:
class Quote;
class Bulk_quote : Quote { ... }; // 錯誤
原因顯而易見,派生類需要包含且使用基類的成員,所以基類必須被定義而非聲明。通過這個規定我們可以知道,一個類不能派生它本身。
一個類是基類的同時,也可以是派生類:
class Base { /* */ };
class D1 : public Base { /* */ };
class D2 : public D1 { /* */ };
在這個繼承關係中,Base 是 D1 的直接基類,是 D2 的間接基類。所以我們可以知道,對於一個派生類來說,它將包含它的直接基類的子對象以及每個間接基類的子對象。
防止繼承的發生
C++11 中提供了一種防止繼承的方法:在類名後跟一個關鍵字 final:
class NoDerived filnal { /* */ }; // NoDerived 不能作爲基類
class Bad : NoDerived { /* */ }; // 錯誤
class Base { /* */ };
class Last final : Base { /* */ }; // Last 不能作爲基類
類型轉換與繼承
正常情況下,我們的引用或指針的類型應該與想要綁定對象的類型相同。但存在繼承關係的類是一個重要的例外:我們可以將基類的指針或引用綁定到派生類對象上。所以,當使用基類的引用 (或指針) 時,實際上我們並不清楚該引用 (或指針) 所綁定對象的真實類型。
靜態類型與動態類型
當我們使用存在繼承關係的類型時,必須將一個變量或其他表達式的靜態類型與該表達式表示對象的動態類型區分開來。
表達式的靜態類型在編譯時總是已知的,它是變量聲明時的類型或表達式生成的類型;動態類型則是變量或表達式表示的內存中的對象類型,動態類型直到運行時纔可知。
例如,當 print_total 調用 net_price 時:
double ret = item.net_price(n);
我們知道 item 的靜態類型是 Quote&,它的動態類型直到運行調用此函數時纔會知道。如果我們傳遞給 print_total 的是 Bulk_quote,則 item 的動態類型是 Bulk_quote。
如果表達式不是引用或指針,則它的動態類型與靜態類型一致。例如,Quote 類型的變量永遠是一個 Quote 對象。
基類的指針或引用的靜態類型可能與其動態類型不一致。這是顯然的,因爲基類的指針或引用綁定到派生類對象上。
不存在基類向派生類的隱式類型轉換……
之所以存在派生類向基類的類型轉換是因爲每個派生類對象都包含基類的一部分,而基類的引用或指針可以綁定到該基類部分上。一個基類對象既可以以獨立的形式存在,也可以作爲派生類對象的一部分存在。所以不存在從基類向派生類的自動轉換。
Quote base;
Bulk_quote* bulkP = &base; // 錯誤:不能從基類轉換成派生類
Bulk_quote& bulkRef = base; // 錯誤:不能從基類轉換成派生類
假設轉換成立,則我們有可能會使用 bulkP 或者 bulkRef 訪問本 base 中本不存在的成員。
需要特別注意一點:即使一個基類指針或引用綁定在一個派生類對象上,我們也不能指向從基類向派生類的轉換:
Bulk_quote bulk;
Quote *itemP = &bulk; // 正確
Bulk_quote *bulkP = item; // 錯誤,不能從基類轉換成派生類
……在對象之間不存在類型轉換
派生類向基類的自動轉換隻對指針類型或引用類型有效,在派生類類類型和基類類型之間不存在這樣的轉換。
我們知道,對於類類型的初始化是調用構造函數,當進行賦值操作時,調用賦值運算符。而這些成員都包含參數類型是該類類型的 const 版本的引用。這些操作都不是虛函數。所以我們允許在基類的拷貝/移動操作中傳遞一個派生類對象,但使用的操作都是基類的操作。
Bulk_quote bulk;
Quote item(bulk); // 使用 Quote::Quote(const Quote&) 構造函數
item = bulk; // 使用 Quote::operator=(const Quote&)
上述代碼會忽略 Bulk_quote 自己定義的成員部分。
當我們用一個派生類對象爲一個基類對象初始化或賦值時,只有該派生類對象中的基類部分會被拷貝、移動或賦值,它的派生類部分將被忽略掉。