封裝隱藏了類內部細節,經過繼承加虛函數的方式,咱們還能夠作到隱藏類之間的差別,這就是多態(運行時多態)。多態意味一個接口有多種行爲,今天就來講說C++的多態是怎麼實現的。ios
編譯時多態感受沒什麼好說的,編譯時直接綁定了函數地址。數組
有下面這麼一段代碼:A有兩個虛函數(virtual
關鍵字修飾的函數),B繼承了A,還有一個參數爲A*
的函數foo()
。數據結構
#include <iostream> class A { public: A(); virtual void foo(); virtual void bar(); private: int a; }; A::A() : a( 1 ) { } void A::foo() { std::cout << "A::foo()\n"; return; } void A::bar() { std::cout << "A::bar()\n"; return; } class B : public A { public: B(); virtual void foo(); virtual void bar(); private: int b ; }; B::B() : b( 2 ) { } void B::foo() { std::cout << "B::foo()\n"; return; } void B::bar() { std::cout << "B::bar()\n"; return; } void foo( A* x ) { x->foo(); x->bar(); return; }
咱們要先知道,對於虛函數的重寫,規則要求編譯器必須根據實際類型調用對應的函數,而不是像重寫普通成員函數那樣,直接調用當前類型的函數。函數
假設
bar()
是一個非虛函數,B重寫了bar()
,那麼即便x
指向B的對象,在foo()
調用x->bar()
時也仍是輸出"A::bar()"指針
這段代碼編譯成動態庫的話,編譯器就沒法肯定foo()
的入參x
指向的對象是什麼類型了(父類指針能夠指向自身類型的對象或任意子類的對象),所以編譯器就沒法直接得出foo()
和bar()
實際的函數地址,沒法完成函數調用。這中間確定發生了什麼!調試
題外話:一旦函數重寫,
A::foo()
和B::foo()
就是兩個函數,兩個地址。若是隻是單純繼承的話,以前介紹繼承的時候說過,子類是不存在B:;foo()
這個函數,而只是讓編譯器容許經過B類型的對象調用A::foo()
。code
一旦沒法天然地想通一個流程,以爲中間缺了什麼東西時,那確定是編譯器幹了什麼。所以仍是要祭出gdb
這件大殺器。對象
// 省略前面那段代碼 int main() { B* x = new B; foo( x ); return 0; }
當咱們打印x
的內容時,會發現其多了一個位於對象的首地址的_vptr.A
,它其實指向了虛函數表。繼承
(gdb) p *x $2 = {<A> = {_vptr.A = 0x400a70 <vtable for B+16>, a = 1}, b = 2}
foo()
中的x->foo()
和x->bar()
對應着以下彙編接口
# x->foo() 0x0000000000400815 <+8>: mov %rdi,-0x8(%rbp) # 將rdi中的對象地址保存到-0x8(%rbp) 中 => 0x0000000000400819 <+12>: mov -0x8(%rbp),%rax 0x000000000040081d <+16>: mov (%rax),%rax # 取對象首地址的8個字節也就是_vptr.A 0x400a70保存到rax中 0x0000000000400820 <+19>: mov (%rax),%rax # 再取出0x400a70這個地址存放的4個字節數據保存到rax中,其實就是B::foo()函數地址 0x0000000000400823 <+22>: mov -0x8(%rbp),%rdx # 將對象地址保存到rdx中 0x0000000000400827 <+26>: mov %rdx,%rdi # 將對象地址保存到rdi中,做爲虛函數foo()的參數 0x000000000040082a <+29>: callq *%rax # 調用B::foo() # x->bar() 0x000000000040082c <+31>: mov -0x8(%rbp),%rax 0x0000000000400830 <+35>: mov (%rax),%rax # 取對象首地址的8個字節也就是_vptr.A 0x400a70保存到rax中 0x0000000000400833 <+38>: add $0x8,%rax # 跳過8字節,即0x400a70+8 0x0000000000400837 <+42>: mov (%rax),%rax # 取出B::bar()的地址 0x000000000040083a <+45>: mov -0x8(%rbp),%rdx 0x000000000040083e <+49>: mov %rdx,%rdi 0x0000000000400841 <+52>: callq *%rax # 調用B::bar()
看一下0x400a70
這個地址的內容,更容易理解上面的彙編。
(gdb) x /4x 0x400a70 0x400a70 <_ZTV1B+16>: 0x0040095e 0x00000000 0x0040097c 0x00000000 (gdb) x 0x0040095e 0x40095e <B::foo()>: 0xe5894855 # 0x0040095e就是B::foo()的首地址 (gdb) x 0x0040097c 0x40097c <B::bar()>: 0xe5894855 # 0x0040097c就是B::bar()的首地址
從上面能夠看出,虛函數表相似於一個數組,其中每一個元素是該類實現的虛函數地址,利用虛函數表,就執行正確的函數了。
既然虛函數表是類數據結構裏的一部分,那它的初始化確定就是在類的構造函數裏了,讓咱們去找找。
下面是B::B()
的一部分彙編,A::A()
也相似只不過是將A的虛函數表地址賦值給_vptr.A
。
0x0000000000400941 <+19>: callq 0x4008d2 <A::A()> # 先構造父類 0x0000000000400946 <+24>: mov -0x8(%rbp),%rax 0x000000000040094a <+28>: movq $0x400a70,(%rax) # 將B的虛函數表地址0x400a70保存到對象的首地址中,即給_vptr.A賦值 0x0000000000400951 <+35>: mov -0x8(%rbp),%rax 0x0000000000400955 <+39>: movl $0x2,0xc(%rax) # 初始化列表
題外話:在更新虛函數表和初始化列表以後,才執行咱們顯式寫在
B::B()
中的代碼。
每一個類都有一個本身的虛函數表,這在編譯時就肯定了。若是子類沒有實現虛函數,虛函數表裏對應位置的函數地址就仍是父類的函數地址。
從上面咱們知道
class A { public: A(); virtual void bar(); virtual void foo(); private: int a; }; class B : public A { public: B(); virtual void bar(); virtual void foo(); int b; }; void bar( A* x ) { x->foo(); x->bar(); return; } int main() { B* b = new B; bar( b ); return 0; }
上面代碼的輸出是
B::bar() B::foo()
與預期結果恰好相反
B::foo() B::bar()
出現這樣錯誤的緣由就是在編譯main.cpp時,編譯器認爲B::foo()
是虛函數表的第二個元素,但實際在liba.so中B::foo()
是虛函數表中的第一個元素。
強烈建議虛函數的聲明順序必須保持一致,並且增長虛函數時,只在尾部增長
瞭解C++的多態實現後,對於理解其餘語言的多態實現也是有益處的,本質都應當是在經過一箇中間結構肯定實際函數的地址。
除了以上內容外,還有
gcc version 4.8.5