本篇內容主要講解“c++中的虛函數”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“c++中的虛函數”吧!
匯編語言是難讀的,特別是對一些沒有匯編基礎的朋友,因此,本文將匯編翻譯成相應的C語言,以方便讀者分析問題。
1. 代碼
為了方便表述問題,本文選取只有虛函數的兩個類,當然,還有它的構造函數,如下:
[cpp] view
plaincopyprint?
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++代碼生成它相應的匯編代碼。
[cpp] view
plaincopyprint?
_ZTV4Base:
.long 0
.long _ZTI4Base
.long _ZN4Base1fEv
.long _ZN4Base1gEv
.weak _ZTS6Derive
.section .rodata._ZTS6Derive,"aG",@progbits,_ZTS6Derive,comdat
.type _ZTS6Derive, @object
.size _ZTS6Derive, 8
_ZTV4Base是一個數據符號,它的命名規則是根據g++的內部規則來命名的,如果你想查看它真正表示C++的符號名,可使用c++filt命令來轉換,例如:
[lyt@t468 ~]$ c++filt _ZTV4Base
vtable for Base
_ZTV4Base符號(或者變量)可看作為一個數組,它的第一項是0,第二項_ZIT4Base是關于Base的類型信息,這與typeid有關。為方便討論,我們略去此二項數據。 因此Base類的vtable的結構,翻譯成相應的C語言定義如下:
[cpp] view
plaincopyprint?
unsigned long Base_vtable[] = {
&Base::f(),
&Base::g(),
};
而Derive的更是類似,只有稍為有點不同:
[cpp] view
plaincopyprint?
_ZTV6Derive:
.long 0
.long _ZTI6Derive
.long _ZN6Derive1fEv
.long _ZN4Base1gEv
.weak _ZTV4Base
.section .rodata._ZTV4Base,"aG",@progbits,_ZTV4Base,comdat
.align 8
.type _ZTV4Base, @object
.size _ZTV4Base, 16
相應的C語言定義如下:
[cpp] view
plaincopyprint?
unsigned long Derive_vtable[] = {
&Derive::f(),
&Base::g(),
};
從上面兩個類的vtable可以看到,Derive的vtable中的第一項重寫了Base類vtable的第一項。只要子類重寫了基類的虛函數,那么子類vtable相應的項就會更改父類的vtable表項。 這一過程是編譯器自動處理的,并且每個的類的vtable內容都放在數據段里面。
3. 誰讓對象與vtable綁到一起
上述代碼只是定義了每個類的vtable的內容,但我們知道,帶有虛函數的對象在它內部都有一個vtable指針,指向這個vtable,那么是何時指定的呢? 只要看看構造函數的匯編代碼,就一目了然了:
Base::Base()函數的編譯代碼如下:
[cpp] view
plaincopyprint?
_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
ZN4BaseC1Ev這個符號是C++函數Base::Base() 的內部符號名,可使用c++flit將它還原。C++里的class,可以定義數據成員,函數成員兩種。但轉化到匯編層面時,每個對象里面真正存放的是數據成員,以及虛函數表。
在上面的Base類中,由于沒有數據成員,因此它只有一個vtable指針。故Base類的定義,可以寫成如下相應的C代碼:
[cpp] view
plaincopyprint?
struct Base {
unsigned long **vtable;
}
構造函數中最關鍵的兩句是:
movl 8(%ebp), %eax
movl $_ZTV4Base+8, (%eax)
$_ZTV4Base+8 就是Base類的虛函數表的開始位置,因此,構造函數對應的C代碼如下:
[cpp] view
plaincopyprint?
void Base::Base(struct Base *this)
{
this->vtable = &Base_vtable;
}
同樣地,Derive類的構造函數如下:
[cpp] view
plaincopyprint?
struct Derive {
unsigned long **vtable;
};
void Derive::Derive(struct Derive *this)
{
this->vtable = &Derive_vtable;
}
4. 實現運行時多態的最關鍵一步
在造構函數里面設置好的vtable的值,顯然,同一類型所有對象內的vtable值都是一樣的,并且永遠不會改變。下面是main函數生成的匯編代碼,它展示了C++如何利用vtable來實現運行時多態。
[cpp] view
plaincopyprint?
.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
andl $-16, %esp
subl $32, %esp
這兩句是為局部變量d和bp在堆棧上分配空間,也即如下的語句:
Derive d;
Base *pb;
leal 24(%esp), %eax
movl %eax, (%esp)
call _ZN6DeriveC1Ev
esp+24是變量d的首地址,先將它壓到堆棧上,然后調用d的構造函數,相應翻譯成C語言則如下:
Derive::Dervice(&d);
leal 24(%esp), %eax
movl %eax, 28(%esp)
這里其實是將&d的值賦給pb,也即:
pb = &d;
最關鍵的代碼是下面這一段:
[cpp] view
plaincopyprint?
movl 28(%esp), %eax
movl (%eax), %eax
movl (%eax), %edx
movl 28(%esp), %eax
movl %eax, (%esp)
call *%edx
翻譯成C語言也就傳神的那句:
pb->vtable[0](bp);
編譯器會記住f虛函數放在vtable的第0項,這是編譯時信息。
5. 小結
這里省略了很多關于編譯器和C++的細枝未節,是出于討論方便用的需要。從上面的編譯代碼可以看到以下信息:
1.每個類都有各有的vtable結構,編譯會正確填寫它們的虛函數表
2. 對象在構造函數時,設置vtable值為該類的虛函數表
3.在指針或者引用時調用虛函數,是通過object->vtable加上虛函數的offset來實現的。
當然這僅僅是g++的實現方式,它和VC++的略有不同,但原理是一樣的。
到此,相信大家對“c++中的虛函數”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。