[C++ Primer Note14] 面向物件程式設計
- 對於某些函式,基類希望它的派生類各自定義適合自身的版本,此時基類就將這些函式宣告成虛擬函式(virtual function) :
class Quote{ public: string isbn() const; virtual double net_price(size_t n) const; }
- 派生類必須通過使用類派生列表 明確指出它是從哪個基類繼承而來的。類派生列表的形式是:首先是一個冒號,後面緊跟著以逗號分隔的基類列表,其中每個基類可以有訪問說明符 。
- 派生類必須在其內部對所有重新定義的虛擬函式進行宣告。派生類可以在這樣的函式之前加上virtual 關鍵字,但並不是非得這麼做。
- 當我們使用基類 的引用或指標呼叫一個虛擬函式時將發生動態繫結 ,即根據實際物件型別來選擇函式版本。
- 基類通常都應該定義一個虛解構函式 ,即使該函式什麼都不做
- 在C++中,基類必須把它的兩種成員函式區分開來:一種是基類希望其派生類進行覆蓋的函式;另一種是基類希望派生類直接繼承而不要改變的函式。
- 關鍵字virtual 智慧出現在類內部的宣告語句之前而不能用於類外部的函式定義。如果基類把一個函式宣告成虛擬函式,則該函式在派生類中隱式地也是虛擬函式 。
- 派生類可以繼承定義在基類中的成員,但是派生類的成員函式不一定有權訪問從基類繼承而來的成員。派生類能訪問公有成員 ,而不能訪問私有成員 。不過某些時候有一種成員基類希望派生類有權訪問,而其他使用者禁止訪問,我們用protected 訪問運算子說明這些成員。
- 類派生列表中用到的訪問說明符 可以是:public ,protected 或者private 中的一個。它的作用是控制派生類從基類繼承而來的成員是否對派生類的使用者可見 。
- 如果派生類沒有覆蓋基類中的某個虛擬函式,則該虛擬函式的行為類似於其他普通成員,派生類會直接繼承其在基類中的版本 。C++11標準允許派生類顯式 地註明它覆蓋了虛擬函式,具體做法是在形參列表後(const,引用限定符後)新增一個關鍵字override 。
- 一個派生類物件包含多個組成部分:一個含有派生類自己定義的成員的子物件,以及一個與該派生類繼承的基類對應的子物件,因為在派生類物件含有與其基類對應的組成部分,所以我們能把派生類物件當成基類物件來使用 ,而且我們也能將基類的指標或引用繫結到派生類物件中的基類部分:
Father father; Son son; Father *p=&father; p=&son; Father &r=son;
這種轉換通常稱為派生類到基類的(derived-to-base) 型別轉換,和其他型別轉換一樣,編譯器會隱式地執行這種轉換。
我們可以把派生類物件或者派生類物件的引用用在需要基類引用的地方;也可以把派生類物件的指標用在需要基類指標的地方。
- 儘管在派生類物件中含有從基類繼承的成員,但是派生類並不能直接初始化成員,派生類必須使用基類的建構函式來初始化它的基類部分 。
- 派生類物件的基類部分和派生類物件自己的資料成員都是在建構函式 的初始化階段執行初始化操作的。派生類建構函式同樣通過初始化列表 來將實參傳遞給基類建構函式的,比如:
Bulk_quote(const string &book,double p,size_t pty,double disc): Quote(book,p), min_qty(qty),discount(disc){ }
首先初始化基類的部分,然後按照宣告的順序依次初始化派生類的成員。
- 如果基類定義了一個靜態成員,則在整個繼承體系中只存在該成員的唯一定義 。
- 派生類的宣告與其他類差別不大,宣告中包含類名但是不包含派生列表 。如果我們想將某個類用作基類,則該類必須已經定義 而非僅僅宣告。
- C++11標準提供了一種防止繼承發生的方法,即在類名後跟一個關鍵字final 。
- 因為一個基類的物件可能是派生類物件的一部分,也可能不是,所以不存在從基類到派生類的自動型別轉換 ,除此以外即使一個基類指標或引用繫結在一個派生類物件上,我們也不能執行基類到派生類的轉換 :
Son son; Father *p=&son;//正確,動態型別是Son Son *ps=p;//錯誤,不能將基類轉換成派生類
- 當我們用一個派生類物件為一個基類物件初始化或賦值時,只有該派生類物件的基類部分會被拷貝,移動或賦值,它的派生類部分將被忽略。
- 我們必須為每一個虛擬函式都提供定義 ,不管它是否被使用,因為編譯器無法確定到底會使用哪個虛擬函式。
- 當且僅當對通過指標或引用呼叫虛擬函式時,才會在執行時解析該呼叫,也只有在這種情況下物件的動態型別才有可能與靜態型別不同。
-
一個派生類的函式如果覆蓋了某個繼承而來的虛擬函式,則它的形參型別必須與被它覆蓋的基類函式完全一致
,同時返回型別
也必須相匹配,但如果類的虛擬函式返回型別是類本身的指標或引用時
,規則無效。
如果D由B派生得到,則B的虛擬函式可以返回B*而派生類可以返回D*,只不過要求從D到B的型別轉換時可訪問的。 - 派生類如果定義了一個函式與基類中虛擬函式的名字相同但是形參列表不同,這仍然是合法的行為 ,但這有時候可能是一種錯誤。我們可以通過override 關鍵字來讓編譯器為我們發現一些錯誤。
- 我們還能把某個函式指定為final 的,這樣之後任何嘗試覆蓋此函式的操作都將引發錯誤:
struct D2:B{ void fi(int) const final; };
- 和其他函式一樣,虛擬函式也可以擁有預設實參,如果某次函式呼叫使用了預設實參,則該實參值由本次呼叫的靜態型別 決定,所以基類和派生類中定義的預設實參最好一致。
- 我們可以通過作用域運算子 來讓對虛擬函式的呼叫不要進行動態繫結,而是強迫其執行虛擬函式的某個特定版本:
double undiscounted=baseP->Quote::net_price(42);
這種機制一般用在派生類的虛擬函式體內呼叫基類虛擬函式版本時,如果沒有使用作用域運算子,則會導致無限遞迴 。
- 我們可以定義純虛擬函式 告訴使用者當前這個函式沒有實際意義。一個純虛擬函式無需定義 ,我們通過在函式體的位置(宣告語句的分號前)書寫=0 就可以將一個虛擬函式說明為純虛擬函式,其中=0 只能出現在類內部的宣告語句處。我們也可以為純虛擬函式提供定義 ,不過函式體必須 在類的外部。
- 含有(或者未經覆蓋直接繼承)純虛擬函式的類是抽象基類(abstract base class) ,我們不能直接建立一個抽象基類的物件 。
- protected 成員對於派生類的成員 和友元 是可訪問的,但只能通過派生類物件 來訪問,派生類對於一個基類物件中的protected 成員沒有任何訪問特權 。
- 派生列表中的訪問說明符對於派生類成員(友元)能否訪問其直接基類的成員沒什麼影響 。對基類成員的訪問許可權只與基類中的訪問說明符有關 。派生列表訪問說明符的目的是控制派生類使用者(包括派生類的派生類)對於基類成員的訪問許可權 。
- 只有當公有繼承時 ,使用者程式碼才能使用派生類向基類的轉換
- 友元關係不能繼承
- 有時我們需要改變派生類繼承的某個名字的訪問級別,通過使用using宣告 可以達到這一目的:
class Base{ public: size_t size() const { return n;} protected: size_t n; }; class Derived: private Base{ public: using Base::size; protected: using Base::n; };
using宣告語句中名字的訪問許可權由之前的訪問說明符決定 。
- 我們曾經介紹過struct 和class 具有不同的預設訪問說明符。類似的,預設派生運算子也由定義派生類所用的關鍵字來決定。預設情況下,使用class定義的派生類是私有繼承的 ,struct則是公有繼承的 。實際上,這兩點也是class和struct的唯二區別了。
- 當存在繼承關係時,派生類的作用域巢狀 在其基類的作用域之內。
- 一個物件,引用或指標的靜態型別 決定了該物件的哪些成員是可見 的,我們能使用哪些成員是由靜態型別決定的。比如我們不能用基類引用呼叫派生類獨有的函式。
- 派生類的成員將隱藏同名的基類成員 。我們可以通過作用域運算子 來使用被隱藏的基類成員。
- 宣告在內層作用域的函式並不會過載宣告在外層作用域的函式。因此,定義在派生類的函式也不會過載基類的同名成員,而只會隱藏 。
- 繼承關係對基類拷貝控制最直接的影響是基類通常應該定義一個虛解構函式 ,這樣我們就能動態分配繼承體系中的物件了。因為這樣我們確保delete基類指標時能執行正確的解構函式版本,如果沒有定義虛解構函式,將產生未定義的行為。
- 如果一個類定義了解構函式,即使通過=default的形式使用了合成的版本,編譯器也不會為這個類合成移動操作 。
- 基類或派生類的合成拷貝控制成員與其他合成的建構函式,賦值運算子或解構函式類似:它們對類本身的成員一次進行初始化,賦值或銷燬。此外,還負責使用直接基類中對應的操作對一個物件的直接基類部分進行相應的操作 。
- 預設情況下,基類預設建構函式初始化派生類物件的基類部分。如果我們想拷貝(或移動)基類部分,則必須在派生類的建構函式初始值列表中顯式地使用基類的拷貝(或移動)建構函式 。同樣的,派生類的賦值運算子也必須顯式地為基類部分賦值 。
- 如果建構函式或解構函式呼叫了某個虛擬函式,則我們應該執行與建構函式或解構函式所屬型別相對應的虛擬函式版本。
- 當派生類物件被賦值給基類物件時,其中的派生類部分將被切掉 ,因此容器和存在繼承關係的型別無法相容。當我們希望在容器中存放具有繼承關係的物件時,我們實際上存放的通常是基類指標 (更好的選擇是智慧指標 )。