虛擬函式是C++語言實現執行時多型的唯一手段,因此掌握C++虛擬函式也成為C++程式設計師是否合格的試金石。csdn網友所發的一篇博文《VC虛擬函式佈局引發的問題》 從彙編角度分析了物件虛擬函式表的構,以及C++指標或者引用是如何利用這個表來實現執行時多型。
誠然,C++虛擬函式的結構會因編譯器不同而異,但所使用的原理是一樣的。為此,本文使用linux平臺下的g++編譯器,試圖從彙編的層面上分析虛擬函式表的結構,以及如何利用它來實現執行時多型。
組合語言是難讀的,特別是對一些沒有彙編基礎的朋友,因此,本文將彙編翻譯成相應的C語言,以方便讀者分析問題。
1. 程式碼
為了方便表述問題,本文選取只有虛擬函式的兩個類,當然,還有它的建構函式,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Base { public: virtual void f() { } virtual void g() { } }; class Derive : public Base { public: virtual void f() {} }; int main() { Derive d; Base *pb; pb = &d; pb->f(); return 0; } |
2. 兩個類的虛擬函式表(vtable)
使用g++ –Wall –S test.cpp命令,可以將上述的C++程式碼生成它相應的彙編程式碼。
1 2 3 4 5 6 7 8 9 |
ZTV4Base: .long 0 .long _ZTI4Base .long _ZN4Base1fEv .long _ZN4Base1gEv .weak _ZTS6Derive .section .rodata._ZTS6Derive,"aG",@progbits,_ZTS6Derive,comdat .type _ZTS6Derive, <a href="http://www.jobbole.com/members/anduo1989">@object</a> .size _ZTS6Derive, 8 |
_ZTV4Base是一個資料符號,它的命名規則是根據g++的內部規則來命名的,如果你想檢視它真正表示C++的符號名,可使用c++filt命令來轉換,例如:
1 2 |
[lyt@t468 ~]$ c++filt _ZTV4Base vtable for Base |
_ZTV4Base符號(或者變數)可看作為一個陣列,它的第一項是0,第二項_ZIT4Base是關於Base的型別資訊,這與typeid有關。為方便討論,我們略去此二項資料。 因此Base類的vtable的結構,翻譯成相應的C語言定義如下:
1 2 3 4 |
unsigned long Base_vtable[] = { &Base::f(), &Base::g(), }; |
而Derive的更是類似,只有稍為有點不同:
1 2 3 4 5 6 7 8 9 10 |
ZTV6Derive: .long 0 .long _ZTI6Derive .long _ZN6Derive1fEv .long _ZN4Base1gEv .weak _ZTV4Base .section .rodata._ZTV4Base,"aG",@progbits,_ZTV4Base,comdat .align 8 .type _ZTV4Base, <a href="http://www.jobbole.com/members/anduo1989">@object</a> .size _ZTV4Base, 16 |
相應的C語言定義如下:
1 2 3 4 |
unsigned long Derive_vtable[] = { &Derive::f(), &Base::g(), }; |
從上面兩個類的vtable可以看到,Derive的vtable中的第一項重寫了Base類vtable的第一項。只要子類重寫了基類的虛擬函式,那麼子類vtable相應的項就會更改父類的vtable表項。 這一過程是編譯器自動處理的,並且每個的類的vtable內容都放在資料段裡面。
3. 誰讓物件與 vtable 綁到一起
上述程式碼只是定義了每個類的vtable的內容,但我們知道,帶有虛擬函式的物件在它內部都有一個vtable指標,指向這個vtable,那麼是何時指定的呢? 只要看看建構函式的彙編程式碼,就一目瞭然了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
_ZN4BaseC1Ev: .LFB6: .cfi_startproc .cfi_personality 0x0,__gxx_personality_v0 pushl %ebp .cfi_def_cfa_offset 8 movl %esp, %ebp .cfi_offset 5, -8 .cfi_def_cfa_register 5 movl 8(%ebp), %eax movl $_ZTV4Base+8, (%eax) popl %ebp ret .cfi_endproc |
Base::Base()函式的編譯程式碼如下:
ZN4BaseC1Ev這個符號是C++函式Base::Base() 的內部符號名,可使用c++flit將它還原。C++裡的class,可以定義資料成員,函式成員兩種。但轉化到彙編層面時,每個物件裡面真正存放的是資料成員,以及虛擬函式表。
在上面的Base類中,由於沒有資料成員,因此它只有一個vtable指標。故Base類的定義,可以寫成如下相應的C程式碼:
1 2 3 |
struct Base { unsigned long **vtable; } |
建構函式中最關鍵的兩句是:
1 2 |
movl 8(%ebp), %eax movl $_ZTV4Base+8, (%eax) |
$_ZTV4Base+8 就是Base類的虛擬函式表的開始位置,因此,建構函式對應的C程式碼如下:
1 2 3 4 |
void Base::Base(struct Base *this) { this->vtable = &Base_vtable; } |
同樣地,Derive類的建構函式如下:
1 2 3 4 5 6 7 |
struct Derive { unsigned long **vtable; }; void Derive::Derive(struct Derive *this) { this->vtable = &Derive_vtable; } |
4. 實現執行時多型的最關鍵一步
在造構函式裡面設定好的vtable的值,顯然,同一型別所有物件內的vtable值都是一樣的,並且永遠不會改變。下面是main函式生成的彙編程式碼,它展示了C++如何利用vtable來實現執行時多型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
.globl main .type main, @function main: .LFB3: .cfi_startproc .cfi_personality 0x0,__gxx_personality_v0 pushl %ebp .cfi_def_cfa_offset 8 movl %esp, %ebp .cfi_offset 5, -8 .cfi_def_cfa_register 5 andl $-16, %esp subl $32, %esp leal 24(%esp), %eax movl %eax, (%esp) call _ZN6DeriveC1Ev leal 24(%esp), %eax movl %eax, 28(%esp) movl 28(%esp), %eax movl (%eax), %eax movl (%eax), %edx movl 28(%esp), %eax movl %eax, (%esp) call *%edx movl $0, %eax leave ret .cfi_endproc |
1 2 |
andl $-16, %esp subl $32, %esp |
這兩句是為區域性變數d和bp在堆疊上分配空間,也即如下的語句:
1 2 3 4 5 6 7 |
Derive d; Base *pb; leal 24(%esp), %eax movl %eax, (%esp) call _ZN6DeriveC1Ev |
esp+24是變數d的首地址,先將它壓到堆疊上,然後呼叫d的建構函式,相應翻譯成C語言則如下:
1 2 3 4 |
Derive::Dervice(&d); leal 24(%esp), %eax movl %eax, 28(%esp) |
這裡其實是將&d的值賦給pb,也即:
1 |
pb = &d; |
最關鍵的程式碼是下面這一段:
1 2 3 4 5 6 |
movl 28(%esp), %eax movl (%eax), %eax movl (%eax), %edx movl 28(%esp), %eax movl %eax, (%esp) call *%edx |
翻譯成C語言也就傳神的那句:
1 |
pb->vtable[0](bp); |
編譯器會記住f虛擬函式放在vtable的第0項,這是編譯時資訊。
5. 小結
這裡省略了很多關於編譯器和C++的細枝未節,是出於討論方便用的需要。從上面的編譯程式碼可以看到以下資訊:
1.每個類都有各有的vtable結構,編譯會正確填寫它們的虛擬函式表
2. 物件在建構函式時,設定vtable值為該類的虛擬函式表
3.在指標或者引用時呼叫虛擬函式,是通過object->vtable加上虛擬函式的offset來實現的。
當然這僅僅是g++的實現方式,它和VC++的略有不同,但原理是一樣的。