- 對于某些函數,基類希望它的派生類各自定義適合自身的版本,此時基類就將這些函數聲明成虛函數(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的形式使用了合成的版本,編譯器也不會為這個類合成移動操作。
- 基類或派生類的合成拷貝控制成員與其他合成的構造函數,賦值運算符或析構函數類似:它們對類本身的成員一次進行初始化,賦值或銷毀。此外,還負責使用直接基類中對應的操作對一個對象的直接基類部分進行相應的操作。
- 默認情況下,基類默認構造函數初始化派生類對象的基類部分。如果我們想拷貝(或移動)基類部分,則必須在派生類的構造函數初始值列表中顯式地使用基類的拷貝(或移動)構造函數。同樣的,派生類的賦值運算符也必須顯式地為基類部分賦值。
- 如果構造函數或析構函數調用了某個虛函數,則我們應該執行與構造函數或析構函數所屬類型相對應的虛函數版本。
- 當派生類對象被賦值給基類對象時,其中的派生類部分將被切掉,因此容器和存在繼承關系的類型無法兼容。當我們希望在容器中存放具有繼承關系的對象時,我們實際上存放的通常是基類指針(更好的選擇是智能指針)。