在C++中可以通过继承基类的虚函数来实现多态,virtual就是虚函数的关键字,当编译器扫描一个类定义时,就可将该类所有标记为virtual的函数找出,然后分配一个全局数组保存他们的地址,这时就可以给保存函数地址的数组一个新称号“虚表”,就是虚函数的地址表。而且虚函数第一次声明的顺序就决定了他们在虚表中存储的顺序。
当生成虚表后,编译器在编译一个有虚函数的类(包括自己没有,但是基类有)的构造函数时,就可在函数的一开始加入代码,将虚表的地址赋给对象的前4个字节(其实就是在对象的前4个字节保存指向虚表的指针)。
class A {
public:
virtual void p1();
virtual void p2();
virtual void p3();
};
class B : public A {
public:
virtual void p2();
};
这里首先定义了一个类A,其含有3个虚函数,然后定义一个类B继承类A,他们的对象模型布局如下图。
在实例化的每一个对象中,对象中的首4个字节保存的都是指向虚表的地址。接下来就来验证该对象模型,IDE是Visual Studio 2019。
#include <iostream>
class A {
public:
virtual void work() { std::cout << "I am work" << std::endl; };
virtual void finish() { std::cout << "I am finish" << std::endl; };
};
int main()
{
A* aPtr;
aPtr = new A;
aPtr->work();
aPtr->finish();
}
在aPtr -> work()处打断点,开始调试,触发断点,调出反汇编窗口。
aPtr->work();
007428A7 8B 45 F8 mov eax,dword ptr [aPtr]
aPtr->work();
007428AA 8B 10 mov edx,dword ptr [eax]
007428AC 8B F4 mov esi,esp
007428AE 8B 4D F8 mov ecx,dword ptr [aPtr]
007428B1 8B 02 mov eax,dword ptr [edx]
007428B3 FF D0 call eax
007428B5 3B F4 cmp esi,esp
007428B7 E8 2D EA FF FF call __RTC_CheckEsp (07412E9h)
aPtr->finish();
007428BC 8B 45 F8 mov eax,dword ptr [aPtr]
007428BF 8B 10 mov edx,dword ptr [eax]
007428C1 8B F4 mov esi,esp
007428C3 8B 4D F8 mov ecx,dword ptr [aPtr]
007428C6 8B 42 04 mov eax,dword ptr [edx+4]
007428C9 FF D0 call eax
007428CB 3B F4 cmp esi,esp
007428CD E8 17 EA FF FF call __RTC_CheckEsp (07412E9h)
- 在调用work()函数之前,将对象的aPtr的地址传入eax中,再将对象的首4个字节内容赋值给edx,此时edx保存的就是指向虚表的指针;
- 然后将aPtr的地址传递给ecx,这一步就是在传递this指针;
- 最后将edx的内容赋值给eax,此时eax保存的就是第一个虚函数work()的地址,紧接着调用该函数。
类似可以分析出finish()函数的调用过程,在调用finish()过程中:eax,dword ptr [edx+4],将edx+4赋值给eax,edx+4就是虚表中第二个函数(finish())的地址。这里从反汇编的角度分析了虚函数的调用过程,接下来不采用对象来调用虚函数,而是直接利用虚表来调用虚函数。
代码如下:
#include <iostream>
class VirClass {
public:
virtual void p1() { std::cout << "hello p1" << std::endl; };
virtual void p2() { std::cout << "hello p2" << std::endl; }
};
int main()
{
VirClass vc;
void** aPtr;
void** ptrVFT; //指向虚表指针
void* pVirFunc; //指向虚函数
aPtr = (void**)&vc; //将vc的地址赋值给aPtr
ptrVFT = (void**)*aPtr; //将vc对象中指向虚表的指针赋值给ptrVFT
pVirFunc = *ptrVFT; //虚表指针指向的第一个函数是p1(),将p1()地址赋值给pVirFunc
_asm {
mov ecx, aPtr; //传递this指针
call pVirFunc; //调用p1()
};
pVirFunc = *(ptrVFT + 1); //此时pVirFunc指向p2()地址
_asm {
mov ecx, aPtr; //传递this指针
call pVirFunc; //调用p2()
};
return 0;
}
定义一个对象,先找出其指向虚表的指针,再根据该指针找出虚函数的地址,传递this指针,调用虚函数,运行结果如下,调用成功。
本文简要分析虚函数实现机制,从底层剖析了虚函数的调用,在这里或许你该明白了为什么“virtual”不能与“static”一起使用了。