標籤:

c++為什麼需要虛函數表?

c++ virtual 定義虛函數後 ,子類重寫了該方法 ,由於c++編譯器的命名規則 ,這兩個方法最後的 符號不一樣 ,編譯器就能找到這兩個方法 ,為什麼還需要虛函數表呢 ?

以下是我定義的兩個類 ,Animal 為父類, Dog為子類 ,virtual void voice() 為虛方法 ,轉化為彙編後 ,發現這兩個方法的符號不同 。

這樣用虛函數表對於多態的實現有什麼意義呢?

===============

同樣的例子,如果不用虛函數表,又可以怎麼實現呢?


struct Base
{
virtual void foo() { cout &<&< "foo"; } virtual ~Base() {} }; struct Derived : Base { virtual void foo() override { cout &<&< "Derived"; } }; int main() { int n; cin &>&> n;
Base* b;

if( n &> 10 )
{
b = new Derived();
}
else
{
b = new Base();
}
b-&>foo();

delete b;
}

你說這種情況,就算有確定的符號,編譯器怎麼預先知道b-&>foo()調用的是哪個函數


你想一想,變數是有類型的,但是它們作為數據存在內存里,你怎麼確定這個指針/引用指向的對象是 Dog 而不是 Cat 呢?顯然你需要有一個變數來記錄它的類型。

而在 C++ 里,我們要調用虛函數,Cat 類里有 voice,Dog 里有 voice,計算機怎麼知道你要調用的是哪個 voice 呢?於是就用到了虛函數表,裡面存著這個對象虛函數的地址。這樣只要查表就可以了。


本來c++也沒有欽定一定要有虛函數表,實現是可以不通過虛函數表實現多態的,另外通過實例而不是引用或指針調用該類的虛函數時實際上是像非虛函數那樣靜態地調用的,這時候肯定需要自己的符號。


通過簡單地增加另外一個間接層就可以解決軟體的任何問題。

比如運行時多態,解決方法就是間接調用。

至於這個為什麼是虛表而不是扁平化的binding,我可以告訴你,性能。

比如,你可以不設計虛表,而是為每個類增加一個指針數組欄位作為間接層,就可以實現多態,但你的性能不會有官方的好。


也不是一定要虛函數表,圖形框架裡面,MFC用了宏做消息映射,QT用了信號槽,這兩種都有些動態綁定的意思。虛函數表可能對大多數應用場景是個各方面比較均衡的方案。

強調虛函數表這些技術,通常意味著在設計上傾向於構建多層繼承的體系來映射問題域中的概念。近年來,c++社區似乎更傾向用扁平方案,結合bindind之類的模板技術來實現動態綁定。

2016-05-23更新(上述答覆2016-05-22發布,今天收穫一贊,就再寫點嘍^_^):

=====================================

個人對上述趨勢的理解:在C++誕生後的一個很長的時期內,硬體的性能還不能支持其他語言開發大型系統,這個時代,C++逐漸發展成一種大而全的編程語言,各種編程範式都是基於這個時代背景出現的,用陳碩的話說,就是那個時代的CPU時間比程序員時間貴。

所以我們看到大概十年前的c++教材會強調多層繼承、多態這樣一些概念。

MFC選擇那樣一個框架,應該有向性能妥協的考慮,放棄了優雅的虛函數表的方案。

由於摩爾定律一直在起作用,今天的硬體性能遠非當年可比,還是借用陳碩的說法,變成了程序員時間比CPU時間貴,此時,就會出現分工(非常像政治經濟學上將全能人和社會分工之間的關係)

由於今天C++已經後退至一些關鍵點上面,負責解決大量計算的效率問題,開發過程中需要面對的客觀世界的模型大大簡化,就會想扁平方案轉變。

安利一篇我寫的文章,可以看到技術發展的路徑和社會發展的路徑有相似之處:

文萱逃課秀7:魅惑與卻魅 - 老王的文章 - 知乎專欄


基類指針可以指向基類對象,也可以指向派生類對象,實現多態的時候,是用基類指針指向一個派生類對象。

在同一個基類指針上調用同一個虛函數,會因為基類指針實際指向的對象不同而調用不同的函數,這就是所謂多態,你自己考慮下要怎麼做到這一點?基類指針只不過是個普通指針變數而已,所以靠這個指針是無法知道究竟應該調用哪個函數的。從頭到尾只有指向的對象不同,明確這一點很重要,這說明只有對象本身可以確定多態時究竟應該調用哪個函數,即所謂動態綁定,虛函數表即是存在於對象中,基類對象的虛函數表和派生類是不同的,不同的虛函數表最終指引到不同的函數調用。

至於你說的函數簽名符號問題,和多態的實現無關。


我來說說我自己的理解吧, 之前覺得多態中 ,傳遞的this指針是繼承類 ,現在才知道是編譯器將繼承類強轉為基類 ,只不過保留了虛函數表 。故猜測編譯器在對待虛函數的調用時會有自己的方式(即去虛函數表中查找對應的方法)。


否則你需要的繼承多態怎麼實現哩,樓主偷偷的學好虛表的數據結構 偷偷地做修改 埋個點留點炸彈 嘿嘿


要實現多態,

動態綁定的時候,你又不知道需要用到哪個。

另外動態綁定速度好像略差於靜態

因為要查表


所以你到底知道不知道什麼是多態


There are two terms that are commonly used when object-oriented programming languages are discussed: early binding and late binding. Relative to C++, these terms refer to events that occur at compile time and events that occur at run time, respectively.

Early binding means that a function call is resolved at compile time. Late binding means that a function call is resolved at run time.《C++ from the Ground up》

虛函數表是用在運行時多態。假如Animal有Sleep虛函數,Dog繼承重寫。那麼你某個函數參數是動物的引用或指針,函數里調用參數的Sleep函數,如果沒有虛函數表,編譯時已經知道要運行動物的Sleep函數,沒有多態了。有虛函數表可以看你傳進的是哪一類對象的實例或其地址,執行該類的方法來實現運行時多態。


顯然,有了虛函數表之後你才不會在Base*裡面調用到Base::method而是根據原本的類型調用Derived::method


推薦閱讀:

現在編譯器處理那種「用換行代替分號」的語句邏輯是怎麼做的?
程序如何根據變數名在內存中找到存放這個變數的地址?
如何減小GacLib生成的可執行文件大小?
OCaml pattern有哪些葵花寶典?

TAG:C | 編譯器 |