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; }
#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!
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; } }例如,一组输入输出(黑体是输入):
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。
可以看到vftable的第一个成员是:0x00B61118,第二个是0(表示结束,没有后续的),它很可能是一个函数的地址,在反汇编窗口输入改地址能看到:
由此可以看到,VC2008的vftable和<模型>一书描述的并不相同,vftable第一个slot并没有存放type_info,而是直接存了Base::show;因此这里vftable只有一个slot。
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'。