C 入門教程(六十四) 虛函式和多型

2021-08-18 08:35:41 字數 4406 閱讀 8048

小古銀的官方**(完整教程):

虛函式和多型

注意事項和建議

補充知識

繼承如果使用錯誤會導致記憶體洩漏,請看下面兩個例子:

#include class baseclass

~baseclass(void)

};class derivedclass : public baseclass

~derivedclass(void)

};int main(void)

輸出結果:

基類建構函式執行中

派生類建構函式執行中

基類析構函式執行中

我們沒有看到派生類析構函式執行中。根據前面教程講的,當用基類的指標或者引用儲存派生類的物件的時候,如果此時用這個引用或者指標進行操作,那麼只會執行基類的成員函式,而這個行為同樣也適用於析構函式。所以上面**中通過基類指標釋放堆記憶體,只會呼叫基類的析構函式而不會呼叫派生類的析構函式。也因此,如果析構函式內有釋放堆記憶體的**,那麼將無法釋放堆記憶體而造成記憶體洩漏。

那麼如果派生類不需要在析構函式內進行釋放操作,是不是就不會記憶體洩漏呢?看下面例子:

#include class testclass

~testclass(void)

};class baseclass

~baseclass(void)

};class derivedclass : public baseclass

~derivedclass(void)

private:

testclass obj;

};int main(void)

輸出結果:

基類建構函式執行中

測試建構函式執行中

派生類建構函式執行中

基類析構函式執行中

同樣也看不到測試析構函式執行中。這個例子也印證了我之前教程所說,成員變數的釋放是必須要有析構函式的,而此時派生類的析構函式不執行,導致記憶體洩漏。

那麼如果派生類沒有成員變數,是不是就不會記憶體洩漏呢?事實上仍然會記憶體洩漏或者出現其他問題。

c++標準中對通過基類指標釋放派生類記憶體的行為沒有說明,即未定義行為除非派生類的析構函式是虛的。未定義行為的實際行為取決於編譯器怎樣做,但是無論編譯器怎麼做,使用未定義行為都是錯誤的做法,都會使程式出現未知錯誤

虛函式可以解決上面的問題。

只有除建構函式外的類成員函式才可以定義為虛函式。

在成員函式的宣告前面加上關鍵字virtual,那麼該成員函式就是虛函式,如:

virtual bool empty(void) const;
應該注意的是:virtual應該設定在自己設計的基類中的成員函式上,而不是設定在派生類或者別人設計的類上面。

派生類繼承基類時,如果有成員函式和基類的虛函式同名時,那麼派生類的這個成員函式也是虛函式;而將派生類的成員函式設計成和基類的虛函式同名的行為叫做覆蓋或者重寫(英文:override)。

與過載不同的是:派生類重寫成員函式後,如果用基類指標或者引用操作派生類物件時,這時候如果呼叫重寫的虛函式,那麼呼叫的就是派生類的成員函式。換句話說就是,無論用物件還是基類指標還是基類引用,呼叫的都是派生類的成員函式,所以才會被叫作派生類重寫虛函式。

看例子更好理解:

#include class baseclass

;class derivedclass : public baseclass

;int main(void)

baseclass::~baseclass(void)

void baseclass::print_overload(void) const

void baseclass::print_override(void) const

derivedclass::~derivedclass(void)

void derivedclass::print_overload(void) const

void derivedclass::print_override(void) const

輸出結果:

基類過載函式執行中

派生類重寫函式執行中

派生類析構函式執行中

基類析構函式執行中

首先基類的析構函式宣告為虛函式,那麼所有繼承它的派生類的析構函式都是虛函式。也就是說,無論派生類的析構函式有沒有用virtual修飾,只要基類的析構函式是虛函式,那麼所有派生類的析構函式都是虛函式。那麼上面**中virtual ~derivedclass(void);virtual可以省略。

雖然派生類中的virtual可以省略,但還是建議virtual明確寫出來,因為如果有很多類,新類繼承舊類,都不寫virtual,那麼後面想使用其中乙個派生類作為基類的時候,就不知道是不是有虛函式,還要乙個個找它的基類,看是不是其中乙個定義成虛函式,不方便後續使用。

我們也看到**中的派生類重寫的虛函式後面有關鍵字override,關鍵字override只能用在派生類重寫的虛函式上。關鍵字override也可以省略,但也是建議override明確寫出來。如果使用了關鍵字override,由於關鍵字override只能用在派生類重寫的虛函式上的這個規定,當你寫錯了函式名或者其他粗心大意的失誤時,編譯的時候編譯器會報告錯誤告訴你:這個函式雖然寫了override但它並不是可以重寫的函式。

我們再看回**。首先建立堆記憶體的物件,然後將記憶體位址賦值給基類指標。接著通過基類的指標呼叫成員函式print_overload,由於它沒有指明虛函式,所以我們看到它輸出的是基類過載函式執行中。然後通過基類的指標呼叫成員函式print_override,由於它指明了是虛函式,所以我們看到它輸出的是派生類重寫函式執行中。因為基類的析構函式是虛的,從而導致所有派生類的析構函式都是虛的,所以直接delete基類指標儲存的位址時,它還是可以正確釋放所有的記憶體空間。

最後說回什麼是多型?

多型就是多種狀態,就是基類指標或者基類引用可以儲存不同的派生類物件,而且只需要使用基類統一的成員函式名稱,就可以呼叫不同派生類的成員函式。

假設我們需要設計乙個函式去統計字串中某個字元數量,由於統計部分的**都一樣所以可以封裝成函式,不同的就是由外部傳進來需要對字元進行判斷的條件。這時候就可以使用多型:設計乙個基類和虛函式,使用基類的指標或引用作為統計函式的引數,然後由呼叫統計函式的人去繼承這個基類並且重寫這個虛函式。那麼,統計函式就可以通過這個基類指標或引用呼叫它的成員函式,由於虛函式的作用,它將會呼叫派生類的虛函式,從而實現多型的功能。

說到這裡,是不是感覺到有些熟悉的感覺?就是前面講std::function的時候講解的例子。事實上,大部分的多型都可以使用函式式程式設計(可以理解為使用std::function)來代替,只有少部分是代替不了的。c++標準庫中,很多需要的外部介面都使用函式式程式設計而不是多型,因為使用函式式程式設計可以寫更小**,而且更容易理解,而使用函式式程式設計的額外損耗有可能比多型少。

當派生類的析構函式是非虛的時候,不能通過基類指標刪除(delete)派生類記憶體,否則將產生未定義行為。

c++標準庫中除了io庫和異常以外,所有類的析構函式都是非虛的,所以繼承之後的使用需要注意。

當你可以確定你設計的類不會用堆記憶體分配物件時,析構函式可以非虛。當你不確定你設計的類會被怎樣使用,或者說你設計的類是作為第三方庫供別人呼叫而不知道別人會怎樣使用,這時候析構函式就應該使用虛析構函式,防止記憶體洩漏。

類的內部實現使用虛表儲存虛函式,所以需要的記憶體也會多一點,而用基類指標訪問虛函式時,也會有所下降。自己寫程式的時候,其實不必太在意這些小損耗,畢竟損耗不大,反而可以讓你少費時間去思考怎樣設計。

盡量使用智慧型指標代替new/delete。

關鍵字override從c++11開始加入。

C 入門教程(六十六) 丟擲異常

小古銀的官方 完整教程 使用關鍵字throw丟擲異常。throw不僅僅是丟擲std exception和派生類的物件,其實它可以丟擲所有的變數和值,例如 throw 23333 或者throw std string 小古銀嘿嘿嘿 當丟擲int型別的值時,應該捕獲int型別 include int m...

C 入門教程(五十四) 訪問限制

小古銀的官方 完整教程 鞏固練習 使用關鍵字protected之後,在它下面的所有成員都是保護的,程式設計師不可以通過物件來使用這些保護成員,但是可以在繼承類中使用這些保護的成員。使用關鍵字private之後,在它下面的所有成員都是私有的,程式設計師不可以通過物件來使用這些私有成員,也不能在繼承類中...

c 多型和虛函式

c 有三大特性 封裝,繼承,多型 多型是物件導向程式設計的乙個重要特徵,多型就是乙個東西有多重狀態,具有不同功能的函式可以用乙個函式名,這樣就可以用乙個函式名實現不同的功能 靜態多型和動態多型靜態多型是利用過載實現的,在程式編譯時確定要呼叫的是哪個函式,也稱為編譯時多型。動態多型是利用虛函式實現的,...