1. 程式人生 > >C++ 虛擬函式和虛繼承解析

C++ 虛擬函式和虛繼承解析

本文針對C++裡的虛擬函式,虛繼承表現和原理進行一些簡單分析,有不對的地方請指出。下面都是以VC2008編譯器對這兩種機制內部實現為例。

有喜歡或者想學習C/C++的朋友加一下我的C/C++交流群815393895。謝謝大家的支援

虛擬函式

以下是百度百科對於虛擬函式的解釋:

定義:在某基類中宣告為 virtual 並在一個或多個派生類中被重新定 義的成員函式[1]

語法:virtual 函式返回型別 函式名(引數表) { 函式體 }

用途實現多型性,通過指向派生類的基類指標,訪問派生類中同名覆蓋成員函式

函式宣告和定義和普通的類成員函式一樣,只是在返回值之前加入了關鍵字“virtual”宣告為虛擬函式。而虛擬函式是實現多型的重要手段,意思是隻有對虛擬函式的呼叫才能動態決定呼叫哪一個函式,這是相對於普通成員函式而言的,普通的成員函式在編譯階段就能確定呼叫哪一個函式。舉個栗子:

#include <stdio.h>class A {public:    void fn() { printf("fn in A\n"); }    virtual void v_fn() { printf("virtual fn in A\n"); }
};class B : public A {public:    void fn() { printf("fn in B\n"); }    virtual void v_fn() { printf("virtual fn in B\n"); }
};int main() {
    A *a = new B();
    a->fn();
    a->v_fn();    return 0;
}

基類A有兩個成員函式fn和v_fn,派生類B繼承自基類A,同樣實現了兩個函式,然後在main函式中用A的指標指向B的例項(向上轉型,也是實現多型的必要手段),然後分別呼叫fn和v_fn函式。結果是“fn in A”和”virtual fn in B”。這是因為fn是普通成員函式,它是通過類A的指標呼叫的,所以在編譯的時候就確定了呼叫A的fn函式。而v_fn是虛擬函式,編譯時不能確定,而是在執行時再通過一些機制來呼叫指標所指向的例項(B的例項)中的v_fn函式。假如派生類B中沒有實現(完全一樣,不是過載)v_fn這個函式,那麼依然會呼叫基類類A中的v_fn;如果它實現了,就可以說派生類B覆蓋了基類A中的v_fn這個虛擬函式。這就是虛擬函式的表現和使用,只有通過虛擬函式,才能實現面嚮物件語言中的多型性。

以上只是虛擬函式的表現和用途,下面來探討它的實現機制。在此之前,先來看一個問題,還是以上的程式碼,基類A的大小為多少,也就是“printf(“%d\n”, sizeof(A));”的輸出會是多少呢?A中一個成員變數都沒有,有人可能會說是0。額,0是絕對錯誤的,因為在C++中,即時是空類,它的大小也為1,這是另外的話題,不在本文討論。當然1也是不對的,實際結果是4(32位系統),4剛好是一個int,一個指標(32位)的大小,派生類B的大小同樣為4。這四個位元組和實現多型,虛擬函式的機制有著很重要的關係。

其實用VC2008除錯上面程式碼的時候,就會發現指標a所指向的實力中有一個成員常量(const),它的名字叫做vftable,全稱大概叫做virtual function table(虛擬函式表)。它實際指向了一個數組,數組裡面儲存的是一系列函式指標,而上面的程式中,這個表只有一項,它就是派生類B中的v_fn函式入口地址。假如我們用一個A的指標指向一個A的例項呢?它同樣有一個vftable,而它指向的表中也只有一項,這項儲存的基類的v_fn函式入口地址。這用程式碼表示,就類似於下面這樣:

void* vftable_of_A[] = {
    A::v_fn,
    ...
};class A {    const void* vftable = vftable_of_A;    virtual void v_fn() {}
};void* vftable_of_B[] = {
    B::v_fn,
    ...
};class B {    const void *vftable = vftable_of_B;    vritual void v_fn() {}
};

上面vftable的型別之所以用void*表示,實際上一個類中所有虛擬函式的地址都被放到這個表中,不同虛擬函式對應的函式指標型別不盡相同,所以這個表用C++的型別不好表述,但是在機器級裡都是入口地址,即一個32位的數字(32位系統),等到呼叫時,因為編譯器預先知道了函式的引數型別,返回值等,可以自動做好處理。

這樣我們就能更好的理解虛擬函式和多型了。第一個程式碼中,a指標雖然是A*型別的,但是它卻呼叫了B中的v_fn,因為不管是A類,還是A的基類,都會有一個變數vftable,它指向的虛擬函式表中儲存了正確的v_fn入口。所以a->v_fn()實際做的工作就是從a指向的例項中取出vftable的值,然後找到虛擬函式表,再從表中去的v_fn的入口,進行呼叫。不管a是指向A的例項,還是指向B的例項,a->fn()所做的步驟都是上面說的一樣,只是A的例項和B的例項有著不同的虛擬函式表,虛擬函式表裡也儲存著可能不同的虛擬函式入口,所以最終將進入不同的函式呼叫中。通過表來達到不用判斷型別,亦可實現多型的作用。還有一點指的提醒的是,因為虛擬函式表是一個常量表,在編譯時,編譯器會自動生成,並且不會改變,所以如果有多個B類的例項,每個例項中都會有一個vftable指標,但是它們指向的是同一個虛擬函式表。

上面一段中說到了,A和B的例項有著不同的虛擬函式表,但是虛擬函式表中只是可能儲存著不同的v_fn,那是因為C++允許派生類不覆蓋基類中的虛擬函式,意思就是假如派生類B中沒有實現v_fn這個函式(不是過載),那麼B的例項的虛擬函式表會儲存著基類A中v_fn的入口地址。也就是說B類不實現v_fn函式,但是它同樣提供了這個介面,實際上是呼叫基類A中的v_fn。假如某個類只是一個抽象類,抽象出一些列介面,但是又不能實現這些介面,而要有派生類來實現,那麼就可以把這些介面宣告為純虛擬函式,包含有純虛擬函式的類稱為抽象類。純虛擬函式是一類特殊的虛擬函式,它的宣告方式如下:

class A {public:

  virtual 返回值 函式名(引數表)= 0;

};

在虛擬函式宣告方式後加一個“=0”,並且不提供實現。抽象類不允許例項化(這樣做編譯器會報錯,因為有成員函式沒有實現,編譯器不知道怎麼呼叫)。純虛擬函式的實現機制和虛擬函式類似,只是要求派生類類必須自己實現一個(也可以不實現,但是派生類也會是個抽象類,不能例項化)。

順帶提一下,java中的每一個成員函式都可以以理解為C++中的virtual函式,不用顯式宣告都可以實現過載,多型。而java的介面類似於C++中的抽象類,需要實現裡面的介面。

虛繼承

C++支援多重繼承,這和現實生活很類似,任何一個物體都不可能單一的屬於某一個型別。就像馬,第一想到的就是它派生自動物這個基類,但是它在某系地方可不可以說也派生自交通工具這一個基類呢?所以C++的多重繼承很有用,但是又引入了一個問題(專業術語叫做菱形繼承?)。動物和交通工具都是從最根本的基類——“事物”繼承而來,事物包含了兩個最基本的屬性,體積和質量。那麼動物和交通工具都儲存了基類成員變數——體積和質量的副本。而馬有繼承了這兩個類,那麼馬就有兩份體積和質量,這是不合理的,編譯器無法確定使用哪一個,所以就會報錯。JAVA中不存在這樣的問題,因為JAVA不允許多重繼承,它只可能實現多個介面,而接口裡面只包含一些函式宣告不包含成員變數,所以也不存在這樣的問題。

這個問題用具體程式碼表述如下所示:

class A {public:    int a;
};class B : public A {
};class C : public A {
};class D : public B, public C {
};int main() {
    D d;
    d.a = 1;    return 0;
}

這個程式碼會報錯,因為d中儲存了兩份A的副本,即有兩個成員變數a,一般不會報錯,但是一旦對D中的a使用,就會報一個“對a的訪問不明確”。虛繼承就可以解決這個問題。在探討虛擬函式之前,先來一個sizeof的問題。

#include <stdio.h>class A {public:    int a;
};class B : virtual public A {
};int main() {    printf("%d\n", sizeof(B));    return 0;
}

B的大小是?首先回答0的是絕對錯的,理由我之前都說了。1也是錯的,不解釋。4也是錯的,如果B不是虛繼承自A的,那麼4就是對的。正確答案是8,B虛繼承A了之後,比預想中的多了4個位元組,這是怎麼回事呢?這個通過除錯是看不出來的,因為看不到類似於vftable的成員變數(實際上編譯器生成了一個類似的東西,但是除錯時看不到,但是在觀察反彙編的時候,可以見到vbtable的字樣,應該是virtual base table的意思)。

虛繼承的提出就是為了解決多重繼承時,可能會儲存兩份副本的問題,也就是說用了虛繼承就只保留了一份副本,但是這個副本是被多重繼承的基類所共享的,該怎麼實現這個機制呢?編譯器會在派生類B的例項中儲存一個A的例項,然後在B中加入一個變數,這個變數是A的例項在實際B例項中的偏移量,實際上B中並不直接儲存offset的值,而是儲存的一個指標,這個指標指向一個表vbtable,vbtable表中儲存著所有虛繼承的基類在例項中的offset值,多個B的例項共享這個表,每個例項有個單獨的指標指向這個表,這樣就很好理解為什麼多了4個位元組了。用程式碼表示就像下面這樣。

class A {public:
    ...
};int vbtable_of_B[] = {
  offset(B::_a),
    ...
};class B :virtual public A{private:    const int* vbtable = vbtable_of_B;
    A _a;
};

每一個A的虛派生類,都會有自己的vbtable表,這個派生類的所有例項共享這個表,然後每個例項各自儲存了一個指向vbtable表的指標。假如還有一個類C虛繼承了A,那麼編譯器就會為它自動生成一個vbtable_of_C的表,然後C的例項都會有一個指向這個vbtable表的指標。

假如有多級的虛繼承會發生什麼情況,就像下面這段程式碼一樣:

#include <stdio.h>class A {public:    int a;
};class B : virtual public A {public:
  int b;
};class C : virtual public B {
};int main() {    printf("%d\n", sizeof(C));    return 0; 
}

程式執行的結果是16,按照之前的理論,大概會這麼想。基類A裡有1個變數,4個位元組。B類虛繼承了A,所以它有一個A的副本和一個vbtable,還有自己的一個變數,那就是12位元組。然後C類又虛繼承了B類,那麼它有一個B的副本,一個vbtable,16位元組。但實際上通過除錯和反彙編發現,C中儲存分別儲存了A和B的副本(不包括B類的vbtable),8位元組。然後有一個vbtable指標,4位元組,表裡麵包含了A副本和B副本的偏移量。最後還有一個無用的4位元組(?),一共16位元組。不僅是這樣,每經過一層的虛繼承,便會多出4位元組。這個多出來的四位元組在反彙編中沒發現實際用途,所以這個有待探討,不管是編譯器不夠智慧,還是有待其它作用,虛繼承和多重繼承都應該謹慎使用。

還是以上面的例子,假如C類是直接繼承B類,而不是使用虛繼承,那麼C類的大小為12位元組。它裡面是直接儲存了A和B的副本(不包含B的vbtable),然後還有一個自己的vbtable指標,所以一共12位元組,沒有了上一段所說的最後的4個位元組。

但是如果想下面一種繼承,會是什麼情況?

#include <stdio.h>class A {public:    int a;
};class B : virtual public A {
};class C : virtual public A {
};class D : public B, public C{
};int main() {    printf("%d\n", sizeof(D));    return 0; 
}

D從B,C類派生出來,而B和C又同時虛繼承了A。輸出的結構是12,實際除錯反彙編的時候發現,D中繼承了B和C的vbtable,這就是8位元組,而同時還儲存了一個A的副本,4位元組,總共12位元組。它和上面的多重虛繼承例子裡的12位元組是不一樣的。之前一個例子中只有一個vbtable,一個A的例項,末尾還有一個未知的4位元組。而這個例子中是有兩個僅挨著的vbtable(都有效)和一個A的例項。

相關推薦

C++ 虛擬函式繼承解析

本文針對C++裡的虛擬函式,虛繼承表現和原理進行一些簡單分析,有不對的地方請指出。下面都是以VC2008編譯器對這兩種機制內部實現為例。 有喜歡或者想學習C/C++的朋友加一下我的C/C++交流群815393895。謝謝大家的支援 虛擬函式 以下是百度百科對於虛擬函式的

c++虛擬函式繼承

1.多繼承可能會出現奇葩現象,多個同樣的變數,導致子類不知道呼叫的那個變數來自哪個父類。2.如果一個外部方法的引數是父類,那麼即使傳了一個子類,在方法中呼叫這個類的內部方法,不管你傳入的是子類還是父類,都會強行給你呼叫父類的方法。因為編譯器認為這樣是安全的,這個方法一定在父類

C++:虛擬函式繼承

1:虛解構函式主要是為了解決釋放父類的指標,同時釋放子類的指標,防止記憶體的洩露;例如 Father p = new Son();delete P;P= NULL;如果父類沒有解構函式則會造成記憶體洩露

C++虛擬函式繼承、物件記憶體模型

虛擬函式的工作原理      虛擬函式的實現要求物件攜帶額外的資訊,這些資訊用於在執行時確定該物件應該呼叫哪一個虛擬函式。典型情況下,這一資訊具有一種被稱為 vptr(virtual table pointer,虛擬函式表指標)的指標的形式。vptr 指向一個被稱為 vtb

轉載:虛擬函式繼承的記憶體分佈

最近做題的時候經常遇到虛擬函式和虛繼承的記憶體分佈情況問題,自己也有點生疏了,所以,趕緊在這裡回憶補充一下! 先從一個類的記憶體分佈情況開始看起: 環境:VS2012 class A { int a; public: A() :a(1) {} void fu

c++單繼承與多繼承(包含虛擬函式繼承的對比)

先來個概念分析題: class Person { public: void Show() { cout<<"Person::"<<_name&l

C++類物件大小的計算(三)含有虛擬函式繼承類大小計算

在前一篇文章《C++類物件大小的計算(二)含有虛擬函式類大小計算》的基礎上,我們來討論如果包含虛擬函式時,對類物件大小的影響。 以下記憶體測試環境為Win7+VS2012,作業系統為32位 六、當類中含有虛繼承情況時     1. 派生類物件中會新增一個指標,該指標指向虛繼

C++學習之多型篇(虛擬函式解構函式的實現原理--虛擬函式表)

通過下面的程式碼來說明: #include <iostream> #include <stdlib.h> #include <string> using namespace std; /**  *  定義動物類:Animal  *  成員

C++學習:虛擬函式,純虛擬函式(virtual),繼承,解構函式

C++學習:虛擬函式,虛繼承,純虛擬函式(virtual)虛解構函式 虛擬函式 純虛擬函式 虛解構函式 虛繼承 簡介 在java這種高階語言中,有abstract和interface這兩個關鍵字.代表的是抽象類和介面,但是在C++這門語言中

虛擬函式、純虛擬函式繼承

實現多型。 虛擬函式都是動態繫結,繫結的是動態型別,所對應的函式或屬性依賴於物件的動態型別,發生在執行期; 虛擬函式 類裡如果聲明瞭虛擬函式,這個函式是實現的,哪怕是空實現,它的作用就是為了能讓這個函式在它的子類裡面可以被覆蓋,這樣的話,這樣編譯器就可以使用後期繫結來達到多型

C++虛擬函式虛擬函式抽象類

1 虛擬函式     虛擬函式是類的一種特殊成員函式,主要是為實現C++的多型特性引入。     虛擬函式之所以“虛”是因為呼叫的虛擬函式不是在靜態編譯(靜態編聯)時確定,而是在執行時通過動態編聯確定的。     多型核心理念即是通過基類訪問派生的子類,通常情況是藉助基類指

一個例子徹底搞懂c++虛擬函式虛擬函式

學習C++的多型性,你必然聽過虛擬函式的概念,你必然知道有關她的種種語法,但你未必瞭解她為什麼要那樣做,未必瞭解她種種行為背後的所思所想。深知你不想在流於表面語法上的蜻蜓點水似是而非,今天我們就一起來揭開擋在你和虛擬函式(女神)之間的這一層窗戶紙。 首先,我們要

虛擬函式繼承

虛擬函式 多型時實現了介面複用 c++中實現多型有兩種,一種是靜多型,另一種是動多型 靜多型通過過載完成 靜多型又叫靜態繫結或早繫結,在編譯期間確定函式入口地址 動多型通過虛擬函式完成 動多型又稱動態繫結或晚邦定(在執行時動態確定函式入口地址,call的是暫存器) 虛擬函式關鍵字virtua

虛擬函式繼承記憶體分析

封裝、繼承、多型是面嚮物件語言的三大特性,熟悉C++的人對此應該不會有太多異議。C語言提供的struct,頂多算得上對資料的簡單封裝,而C++的引入把struct“升級”為class,使得面向物件的概念更加強大。繼承機制解決了物件複用的問題,然而多重繼承又會產生成員衝突的問題,虛繼承在我看來更像是一

C++虛擬函式虛擬函式的注意事項

1)純虛擬函式宣告如下:virtual void function()=0;純虛擬函式一定沒有定義,用來規範派生類的行為,即介面。包含純虛擬函式的類是抽象類,抽象類不能定義例項,但是可以宣告指向該抽象類的具體類的指標或者引用; 2)虛擬函式宣告:virtual void f

c++ 虛擬函式虛擬函式

靜態多型和動態多型–虛擬函式、純虛擬函式 靜態多型:程式在編譯階段就可以確定呼叫哪個函式。這種情況叫做靜態多型。比如過載 動態多型:在執行期間才可以確定最終呼叫的函式。需要通過虛擬函式+封裝+繼承實現。 虛擬函式 1、虛擬函式都必須有定義 2、虛擬函式一般用在繼

虛擬函式繼承總結

一. 虛擬函式 虛擬函式的使用是為了實現c++中的多型,即同一介面,不同實現,可用父類指標呼叫子類成員函式。虛擬函式是基於虛擬函式表(virtual table,v-table for short)來實現的。 每個包含虛擬函式的類都將

淺談多型中的虛擬函式

需要實現多型必不可少的就是虛擬函式,類的成員函式前加virtual關鍵字,這個成員函式就是虛擬函式;例如: class T { public: virtual void fun() { cout<<"fun()"<<endl; }

c++虛擬函式抽象類

虛擬函式是c++實現多型的一種機制,基類的虛擬函式可以有子類的函式重新定義,從而實現函式功能的靈活性。 虛擬函式又分為:普通虛擬函式和純虛擬函式。 純虛擬函式是一種特殊的虛擬函式,它的一般格式如下:     class <類名>    {        virtu

C++虛擬函式表在繼承繼承中的差別

下面的程式碼在gcc和VC中的結果 #include <cstdio> class A { public: virtual void funcaa() { printf("class A %s\n",__func__); } }; class AA:virtual pu