C/C++雜記:虛函數的實現的基本原理

1. 概述

簡單地說,每個含有虛函數(不管是其自己的,仍是繼承而來的)的類都至少有一個與之對應的虛函數表,其中存放着該類全部的虛函數對應的函數指針。例:
html

其中:git

  • B的虛函數表中存放着B::foo和B::bar兩個函數指針。
  • D的虛函數表中存放的既有繼承自B的虛函數B::foo,又有重寫(override)了基類虛函數B::bar的D::bar,還有新增的虛函數D::quz。

提示:爲了描述方便,本文在探討對象內存佈局時,將忽略內存對齊對佈局的影響。github

2. 虛函數表構造過程

從編譯器的角度來講,B的虛函數表很好構造,D的虛函數表構造過程相對複雜。下面給出了構造D的虛函數表的一種方式(僅供參考):
ide

提示:該過程是由編譯器完成的,所以也能夠說:虛函數替換過程發生在編譯時。函數

3. 虛函數調用過程

如下面的程序爲例:
佈局

編譯器只知道pb是B*類型的指針,並不知道它指向的具體對象類型 :pb可能指向的是B的對象,也可能指向的是D的對象。指針

但對於「pb->bar()」,編譯時可以肯定的是:此處operator->的另外一個參數是B::bar(由於pb是B*類型的,編譯器認爲bar是B::bar),而B::bar和D::bar在各自虛函數表中的偏移位置是相等的。htm

不管pb指向哪一種類型的對象,只要可以肯定被調函數在虛函數中的偏移值,待運行時,可以肯定具體類型,並能找到相應vptr了,就能找出真正應該調用的函數。對象

提示:本人曾在「C/C++雜記:深刻理解數據成員指針、函數成員指針」一文中提到:虛函數指針中的ptr部分爲虛函數表中的偏移值(以字節爲單位)加1。blog

B::bar是一個虛函數指針, 它的ptr部份內容爲9,它在B的虛函數表中的偏移值爲8(8+1=9)。

當程序執行到「pb->bar()」時,已經可以判斷pb指向的具體類型了:

  • 若是pb指向B的對象,能夠獲取到B對象的vptr,加上偏移值8((char*)vptr + 8),能夠找到B::bar。
  • 若是pb指向D的對象,能夠獲取到D對象的vptr,加上偏移值8((char*)vptr + 8) ,能夠找到D::bar。
  • 若是pb指向其它類型對象...同理...

4. 多重繼承

當一個類繼承多個類,且多個基類都有虛函數時,子類對象中將包含多個虛函數表的指針(即多個vptr),例:

其中:D自身的虛函數與B基類共用了同一個虛函數表,所以也稱B爲D的主基類(primary base class)。

虛函數替換過程與前面描述相似,只是多了一個虛函數表,多了一次拷貝和替換的過程。

虛函數的調用過程,與前面描述基本相似,區別在於基類指針指向的位置可能不是派生類對象的起始位置,以以下面的程序爲例:

5. 菱形繼承

本文不討論菱形繼承的情形,我的以爲:菱形繼承的複雜度遠大於它的使用價值,這也是C++讓人又愛又恨的緣由之一。

若是想要深刻研究,能夠參考:Itanium C++ ABI

相關文章
相關標籤/搜索