C++中和虛函數(Virtual Function)密切相關的概念是“動態綁定”(Dynamic Binding),與之相對的概念是“靜態綁定”(Static Binding)。所謂“靜態綁定”,是指在編譯時就能確定函數調用語句和實際執行的函數;而“動態綁定”則是——對於同一個函數調用,編譯時並不能確定具體調用的函數,直到執行時才能決定。
靜態綁定
#include <iostream>
using namespace std;
class Base
{
public:
void show() { cout << "I am a Base object!\n"; }
};
class Derived : public Base
{
public:
void show() { cout << "I am a Derived object!\n"; }
};
int main(int argc, char *argv[])
{
Base *pBase = new Base();
pBase->show();
pBase = new Derived();
pBase->show();
return 0;
}
I am a Base object!
I am a Base object!
這個例子說明,通過基類指針(或引用)調用一般的成員函數(Member Function)時(編譯器)採取的都是靜態綁定。靜態綁定的實現
class Simple
{
int data;
public:
void setData(int d) { data = d; }
int getData() { return data; }
};
// 僞代碼, 說明編譯器對一個成員函數定義的展開形式
void setData(Simple* this, int d)
{
this->data = d;
}
int getData(Simple* this)
{
return this->data;
}
靜態綁定的實現大體如以上代碼所示,即在成員函數的參數列表最前面插入一個指針(this指針),成員函數內部所有對成員變量的訪問都將由此this指針尋址;這樣即可實現語言層面上不同對象調用相同成員函數,訪問各自的數據拷貝。
動態綁定
只需將上面靜態綁定的例子上的show函數加上virtual,此時雖然Derived::show沒有聲明爲virtual,但它也是virtual(由繼承而來的virtual屬性,標準就是這樣規定的)。#include <iostream>
using namespace std;
class Base
{
public:
virtual void show() { cout << "I am a Base object!\n"; }
};
class Derived : public Base
{
public:
void show() { cout << "I am a Derived object!\n"; }
};
int main(int argc, char *argv[])
{
Base *pBase = new Base();
pBase->show();
pBase = new Derived();
pBase->show();
return 0;
}
這樣,程序將輸出:I am a Base object!
I am a Derived object!
一個更激進的例子是——爲了證明是在執行時期,可以讓pBase所指向的對象由用戶輸入決定:
void testRTTI()
{
int n = 0;
while(cin >> n) { // 遇到EOF字符結束,Windows控制檯上Ctrl+Z可輸入EOF,Linux Ctrl+D
if( n % 2 )
pBase = new Base();
else
pBase = new Derived();
pBase->show();
delete pBase;
}
}
例如,一組輸入輸出(黑體是輸入):1
I am a Base object!
2
I am a Derived object!
3
I am a Base object!
動態綁定的實現
pBase->_vptr[1](pBase);
VC2008跟蹤
1.構造函數初始化vptr
Base *pBase = new Base();
00B6152E push 4
00B61530 call operator new (0B6120Dh) ; 申請內存
00B61535 add esp,4 ; 清除壓入的4
00B61538 mov dword ptr [ebp-0E0h],eax ; 保存到棧上臨時變量(暫計爲ret)
00B6153E cmp dword ptr [ebp-0E0h],0 ; ret和0比較
00B61545 je main+4Ah (0B6155Ah) ; 如果ret==0 ,不執行構造函數
00B61547 mov ecx,dword ptr [ebp-0E0h] ; ret存入 ecx(this指針)
00B6154D call Base::Base (0B61136h) ; 調用Base::Base
看到了call Base::Base,繼續:
00B61136 Base::Base (0B61630h)
Base::Base:
00B61630 push ebp
00B61631 mov ebp,esp
00B61633 sub esp,0CCh ; 棧上開闢空間(棧向下生長)
00B61639 push ebx
00B6163A push esi
00B6163B push edi
00B6163C push ecx ; 最後一個push
00B6163D lea edi,[ebp-0CCh] ; \
00B61643 mov ecx,33h ; 初始化剛開闢的空間
00B61648 mov eax,0CCCCCCCCh ; (Debug版 特有代碼)
00B6164D rep stos dword ptr es:[edi] ; /
00B6164F pop ecx ; 最近一次 push的是 ecx,而這期間esp沒有被修改;而上次push之前ecx也沒有被修改,所以ecx還是原來的值(main寫入的this指針)
00B61650 mov dword ptr [ebp-8],ecx
00B61653 mov eax,dword ptr [this] ; 取出this指針
00B61656 mov dword ptr [eax],offset Base::`vftable' (0B67804h) ; 初始化__vptr,讓它指向vftable
00B6165C mov eax,dword ptr [this] ; 取出this指針,寫入eax
00B6165F pop edi
00B61660 pop esi
00B61661 pop ebx
00B61662 mov esp,ebp
00B61664 pop ebp
00B61665 ret
可以看到 dword ptr [eax],offset Base::`vftable' (0B67804h) 就是用來設置__vptr的,因爲代碼中的Base,Derived沒有定義其他數據成員,所以this指針所指的dword(4B)就是__vptr。
2.virtual table裏保存什麼?
通過VC2008的內存窗口(調試->窗口->內存)可以查看vftable的內容:可以看到vftable的第一個成員是:0x00B61118,第二個是0(表示結束,沒有後續的),它很可能是一個函數的地址,在反彙編窗口輸入改地址能看到:
由此可以看到,VC2008的vftable和<模型>一書描述的並不相同,vftable第一個slot並沒有存放type_info,而是直接存了Base::show;因此這裏vftable只有一個slot。
3.調用virtual function的實際代碼
再來看看從調用Base::Base()到pBase->show();的代碼:00B6154D call Base::Base (0B61136h)
00B61552 mov dword ptr [ebp-0E8h],eax ; eax 裏存的是this指針,這相當於保存函數返回值到臨時變量
00B61558 jmp main+54h (0B61564h)
00B6155A mov dword ptr [ebp-0E8h],0 ; 這行代碼被忽略
00B61564 mov eax,dword ptr [ebp-0E8h] ;
00B6156A mov dword ptr [pBase],eax ; 將this保存到pBase裏; 相當於 pBase = eax
pBase->show();
00B6156D mov eax,dword ptr [pBase] ; 再取出; 相當於 eax = pBase
00B61570 mov edx,dword ptr [eax] ; 這裏很關鍵,Base::show()在vftable的slot 0中,所以直接取eax所指向的dword(4字節)
00B61572 mov esi,esp
00B61574 mov ecx,dword ptr [pBase] ; ecx 傳入 this 指針
00B61577 mov eax,dword ptr [edx] ; 取出virtual function實際地址
00B61579 call eax ; 調用
至此,一個完整的virtual function的執行已經梳理清楚了。第二次,pBase = new Derived(); 後 pBase->show(); 的流程與此完全類似,這裏不再羅列;唯一不同的是,Derived::Derived()裏初始化__vptr的值會是offset Derived::`vftable'。