總結 筆試中常見virtual函式問題

2021-06-05 03:53:55 字數 4766 閱讀 2635

c++中的虛函式(virtual function) 

1.簡介

虛函式是c++中用於實現多型(polymorphism)的機制。核心理念就是通過基類訪問派生類定義的函式。假設我們有下面的類層次:

class a

};

class b: public a

};

那麼,在使用的時候,我們可以:

a * a = new b();

a->foo(); // 在這裡,a雖然是指向a的指標,但是被呼叫的函式(foo)卻是b的!

這個例子是虛函式的乙個典型應用,通過這個例子,也許你就對虛函式有了一些概念。它虛就虛在所謂「推遲聯編」或者「動態聯編」上,乙個類函式的呼叫並不是在編譯時刻被確定的,而是在執行時刻被確定的。由於編寫**的時候並不能確定被呼叫的是基類的函式還是哪個派生類的函式,所以被成為「虛」函式。

虛函式只能借助於指標或者引用來達到多型的效果,如果是下面這樣的**,則雖然是虛函式,但它不是多型的:

class a ;

class b: public a ;

void bar()

1.1 多型

在了解了虛函式的意思之後,再考慮什麼是多型就很容易了。仍然針對上面的類層次,但是使用的方法變的複雜了一些:

void bar(a * a)

因為foo()是個虛函式,所以在bar這個函式中,只根據這段**,無從確定這裡被呼叫的是a::foo()還是b::foo(),但是可以肯定的說:如果a指向的是a類的例項,則a::foo()被呼叫,如果a指向的是b類的例項,則b::foo()被呼叫。

這種同一**可以產生不同效果的特點,被稱為「多型」。

1.2 多型有什麼用?

多型這麼神奇,但是能用來做什麼呢?這個命題我難以用一兩句話概括,一般的c++教程(或者其它物件導向語言的教程)都用乙個畫圖的例子來展示多型的用途,我就不再重複這個例子了,如果你不知道這個例子,隨便找本書應該都有介紹。我試圖從乙個抽象的角度描述一下,回頭再結合那個畫圖的例子,也許你就更容易理解。

在物件導向的程式設計中,首先會針對資料進行抽象(確定基類)和繼承(確定派生類),構成類層次。這個類層次的使用者在使用它們的時候,如果仍然在需要基類的時候寫針對基類的**,在需要派生類的時候寫針對派生類的**,就等於類層次完全暴露在使用者面前。如果這個類層次有任何的改變(增加了新類),都需要使用者「知道」(針對新類寫**)。這樣就增加了類層次與其使用者之間的耦合,有人把這種情況列為程式中的「bad smell」之一。

多型可以使程式設計師脫離這種窘境。再回頭看看1.1中的例子,bar()作為a-b這個類層次的使用者,它並不知道這個類層次中有多少個類,每個類都叫什麼,但是一樣可以很好的工作,當有乙個c類從a類派生出來後,bar()也不需要「知道」(修改)。這完全歸功於多型--編譯器針對虛函式產生了可以在執行時刻確定被呼叫函式的**。

1.3 如何「動態聯編」

編譯器是如何針對虛函式產生可以再執行時刻確定被呼叫函式的**呢?也就是說,虛函式實際上是如何被編譯器處理的呢?lippman在深度探索c++物件模型[1]中的不同章節講到了幾種方式,這裡把「標準的」方式簡單介紹一下。

我所說的「標準」方式,也就是所謂的「vtable」機制。編譯器發現乙個類中有被宣告為virtual的函式,就會為其搞乙個虛函式表,也就是vtable。vtable實際上是乙個函式指標的陣列,每個虛函式占用這個陣列的乙個slot。乙個類只有乙個vtable,不管它有多少個例項。派生類有自己的vtable,但是派生類的vtable與基類的vtable有相同的函式排列順序,同名的虛函式被放在兩個陣列的相同位置上。在建立類例項的時候,編譯器還會在每個例項的記憶體布局中增加乙個vptr欄位,該欄位指向本類的vtable。通過這些手段,編譯器在看到乙個虛函式呼叫的時候,就會將這個呼叫改寫,針對1.1中的例子:

void bar(a * a)

會被改寫為:

void bar(a * a)

因為派生類和基類的foo()函式具有相同的vtable索引,而他們的vptr又指向不同的vtable,因此通過這樣的方法可以在執行時刻決定呼叫哪個foo()函式。

雖然實際情況遠非這麼簡單,但是基本原理大致如此。

1.4 overload和override

虛函式總是在派生類中被改寫,這種改寫被稱為「override」。我經常混淆「overload」和「override」這兩個單詞。但是隨著各類c++的書越來越多,後來的程式設計師也許不會再犯我犯過的錯誤了。但是我打算澄清一下:

override是指派生類重寫基類的虛函式,就象我們前面b類中重寫了a類中的foo()函式。重寫的函式必須有一致的參數列和返回值(c++標準允許返回值不同的情況,這個我會在「語法」部分簡單介紹,但是很少編譯器支援這個feature)。這個單詞好象一直沒有什麼合適的中文詞彙來對應,有人譯為「覆蓋」,還貼切一些。 

overload約定成俗的被翻譯為「過載」。是指編寫乙個與已有函式同名但是參數列不同的函式。例如乙個函式即可以接受整型數作為引數,也可以接受浮點數作為引數。 

2. 虛函式的語法 

虛函式的標誌是「virtual」關鍵字。

2.1 使用virtual關鍵字

考慮下面的類層次:

class a ;

class b: public a ;

class c: public b // 從b繼承,不是從a繼承! ;

這種情況下,b::foo()是虛函式,c::foo()也同樣是虛函式。因此,可以說,基類宣告的虛函式,在派生類中也是虛函式,即使不再使用virtual關鍵字。

2.2 純虛函式

如下宣告表示乙個函式為純虛函式:

class a ;

乙個函式宣告為純虛後,純虛函式的意思是:我是乙個抽象類!不要把我例項化!純虛函式用來規範派生類的行為,實際上就是所謂的「介面」。它告訴使用者,我的派生類都會有這個函式。

2.3 虛析構函式

析構函式也可以是虛的,甚至是純虛的。例如:

class a ;

當乙個類打算被用作其它類的基類時,它的析構函式必須是虛的。考慮下面的例子:

class a

~a() // 非虛析構函式

private:

char * ptra_;

};class b: public a

~b()

private:

char * ptrb_;

};void foo()

在這個例子中,程式也許不會象你想象的那樣執行,在執行delete a的時候,實際上只有a::~a()被呼叫了,而b類的析構函式並沒有被呼叫!這是否有點兒可怕?

如果將上面a::~a()改為virtual,就可以保證b::~b()也在delete a的時候被呼叫了。因此基類的析構函式都必須是virtual的。

純虛的析構函式並沒有什麼作用,是虛的就夠了。通常只有在希望將乙個類變成抽象類(不能例項化的類),而這個類又沒有合適的函式可以被純虛化的時候,可以使用純虛的析構函式來達到目的。

2.4 虛建構函式?

建構函式不能是虛的。

3. 虛函式使用技巧 

3.1 private的虛函式

考慮下面的例子:

class a

private:

virtual void bar()

};class b: public a

};

在這個例子中,雖然bar()在a類中是private的,但是仍然可以出現在派生類中,並仍然可以與public或者protected的虛函式一樣產生多型的效果。並不會因為它是private的,就發生a::foo()不能訪問b::bar()的情況,也不會發生b::bar()對a::bar()的override不起作用的情況。

這種寫法的語意是:a告訴b,你最好override我的bar()函式,但是你不要管它如何使用,也不要自己呼叫這個函式。

3.2 建構函式和析構函式中的虛函式呼叫

乙個類的虛函式在它自己的建構函式和析構函式中被呼叫的時候,它們就變成普通函式了,不「虛」了。也就是說不能在建構函式和析構函式中讓自己「多型」。例如:

class a

// 在這裡,無論如何都是a::foo()被呼叫!

~a() // 同上

virtual void foo();

};class b: public a ;

void bar()

如果你希望delete a的時候,會導致b::foo()被呼叫,那麼你就錯了。同樣,在new b的時候,a的建構函式被呼叫,但是在a的建構函式中,被呼叫的是a::foo()而不是b::foo()。

3.3 多繼承中的虛函式

3.4 什麼時候使用虛函式

在你設計乙個基類的時候,如果發現乙個函式需要在派生類裡有不同的表現,那麼它就應該是虛的。從設計的角度講,出現在基類中的虛函式是介面,出現在派生類中的虛函式是介面的具體實現。通過這樣的方法,就可以將物件的行為抽象化。

以設計模式[2]中factory method模式為例,creator的factorymethod()就是虛函式,派生類override這個函式後,產生不同的product類,被產生的product類被基類的anoperation()函式使用。基類的anoperation()函式針對product類進行操作,當然product類一定也有多型(虛函式)。

另外乙個例子就是集合操作,假設你有乙個以a類為基類的類層次,又用了乙個std::vector

來儲存這個類層次中不同類的例項指標,那麼你一定希望在對這個集合中的類進行操作的時候,不要把每個指標再cast回到它原來的型別(派生類),而是希望對他們進行同樣的操作。那麼就應該將這個「一樣的操作」宣告為virtual。

現實中,遠不只我舉的這兩個例子,但是大的原則都是我前面說到的「如果發現乙個函式需要在派生類裡有不同的表現,那麼它就應該是虛的」。這句話也可以反過來說:「如果你發現基類提供了虛函式,那麼你最好override它」。

總結 筆試中常見virtual函式問題

c 中的虛函式 virtual function 1.簡介 虛函式是c 中用於實現多型 polymorphism 的機制。核心理念就是通過基類訪問派生類定義的函式。假設我們有下面的類層次 class a class b public a 那麼,在使用的時候,我們可以 a a new b a foo ...

job 面試中常見的筆試梳理

有的公司很詭異,會寫一大堆函式讓你猜他的意思。所以啊。面試準備,就把手冊中根據分類把相關的函式看一下。常見分類如下 1 檔案目錄操作 2 gd庫的操作 3 陣列的操作 4 字串操作 5 nosql,memcache,mysql,mysqli,curl等常用第三方庫的操作 1 http的特點 2 狀態...

pytorch中常見錯誤總結

錯誤1 在console中輸入import torchvision會報錯 importerror cannot import name pillow version 原因 torchvision和pillow版本不相容,可能pillow版本過高,如pillow 7.0和torchvision 0.3...