多态是C++的一个重要特性,本文将深入介绍C++编译器是如何实现C++多态特性。
先看一段涉及多态的代码。
#include <iostream>
using namespace std;
class BaseClass
{
public:
virtual ~BaseClass() {}
virtual void VirtualFunction1() {
cout << "I am base virtual function 1" << endl;
}
virtual void VirtualFunction2() {
cout << "I am base virtual function 2" << endl;
}
void NonVirtualFunction() {
cout << "I am base non virtual function" << endl;
}
private:
int m_nBaseField = 0;
};
class SubClass : public BaseClass
{
public:
virtual ~SubClass() {}
virtual void VirtualFunction1() override {
cout << "I am subclass function 1, override base class" << endl;
}
void NonVirtualFunction() {
cout << "I am subclass non virtual function" << endl;
}
private:
int m_nSubClassField = 0;
};
int main()
{
BaseClass baseClass;
SubClass subClass;
BaseClass* pBaseClass = &baseClass;
pBaseClass->VirtualFunction1();
pBaseClass = &subClass;
pBaseClass->VirtualFunction1();
pBaseClass->NonVirtualFunction();
return 0;
}
如上示例代码,定义基类BaseClass,BaseClass定义了虚析构函数、虚函数VirtualFunction1、虚函数VirtualFunction2、非虚函数NonVirtualFunction。
然后定义子类SubClass继承于BaseClass,覆盖重写BaseClass的虚析构函数、虚函数VirtualFunction1、函数NonVirtualFunction,未覆盖重写BaseClass的虚函数VirtualFunction2。
Main函数定义了两个对象baseClass和subClass,定义了BaseClass指针pBaseClass,先让pBaseClass指向baseClass对象,调用VirtualFunction1,然后让pBaseClass指向subClass,调用VirtualFunction1和NonVirtualFunction。
下面是输出结果:
从输出结果知:
第一,pBaseClass指向baseClass对象,调用VirtualFunction1时,是调用BaseClass::VirtualFunction1。当pBaseClass指向subClass对象,调用VirtualFunction1时,是调用SubClass::VirtualFunction1。也就是说,pBaseClass同样调用VirtualFunction1,但是会因为pBaseClass指向的对象不同,而产生不同的行为(调用不同的方法),这就是多态。
第二,pBaseClass指向subClass对象,调用NonVirtualFunction时,调用的是BaseClass::NonVirtualFunction,而不是SubClass::NonVirtualFunction。
接下来将重点解释下这两个结论,首先让我们看下baseClass和subClass这两个对象的内存布局。
从上图可以发现,baseClass有一个数据成员__vfptr,__vfptr是为了实现多态,编译器在最前面隐式添加的一个指针,叫虚函数表指针,其指向虚函数表。baseClass虚函数表有三个元素,对应BaseClass定义的三个虚函数(虚构函数、VirtualFunction1、VirtualFunction2),这三个虚函数的地址是编译阶段编译器计算生成。
subClass继承于baseClass,内存布局上首先是baseClass的__vfptr和数据成员m_nBaseField,然后是SubClass定义的数据成员m_nSubClassField。subClass的__vfptr的值与baseClass的__vfptr的值不同,说明它们指向的虚函数表是不一样的。由于subClass没有新增虚函数,所以虚函数表也是三个元素与BaseClass的虚函数个数相同。但是,subClass覆盖重写了baseClass的虚析构函数和VirtualFunction1,所以第一和第二个元素的值不同,指向的是SubClass对应的虚函数。由于VirtualFunction2未被覆盖重写,所以第三个元素的值与BaseClass的虚函数表的第三个元素的值相同,都指向BaseClass::VirtualFunction2。
NonVirtualFunction不是虚函数,所以没有在虚函数表中。
接下来,我将结合虚函数表和汇编代码对上面的结论进行解释说明。
指针pBaseClass指向subClass对象,为什么调用NonVirtualFunction时不是调用subClass的NonVirtualFunction方法?这是因为pBaseClass是BaseClass*类型,NonVirtualFunction方法不是虚函数,所以它的调用地址在编译阶段,编译器就已经决定调用CBaseClass::NonVirtualFunction方法,我们从如下汇编代码知道call指令的地址固定为0A61519h。
指针pBaseClass指向subClass对象,为什么调用VirtualFunction1方法时,是调用subClass::VirtualFunction1呢?让我们先看下汇编代码。
首先,将指针pBaseClass的值(subClass对象的地址)放入寄存器eax。
其次,读取eax所指向的内存的值放入寄存器edx,也就是pBaseClass->__vfptr的值(虚函数表指针的值)。
再次,读取edx+4所指向的内存的值放入eax,也就是pBaseClass->__vfptr[1],也就是VirtualFunction1函数的地址。
最后,call eax,也就是call VirtualFunction1。
可见,调用虚函数和调用非虚函数有很大区别,调用虚函数时是在运行时期决定的,而调用非虚函数是在编译时期就决定了。调用虚函数时会查询虚函数表对应函数的地址,如果子类覆盖重写基类的虚函数,子类的虚函数表对应函数的地址就会被编译器更新为子类的函数。子类对象的地址可以赋值给基类(BaseClass* pBaseClass = &subClass),由于__vfptr数据成员是从基类继承,所以pBaseClass虽然指向SubClass对象,但是可以访问到__vfptr,而此时__vfptr指向SubClass的虚函数表,自然就调用SubClass的方法。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有