第15章 面向對象程序設計
15.1 OOP:概述
- 面向對象程序設計的核心思想是數據抽象、繼承和動態綁定。數據抽象可將類的接口與實現分離,繼承可定義相似的類型并對相似關系建模,動態綁定可在一定程度上忽略相似類型的區別,而以統一的方式使用它們的對象。
- 繼承時分基類和派生類,基類定義所有類共同擁有的成員,派生類定義各自特有的成員。派生類必須在其內部對所有重新定義的虛函數進行聲明。
- 動態綁定又稱運行時綁定,即在運行時選擇函數版本。當使用基類的引用或指針調用虛函數時發生動態綁定。
15.2 定義基類和派生類
1. 定義基類
- 基類通常應該定義一個虛析構函數,即使該函數不執行任何操作。
- 基類將成員函數分為兩種:一種是基類希望其派生類進行覆蓋的函數;另一種是基類希望派生類直接繼承而不要改變的函數。
- 除構造函數之外的非靜態函數都可以是虛函數。
-
virtual
只能出現在類內聲明語句之前,而不能用于類外函數定義。
2. 定義派生類
- 派生類通常覆蓋其繼承的虛函數,若未覆蓋,則派生類會直接繼承其在基類中的版本。
- 派生類包含一個派生類自定義的子對象、一個或多個基類子對象。派生類到基類的類型轉換即基類引用或指針可以綁定派生類對象的基類部分。
- 每個類負責定義各自的接口。派生類不能直接初始化基類的成員,而應該通過基類的構造函數來進行初始化。
- 類必須在定義后才能作為基類,僅聲明而已不行。類不能派生它本身。派生類聲明時應包含類名,而不能包含派生列表。
- 基類可分為直接基類和間接基類,直接基類出現在派生列表中,間接基類通過其直接基類繼承而來。
3. 類型轉換與繼承
- 存在派生類向基類的類型轉換,但不存在基類向派生類的類型轉換(基類指針或引用可以綁定到派生類對象,但派生類指針或引用不能綁定到基類對象)。
- 靜態類型是變量聲明時的類型或表達式生成的類型,編譯時已知。動態類型是變量或表達式表示的內存中的對象的類型,運行時才可知。
- 若表達式不是指針或引用,則其靜態類型與動態類型保持一致。基類指針或引用的靜態類型可能與動態類型不一致。
- 對象之間不存在類型轉換(基類對象由派生類對象轉換而成則只處理派生類的基類部分,派生類不能由基類轉換而成)。
Quote base; // 基類
Bulk_quote bulk; // 派生類
Quote* baseP = &bulk;
Quote& baseR = bulk;
Bulk_quote *bulkP = &base; // 錯誤
Bulk_quote &bulkR = base; // 錯誤
base = bulk; // 只處理bulk的基類部分
bulk = base; // 錯誤
15.3 虛函數
- 普通函數不使用時可以只聲明不定義,但虛函數無論是否使用都必須提供定義。
- 動態綁定只有當我們通過指針或引用調用虛函數時才發生。
- C++支持多態性的根本在于引用或指針的靜態類型與動態類型不同。
- 基類中的虛函數在派生類中隱含是一個虛函數。
- 若派生類覆蓋虛函數,則該虛函數在基類和派生類中的形參和返回類型必須完全一致。例外:若虛函數返回類型是類本身的指針或引用,且派生類向基類的類型轉換是可訪問的,則基類返回基類的引用或指針,派生類返回派生類的引用或指針。
-
override
顯式注明要覆蓋虛函數,當某個函數被標記為override
時要求該函數是虛函數,且已覆蓋基類中的虛函數。final
可以防止繼承發生。override
和final
可出現在形參列表之后(包括任何const
和引用修飾符)以及尾置返回類型。
- 若調用虛函數時使用默認實參,則不論是派生類或基類,都使用基類定義的默認實參,故基類和派生類中定義的默認實參最好一致。
- 通過作用域運算符
::
可指定執行虛函數的特定版本,而避免虛函數調用時進行動態綁定。
15.4 抽象基類
- 在類內的虛函數聲明語句處
=0
可定義純虛函數,表示該函數無實際意義。純虛函數一般不用定義,若要定義則只能在類外定義函數體。
- 含有純虛函數或者未經覆蓋直接繼承純虛函數的類是抽象基類。不能創建抽象基類的對象,但可以在派生類覆蓋純虛函數時定義派生類的對象。
- 重構負責重新設計類的體系以便將操作或數據從一個類移動到另一個類。
class Quote; // 間接基類
class Disc_quote; // 直接基類,抽象基類
class Bulk_quote; // 派生類
Disc_quote disc; // 錯誤,不能創建抽象基類的對象
Bulk_quote bulk;
15.5 訪問控制與繼承
- 每個類分別控制其成員的初始化過程和對派生類的訪問權限。
public
成員在整個程序內可見。private
成員只對該類成員函數和友元函數可見。protected
成員只對該類成員函數和友元函數、派生類成員函數和友元函數可見。
- 若基類的
public
成員是可訪問的,則派生類向基類的類型轉換也是可訪問的;反之不行。
- 友元關系不能傳遞和繼承。
- 派生類只能為可訪問成員提供
using 類名::成員
以修改直接或間接基類成員的訪問權限,該權限由using 類名::成員
前的訪問說明符決定。
-
class
和struct
唯一的區別:class
的默認成員訪問說明符和默認派生訪問說明符是private
,而struct
的是public
。
15.6 繼承中的類作用域
- 派生類的作用域位于基類作用域之內。查找派生類成員名字時,若在派生類中未找到則會前往基類查找。
- 若派生類成員與基類成員同名,則派生類成員會隱藏基類成員。雖然可通過作用域運算符
::
來使用隱藏成員,但最好不要在派生類中重用除虛函數之外的定義在基類中的名字。
- 調用
p->mem()
執行的步驟:確定p
的靜態類型;在靜態類型對應的類中查找mem
;找到后,對mem
進行類型檢查確保調用合法;合法后,若mem
是虛函數且通過引用或指針進行調用則會在運行時依據動態類型確定虛函數版本,若mem
不是虛函數或通過對象進行調用則會產生一個常規函數調用。
15.7 構造函數與拷貝控制
- 若基類的析構函數不是虛函數,則
delete
一個指向派生類對象的基類指針將產生未定義的行為。
- 若一個類已自定義拷貝構造函數、拷貝賦值運算符或析構函數,則編譯器不會為該類合成移動構造函數或移動賦值運算符。
- 基類通常不會合成移動操作。當基類通過
=default
顯式合成移動操作時,移動基類對象實際使用合成拷貝操作,故必須同時顯式定義拷貝操作。
- 派生類的構造函數和賦值運算符在拷貝和移動自有成員的同時,也要拷貝和移動基類部分的成員。但析構函數只負責銷毀派生類自己分配的資源。
- 在默認情況下,基類默認構造函數初始化派生類對象的基類部分。若想拷貝或移動基類部分,則必須在派生類的構造函數初始值列表中顯式使用基類的拷貝或移動構造函數。與拷貝和移動構造函數一樣,派生類的賦值運算符也必須顯式為其基類部分賦值。
- 在析構函數體執行完成后,對象的成員會被隱式銷毀。類似的對象的基類部分也是隱式銷毀的。對象銷毀的順序與其創建的順序相反。
- 若構造函數或析構函數調用某個虛函數,則我們應執行與構造函數或析構函數所屬類型相對應的虛函數版本。
- 類不能繼承默認、拷貝和移動構造函數,除非使用
using
。若派生類未定義構造函數,則編譯器將為派生類合成構造函數。
- 和普通成員的
using
聲明不同,構造函數的using
聲明不會改變構造函數的訪問級別。
- 若基類的構造函數是
explicit
或constexpr
,則繼承的構造函數也是explicit
或constexpr
。若基類構造函數含有默認實參,派生類不會繼承默認實參,而會繼承多個構造函數,其中每個構造函數分別省略掉一個含有默認實參的形參。
Derived(const Derived &d):Base(d) {....} // 拷貝基類成員
Derived(const Derived &d): {....} // 基類部分被默認初始化,而非拷貝
Derived(Derived &&d):Base(std::move(d)) {....} // 拷貝基類成員
Derived& Derived::operator=(const Derived &rhs)
{
Base::operator=(rhs);// 為基類部分賦值
....
return *this;
}
15.8 容器與繼承
- 容器中只能存放同一類型的元素。基類與派生類雖然存在繼承關系,但不是同一類型。若需要在容器中存放具有繼承關系的對象,則應存放指向基類對象的指針。
-
upper_bound
返回一個迭代器,該迭代器指向第一個與當前元素的關鍵字不相同的元素。
15.9 文本查詢程序再探