第十五章 面向物件程式設計
阿新 • • 發佈:2022-05-17
轉載自https://github.com/applenob/Cpp_Primer_Practice,看C++primer的時用的筆記。自己做了一些補充,感謝前人的總結
OOP:概述
- 面向物件程式設計(object-oriented programming)的核心思想是資料抽象、繼承和動態繫結。
-
繼承(inheritance):
- 通過繼承聯絡在一起的類構成一種層次關係。
- 通常在層次關係的根部有一個基類(base class)。
- 其他類直接或者簡介從基類繼承而來,這些繼承得到的類成為派生類(derived class)。
- 基類負責定義在層次關係中所有類共同擁有的成員,而每個派生類定義各自特有的成員。
- 對於某些函式,基類希望它的派生類個自定義適合自己的版本,此時基類就將這些函式宣告成虛擬函式(virtual function)。
- 派生類必須通過使用類派生列表(class derivation list)明確指出它是從哪個基類繼承而來。形式:一個冒號,後面緊跟以逗號分隔的基類列表,每個基類前都可以有訪問說明符。
class Bulk_quote : public Quote{};
- 派生類必須在其內部對所有重新定義的虛擬函式進行宣告。可以在函式之前加上
virtual
關鍵字,也可以不加。C++11新標準允許派生類顯式地註明它將使用哪個成員函式改寫基類的虛擬函式,即在函式的形參列表之後加一個override
-
動態繫結(dynamic binding,又稱執行時繫結):
- 使用同一段程式碼可以分別處理基類和派生類的物件。
- 函式的執行版本由實參決定,即在執行時選擇函式的版本。
定義基類和派生類
定義基類
- 基類通常都應該定義一個虛解構函式,即使該函式不執行任何實際操作也是如此。
- 基類通過在其成員函式的宣告語句前加上關鍵字
virtual
使得該函式執行動態繫結。 - 如果成員函式沒有被宣告為虛擬函式,則解析過程發生在編譯時而非執行時。
- 訪問控制:
-
protected
: 基類和和其派生類還有友元可以訪問。 -
private
: 只有基類本身和友元可以訪問。
-
定義派生類
- 派生類必須通過類派生列表(class derivation list)明確指出它是從哪個基類繼承而來。形式:冒號,後面緊跟以逗號分隔的基類列表,每個基類前面可以有一下三種訪問說明符的一個:
public
protected
、private
。 - C++11新標準允許派生類顯式地註明它將使用哪個成員函式改寫基類的虛擬函式,即在函式的形參列表之後加一個
override
關鍵字。 - 派生類建構函式:派生類必須使用基類的建構函式去初始化它的基類部分。
- 靜態成員:如果基類定義了一個基類成員,則在整個繼承體系中只存在該成員的唯一定義。
- 派生類的宣告:宣告中不包含它的派生列表。
- C++11新標準提供了一種防止繼承的方法,在類名後面跟一個關鍵字
final
。
型別轉換與繼承
- 理解基類和派生類之間的型別抓換是理解C++語言面向物件程式設計的關鍵所在。
- 可以將基類的指標或引用繫結到派生類物件上。
靜態型別與動態型別:如果基類在實參中是引用或者指標的形式,那麼就可以動態型別轉換,即基類可以被派生類表示。但如果是實參既不是引用也不是指標,則它的靜態型別和動態型別是一致的
- 不存在從基類向派生類的隱式型別轉換。
- 派生類向基類的自動型別轉換隻對指標或引用型別有效,物件之間不存在型別轉換。
- 如果將一個派生類物件賦值或拷貝給基類物件,該操作只會執行基類成員的拷貝建構函式或者賦值函式,而派生類中的資料會被切掉(sliced down)/當用一個派生類物件為一個基類物件初始化或賦值時,只有該派生類物件中的基類部分會被拷貝、移動或者賦值,它的派生類部分將會被忽略掉
存在繼承關係的型別之間的轉換規則
- 從派生類向基類的型別轉換隻對指標或引用型別有效
- 基類向派生類不存在隱式型別轉換
- 和任何其他成員一樣,派生類想基類的型別轉換也可能會由於訪問受限而變得不可行
虛擬函式
-
使用虛擬函式可以執行動態繫結。
-
OOP的核心思想是多型性(polymorphism)。
- 引用或指標的靜態型別和動態型別不同這一事實正是c++支援多型的根本所在
-
當且僅當對通過指標或引用呼叫虛擬函式時,才會在執行時解析該呼叫,也只有在這種情況下物件的動態型別才有可能與靜態型別不同。
-
派生類必須在其內部對所有重新定義的虛擬函式進行宣告。可以在函式之前加上
virtual
關鍵字,也可以不加。 -
C++11新標準允許派生類顯式地註明它將使用哪個成員函式改寫基類的虛擬函式,即在函式的形參列表之後加一個
override
關鍵字。 -
如果我們想覆蓋某個虛擬函式,但不小心把形參列表弄錯了,這個時候就不會覆蓋基類中的虛擬函式。加上
override
可以明確程式設計師的意圖,讓編譯器幫忙確認引數列表是否出錯。 -
如果虛擬函式使用預設實參,則基類和派生類中定義的預設實參最好一致。
-
通常,只有成員函式(或友元)中的程式碼才需要使用作用域運算子(
::
)來回避虛擬函式的機制。
抽象基類
-
純虛擬函式(pure virtual):清晰地告訴使用者當前的函式是沒有實際意義的。純虛擬函式無需定義,只用在函式體的位置前書寫
=0
就可以將一個虛擬函式說明為純虛擬函式。 - 含有純虛擬函式的類是抽象基類(abstract base class)。不能建立抽象基類的物件。
- 派生類建構函式只初始化他的直接基類,也就是說派生類如果沒有自己的資料成員也要提供一個接受基類中引數的建構函式。
訪問控制與繼承
- 受保護的成員:
-
protected
說明符可以看做是public
和private
中的產物。 - 類似於私有成員,受保護的成員對類的使用者來說是不可訪問的。
- 類似於公有成員,受保護的成員對於派生類的成員和友元來說是可訪問的。
- 派生類的成員或友元只能通過派生類物件來訪問基類的受保護成員。派生類對於一個基類物件中的受保護成員沒有任何訪問特權。
-
- 派生訪問說明符:
- 對於派生類的成員(及友元)能否訪問其直接基類的成員沒什麼影響。
- 派生訪問說明符的目的是:控制派生類使用者對於基類成員的訪問許可權(即類的使用者的許可權,如果是private,則不能通過派生類直接呼叫基類中的資料成員)。比如
struct Priv_Drev: private Base{}
意味著在派生類Priv_Drev
中,從Base
繼承而來的部分都是private
的。
- 友元關係不能繼承。
- 改變個別成員的可訪問性:使用
using
。 - 預設情況下,使用
class
關鍵字定義的派生類是私有繼承的;使用struct
關鍵字定義的派生類是公有繼承的。- 15.18:派生類公有的繼承基類的時候,使用者程式碼才能使用派生類向基類轉換
繼承中的類作用域
- 每個類定義自己的作用域,在這個作用域內我們定義類的成員。當存在繼承關係時,派生類的作用域巢狀在其基類的作用域之內。
- 派生類的成員將隱藏同名的基類成員。
- 除了覆蓋繼承而來的虛擬函式之外,派生類最好不要重用其他定義在基類中的名字。
名字查詢與繼承
假設呼叫p->mem()或者obj.mem(),需要依次呼叫以下四個步驟:
- 首先確定p的靜態型別。因為我們呼叫的是一個成員,所以該型別必然是類型別。
- 在p的靜態型別對應的類中查詢mem。如果找不到,則依次在直接基類中不斷查詢直至到達繼承鏈的頂端。如果找遍了該類及其基類仍然找不到,則編譯器將報錯。
- 一旦找到了mem,就進行常規的型別檢查已確認對於當前找到的mem,本次呼叫是否合法。
- 假設呼叫合法,則編譯器將根據呼叫的是否是虛擬函式而產生不同的程式碼:
- 如果mem是虛擬函式且我們是通過引用或指標進行的呼叫,則編譯器產生的程式碼將在執行時確定到底執行該虛擬函式的哪個版本,依據是物件的動態型別
- 反之,如果mem不是虛擬函式或者我們是通過物件(而非指標或引用)進行的呼叫,則編譯器將產生一個常規函式呼叫。
建構函式與拷貝控制
虛解構函式
- 基類通常應該定義一個虛解構函式,這樣我們就能動態分配繼承體系中的物件了。
- 如果基類的解構函式不是虛擬函式,則
delete
一個指向派生類物件的基類指標將產生未定義的行為。 - 虛解構函式將阻止合成移動操作。
合成拷貝控制與繼承
- 基類或派生類的合成拷貝控制成員的行為和其他合成的建構函式、賦值運算子或解構函式類似:他們對類本身的成員依次進行初始化、賦值或銷燬的操作。
- 如果基類中的預設建構函式,拷貝建構函式、拷貝賦值函式或者解構函式是被刪除的函式或者不可訪問,則派生類中對應的成員將是被刪除的
- 如果在基類中有一個不可訪問或者刪除掉的解構函式,則派生類中合成的預設和拷貝建構函式將是被刪除的,因為編譯器無法銷燬派生類物件的基類部分
- 如果派生類確實需要移動操作,需要在基類中定義移動操作。只要有移動操作就必須有顯式的拷貝操作
派生類的拷貝控制成員
- 當派生類定義了拷貝或移動操作時,該操作負責拷貝或移動包括基類部分成員在內的整個物件。
- 派生類解構函式:派生類解構函式先執行,然後執行基類的解構函式。
繼承的建構函式
- C++11新標準中,派生類可以重用其直接基類定義的建構函式。
- 如
using Disc_quote::Disc_quote;
,註明了要繼承Disc_quote
的建構函式。
容器與繼承
- 當我們使用容器存放繼承體系中的物件時,通常必須採用間接儲存的方式。
- 派生類物件直接賦值給積累物件,其中的派生類部分會被切掉。
- 在容器中放置(智慧)指標而非物件。
- 對於C++面向物件的程式設計來說,一個悖論是我們無法直接使用物件進行面向物件程式設計。相反,我們必須使用指標和引用。因為指標會增加程式的複雜性,所以經常定義一些輔助的類來處理這些複雜的情況。
文字查詢程式再探
- 使系統支援:單詞查詢、邏輯非查詢、邏輯或查詢、邏輯與查詢。
面向物件的解決方案
- 將幾種不同的查詢建模成相互獨立的類,這些類共享一個公共基類:
WordQuery
NotQuery
OrQuery
AndQuery
- 這些類包含兩個操作:
-
eval
:接受一個TextQuery
物件並返回一個QueryResult
。 -
rep
:返回基礎查詢的string
表示形式。
-
- 繼承和組合:
- 當我們令一個類公有地繼承另一個類時,派生類應當反映與基類的“是一種(Is A)”的關係。
- 型別之間另一種常見的關係是“有一個(Has A)”的關係。
- 對於面向物件程式設計的新手來說,想要理解一個程式,最困難的部分往往是理解程式的設計思路。一旦掌握了設計思路,接下來的實現也就水到渠成了。
Query程式設計:
操作 | 解釋 |
---|---|
Query 程式介面類和操作 |
|
TextQuery |
該類讀入給定的檔案並構建一個查詢圖。包含一個query 操作,它接受一個string 實參,返回一個QueryResult 物件;該QueryResult 物件表示string 出現的行。 |
QueryResult |
該類儲存一個query 操作的結果。 |
Query |
是一個介面類,指向Query_base 派生類的物件。 |
Query q(s) |
將Query 物件q 繫結到一個存放著string s 的新WordQuery 物件上。 |
q1 & q2 |
返回一個Query 物件,該Query 繫結到一個存放q1 和q2 的新AndQuery 物件上。 |
`q1 | q2` |
~q |
返回一個Query 物件,該Query 繫結到一個存放q 的新NotQuery 物件上。 |
Query 程式實現類 |
|
Query_base |
查詢類的抽象基類 |
WordQuery |
Query_base 的派生類,用於查詢一個給定的單詞 |
NotQuery |
Query_base 的派生類,用於查詢一個給定的單詞 |
BinaryQuery |
Query_base 的派生類,查詢結果是Query 運算物件沒有出現的行的集合 |
OrQuery |
Query_base 的派生類,返回它的兩個運算物件分別出現的行的並集 |
AndQuery |
Query_base 的派生類,返回它的兩個運算物件分別出現的行的交集 |