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的實例。

?

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/531169.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/531169.shtml
英文地址,請注明出處:http://en.pswp.cn/news/531169.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【網絡攻防】精通C語言的黑客才是真正的黑客!

精通C語言的黑客才是真正的黑客 黑客界&#xff0c;有兩樣重要的課程&#xff0c;一是計算機的本質&#xff0c;二是編譯原理。相對于匯編等底層語言&#xff0c;它簡單&#xff1b;相對于其它高級語言&#xff0c;它更為接近計算機&#xff1b;同樣它對黑客的兩大課程很有幫助…

我兩小時學完指針,你學會數組/指針與函數需要多久?

數組與函數&#xff1a; 這段函數中 函數的參數是數組&#xff0c;注意數組作為函數參數時&#xff0c;數組名和數組元素個數時分別傳遞的。 指針與函數&#xff1a; 這段函數中的參數是指針變量&#xff0c;傳入的是數組的數組名或者首元素的地址&#xff0c;然后用引領操作…

【C語言】C語言結構解析

C 程序結構 在我們學習 C 語言的基本構建塊之前&#xff0c;讓我們先來看看一個最小的 C 程序結構&#xff0c;在接下來的章節中可以以此作為參考。 喜歡編程的或者想學習編程的朋友可以加一下我的C語言編程交流群815393895&#xff0c;謝謝大家的支持 C Hello World 實例 C…

每一個程序員都是自學成才?

為什么CS學位并非是成為開發人員的唯一路徑&#xff0c;因為每個開發人員在工作于他們的項目時學到了很多很多。 學習編程并不難 有興趣學習或者已經在學習C語言的朋友可以加一下我的編程交流群815393895 除了CS學位&#xff0c;還有很多成為程序員的方法。如果你正行進在一…

不妨問問自己,學習C語言是為了什么?

1、首先是雞湯&#xff0c;也就是為什么要學C語言。你可以先問自己&#xff0c;為什么我要學C語言&#xff1f;是為了應付考試&#xff0c;還是為了應聘&#xff0c;還是為了提高自己的編程能力。我想說的是&#xff0c;如果你打算以后長期從事計算機方面的工作&#xff0c;你就…

C語言靈魂——算法!

程序的靈魂—算法 一個程序應包括&#xff1a; 對數據的描述。在程序中要指定數據的類型和數據的組織形式&#xff0c;即數據結構&#xff08;data structure&#xff09;。 對操作的描述。即操作步驟&#xff0c;也就是算法&#xff08;algorithm&#xff09;。 Nikiklaus Wir…

為什么會有那么多人放棄編程?

為什么許多編程人員最后都放棄了呢&#xff1f;小編幫你理清下原因&#xff0c;主要有以下幾個 為什么這么多人選擇放棄學習編程&#xff1f; 加班加點是家常便飯 在軟件行業不加班的公司不是很多&#xff0c;區別就是加班強度。為什么程序員需要加這么多班&#xff0c;軟件是一…

C語言基礎知識梳理

C語言是面向過程的&#xff0c;而C&#xff0b;&#xff0b;是面向對象的 C和C的區別&#xff1a; C是一個結構化語言&#xff0c;它的重點在于算法和數據結構。C程序的設計首要考慮的是如何通過一個過程&#xff0c;對輸入&#xff08;或環境條件&#xff09;進行運算處理得到…

很多人大一就開始學習C語言,你真的學到了嗎?

好多人大一就學了C語言&#xff0c;但你有沒有感覺學的不深&#xff0c;不扎實。或者說越學越迷茫&#xff0c;不知道它能做什么 我相信&#xff0c;這可能是很多朋友的問題&#xff0c;其實&#xff0c;這是很多初學者都會踩到的一個坑&#xff01;C語言本身是一門很簡單的語言…

C語言發展歷史,C語言特點,C語言利于弊,入門須知三招

C語言是面向過程的&#xff0c;而C&#xff0b;&#xff0b;是面向對象的 這些是C/C能做的 服務器開發工程師、人工智能、云計算工程師、信息安全&#xff08;黑客反黑客&#xff09;、大數據 、數據平臺、嵌入式工程師、流媒體服務器、數據控解、圖像處理、音頻視頻開發工程…

程序員怎么看待C語言?最偉大?最落后?

一&#xff0c;前言 對我來說&#xff0c;C語言應該可以算得上是世界上最偉大的編程語言。全中國口氣最大的程序員&#xff0c;業界稱之為“垠神”&#xff0c;曾經發過文章吐槽過業界各種主流的編程語言&#xff08;對Java&#xff0c;的Python稍微寬容一些&#xff09;&…

如何學習C語言?就是這么簡單粗暴!

C語言是面向過程的&#xff0c;而C&#xff0b;&#xff0b;是面向對象的。 C和C的區別&#xff1a; C是一個結構化語言&#xff0c;它的重點在于算法和數據結構。C程序的設計首要考慮的是如何通過一個過程&#xff0c;對輸入&#xff08;或環境條件&#xff09;進行運算處理…

C/C++對編程的重要性!其他編程語言都是弟弟!

C語言是面向過程的&#xff0c;而C&#xff0b;&#xff0b;是面向對象的 C和C的區別&#xff1a; C是一個結構化語言&#xff0c;它的重點在于算法和數據結構。C程序的設計首要考慮的是如何通過一個過程&#xff0c;對輸入&#xff08;或環境條件&#xff09;進行運算處理得…

C語言其實不難,只是你沒有找對方法!

C語言是面向過程的&#xff0c;而C&#xff0b;&#xff0b;是面向對象的 C和C的區別&#xff1a; C是一個結構化語言&#xff0c;它的重點在于算法和數據結構。C程序的設計首要考慮的是如何通過一個過程&#xff0c;對輸入&#xff08;或環境條件&#xff09;進行運算處理得…

想學好C語言?先把基礎打好再說吧!

C語言是面向過程的&#xff0c;而C&#xff0b;&#xff0b;是面向對象的 C和C的區別&#xff1a; C是一個結構化語言&#xff0c;它的重點在于算法和數據結構。C程序的設計首要考慮的是如何通過一個過程&#xff0c;對輸入&#xff08;或環境條件&#xff09;進行運算處理得…

C語言新手最常見的問題!你在這里跌倒過嗎?

C語言是面向過程的&#xff0c;而C&#xff0b;&#xff0b;是面向對象的 C和C的區別&#xff1a; C是一個結構化語言&#xff0c;它的重點在于算法和數據結構。C程序的設計首要考慮的是如何通過一個過程&#xff0c;對輸入&#xff08;或環境條件&#xff09;進行運算處理得…

用C語言做出你自己的七夕表白程序!

C語言是面向過程的&#xff0c;而C&#xff0b;&#xff0b;是面向對象的 C和C的區別&#xff1a; C是一個結構化語言&#xff0c;它的重點在于算法和數據結構。C程序的設計首要考慮的是如何通過一個過程&#xff0c;對輸入&#xff08;或環境條件&#xff09;進行運算處理得…

C語言/C++編程的起源與能力學習

C語言是面向過程的&#xff0c;而C&#xff0b;&#xff0b;是面向對象的 C和C的區別&#xff1a; C是一個結構化語言&#xff0c;它的重點在于算法和數據結構。C程序的設計首要考慮的是如何通過一個過程&#xff0c;對輸入&#xff08;或環境條件&#xff09;進行運算處理得…

Windows下設置自動關機的命令指示符

&#xff08;請先看置頂博文&#xff09;https://blog.csdn.net/GenuineMonster/article/details/104495419 Windows定時操作命令&#xff1a;superR&#xff0c;輸入&#xff1a;&#xff1f;&#xff1f;&#xff1a;&#xff1f;&#xff1f;shutdown -s ”??“代表時間…

Win7系統轉到Win10系統的裝機方法

Windows 10的版本最好是家庭版。 1.進入PC的BIOS。進入方法與PC品牌有關&#xff0c;可根據對應的品牌查尋進入方法。 2.將Boot List Option 選為UEFI。有些電腦在切換UEFI后即可顯示列表&#xff0c;因此可跳過第三步&#xff0c;反之則繼續第三步的操作。 3.重啟PC&#x…