虚表
在C++的多态机制中,使用了 virtual 关键字声明的函数称之为虚函数,每个有虚函数的类或者虚继承的子类,编译器都会为它生成一个虚拟函数表(简称:虚表,以下用 vftable表示),表中的每一个元素都指向一个虚函数的地址。
我们都知道在C++中对象生成有两个步骤:
1、分配内存空间
2、调用构造函数
多态机制发生在运行阶段,也就是对象生成阶段。那么问题就来了,**虚表(编译阶段生成)**是什么时候被写入到对象中的呢?——即,虚表指针与虚表何时关联?
1、探究虚函数表写入时机
目前有两种假设
- 假设一:虚表写入发生在在构造函数之前
- 假设二:虚表写入发生在在构造函数之后
这里设计了一段代码来探究虚表具体的写入时机
#include <iostream>
#include <cstring>
class Base //定义基类
{
public:
Base(int a) :ma(a)
{
::memset(this, 0, sizeof(this));
}
virtual void Show()
{
std::cout << "Base: ma = " << ma << std::endl;
}
protected:
int ma;
};
int main()
{
Base* pb = new Base(10);
pb->Show();
return 0;
}
实验原理:
- 在构造函数中使用
memset()
函数,把对象中的所有值赋值为0,如果虚表在构造函数之前被写入,将会是以下过程:
对象开辟空间后,构造函数调用之前,对象中 vfptr==》vftable 虚表已经被写入对象(虚表指针中存入虚表的地址)
调用构造函数后,std::memset(this, 0, sizeof(this))
将对象全部赋值为0,vfptr==》NULL
推测:如果虚表在构造函数之前被写入,那么,pb->Show()
将无法调用,程序崩溃
运行测试:(注:如果show是普通函数,调用成功,输出结果为 Base: ma = 0 )
现在,我们在分析如果虚表在构造函数之后被写入。
那么,在调用构造函数后(ma=0. vfptr==》NULL),虚表会被写入对象,即 vfptr==》vftable 。根据上面的运行结果显示,显然不是这样的。
结论:
虚表(vftable)在编译阶段生成,对象内存空间开辟以后,写入对象中的 vfptr,然后调用构造函数。即:虚表在构造函数之前写入
2、虚表的二次写入
先别急着下结论,在上面的实验中我们只测试了基类,没有测试派生类。虚表可不只有一张,在它的继承类中也存在一份虚表,因此我们接下来再做一个实验:
#include <iostream>
class Base //定义基类
{
public:
Base(int a) :ma(a)
{
std::memset(this, 0, sizeof(this));
}
virtual void Show()
{
std::cout << "Base: ma = " << ma << std::endl;
}
protected:
int ma;
};
///
// 以下为新添加部分
class Deriver : public Base //派生类
{
public:
Deriver(int b) :mb(b), Base(b) {} // 构造函数什么都不做
void Show()
{
std::cout << "Deriver: mb = " << mb << std::endl;
}
protected:
int mb;
};
int main()
{
Base* pb = new Deriver(10);
pb->Show();
return 0;
}
这次加上派生类,并且依然让基类的构造中进行 memset()
操作,让我们来看看运行结果:
过程分析:(如下图所示)由于在子类的构造函数中没有做任何事,因此第③步虚表指向并没有改变,最后正常输出Deriver::mb 的内容。
从这里也可以看出多态实现的原理。每个类只有一张虚表,类的对象共享类的虚表,并且通过虚表的二次写入机制,让每个对象的虚表指针都能准确的指向到自己类的虚表,为实现动多态提供支持。
最后在附上一张动多态原理图:
在调用虚函数时,如pb->Show()
,通过对象的vfptr 查询虚表,在虚表中保存着 Deriver::Show() 的函数入口地址,从而实现父类指针访问子类对象的方法。
也就是说,在调用成员函数时会根据调用函数的对象的类型来执行不同的函数。从而实现了去调同一函数,而产生了不同的行为,形成了多种状态的效果。
2022-6-15补充…
关于构造函数不能成为虚函数
由上述分析,在调用构造函数之前(或者说在构造函数内部语句执行之前)虚指针已经和虚表建立联系。
1. 构造函数目标明确,无需通过虚函数实现
首先,从构造函数的职能上考虑,构造函数用于初始化对象。那么什么情况下需要会需要用到一个虚函数去构造对象?
很明显,c++在编译期间就明确要创建的对象的具体类型,因此在实例化对象时自动更具所属类调用其对应的构造函数。根本没有存在虚构造函数的必要。虚函数的存在是因为编译期间没法确定具体调用的对象类型,才会有虚函数和虚函数产生。
2. 虚函数使用条件为,对象类型完整(构造调用之后)
其次,从虚表的调用条件上分析。虚函数的调用需要使用到虚表指针,通过虚表指针找到虚表。换言之,虚函数可以使用的条件是,对象已经生成(构造函数已被调用)。
对象构造骤:
1.创建空间(此时,对象不完整,只有空间,内部没有成员变量)
2.调用构造
- (默认构造函数)(此时,对象完整,各空间被划分成类成员)
- (带参数的构造函数)(此时,对象完整,各空间被划分成类成员,成员通过构造函数内的赋值语句被初始化)
因此,虚函数的调用需要得到对象内部的vptr(虚指针)。即,虚函数调用的条件是,此时对象已经完整 ==》 构造函数已经被调用过。这个问题等同于,一个已经构造好了的对象为什么还要调用构造函数?
附上其他解释:(超重要)构造函数为什么不能为虚函数?析构函数为什么要虚函数?
构造函数内部调用虚函数(可以)
由上述分析可知,在调用构造函数时,对象的虚表指针已经与虚表建立联系,因此,在构造函数内部可以改变虚表指针的指向。
那么,在构造函数内部是否可以调用类的虚函数呢?
答案是可以的,因为在构造函数内部可以看到虚表,那么就可以找到虚函数fun的地址。如下图所示:
可以看到,在构造函数中确实可以调用虚函数。下面我们测试一下在继承中,这样的调用是否仍然成立。
参考文章:https://blog.csdn.net/sumup/article/details/78174915 https://zhuanlan.zhihu.com/p/104014640
代码如下:
#include<iostream>
using namespace std;
class Base
{
public:
Base()
{
fun();
}
virtual void fun()
{
cout << "Base::fun" << endl;
}
};
class Derive : public Base
{
public:
Derive()
{
fun();
}
virtual void fun()
{
cout << "Derive::fun" << endl;
}
};
int main()
{
Derive d;
d.fun();
Base * b=&d;
b->fun();
return 0;
}
运行效果如下:
总结:由于虚函数表指针指向的是当前类的虚函数表,因此,调用的是当前类的函数。而这种实现,容易造成混淆和误解,所以,建议在构造函数和析构函数中应该避免直接或者间接调用其他虚函数。
1. 从语法上讲,调用完全没有问题。
2. 但是从效果上看,往往不能达到需要的目的。
Effective 的解释是:
派生类对象构造期间进入基类的构造函数时,对象类型变成了基类类型,而不是派生类类型。同样,进入基类析构函数时,对象也是基类类型。
所以,虚函数始终仅仅调用基类的虚函数(如果是基类调用虚函数),不能达到多态的效果,所以放在构造函数中是没有意义的,而且往往不能达到本来想要的效果。
原文链接:https://blog.csdn.net/sumup/article/details/78174915