专业编程基础技术教程

网站首页 > 基础教程 正文

C++虚函数表的实现原理 c++虚函数写法

ccvgpt 2024-11-12 09:54:11 基础教程 442 ℃

我们知道在C++中,通过虚函数,可以实现用父类指针指向其子类的实例,然后通过该指针可以调用实际子类的方法。这样让父类指针具有了“多种形态”, 而且这种可变性不是编译期确定的,而是在运行过程中确定的调用关系。这也是一种泛型技术,属于动态多态,它的实现机制离不开虚函数表的功能。

一、什么是虚函数表

C++虚函数表的实现原理 c++虚函数写法

如果一个类定义了虚函数,那么就会生成一个虚函数表(Virtual Table),包括继承它的子类, 也同样会具有该表。这样的类对象会添加一个隐藏成员,隐藏成员保存了一个指针,这个指针叫虚表指针(vptr),它指向一个虚函数表。

虚函数表就像一个数组,表中有许多的槽(slot),每个槽中存放的是一个虚函数的地址,可以理解为数组里存放着指向每个虚函数的指针。即:每个类使用一个虚函数表,每个类对象用一个虚表指针。

C++的编译器可以保证虚表指针存在于对象实例中最前面的位置,这是为了保证取到虚函数表的有最高的性能,即使在有多层继承或是多重继承的情况下。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同,子类独有的虚函数放在后面。

二、代码实践验证

假设我们有这样一个类, 代码如下:

#include <iostream>
using namespace std;

typedef void (*func_ptr)(void);

class DemoAA {
public:
	DemoAA(int num) { m_num = num; cout << "Constructor AA" << endl; }
	~DemoAA() { cout << "Destructor AA" << endl; }
	void SayHello() { cout << "Hello, world. m_num=" << m_num << endl; }

	virtual void TestOne() { cout << "Call TestOne" << endl; }
	virtual void TestTwo() { cout << "Call TestTwo" << endl; }
	virtual void TestThree() { cout << "Call TestThree" << endl; }

protected:
	int m_num;
};

class DemoBB : public DemoAA{
public:
	DemoBB(int num) : DemoAA(num) { cout << "Constructor BB" << endl; }
	~DemoBB() { cout << "Destructor BB" << endl; }
	
	virtual void TestOne() { cout << "DemoBB: Call TestOne" << endl; }
	virtual void TestFour() { cout << "DemoBB: Call TestFour" << endl; }
};

void TestAddrCall() {
	DemoAA obj(123);
	obj.SayHello();

	long* vptr = (long*)(&obj);
	cout << "vptr addr: " << vptr << endl;
	func_ptr pfunc = nullptr;
	long* vbtl = (long*)(*vptr);

	pfunc = (func_ptr) * (vbtl);
	pfunc(); // TestOne
	pfunc = (func_ptr) * (vbtl + 1);
	pfunc(); // TestTwo
	pfunc = (func_ptr) * (vbtl + 2);
	pfunc(); // TestThree
}

void TestSubclass() {
	DemoBB obj(456);
	DemoAA* pbase = &obj;
	pbase->SayHello();

	pbase->TestOne();
	pbase->TestTwo();
	pbase->TestOne();
}

int main() {
	TestAddrCall();
	TestSubclass();
	return 0;
}

其中(long*)(&obj)就是虚函数表地址(也是对象实例的地址),(long*)*(long*)(&obj)就是第一个函数地址, 在程序中取出对象obj的地址,根据对象的布局可以得出就是虚表的地址,根据这个地址可以把虚表的第一个内存单元的内容取出,然后强制转换成一个函数指针,利用这个函数指针来访问虚函数。又因为虚表是连续的,利用每次+1可以来访问下一个内存单元。运行结果如下:

三、总结

1、虚函数表记录了当前所属类的最新的所有虚函数,而每个类的对象实例的开始存储了虚函数表的指针,通过这个指针可以找到对应的虚函数表及其函数地址;

2、无论是基类的指针,还是子类的指针,最终指向的都是具体的实例对象。它们能够向上或向下转型,主要原因还是在于首地址存储了虚标指针,这个就有点像C里的union结构体,同一片内存可以有不同的解析方法。但是安全性(特别是向下转型)就需要自己来保证了。

3、如果调用的是虚函数,会有一层虚函数表查询的过程,多了一次寻址转换过程;但是普通函数就不在此列,是直接通过符号表映射到代码地址的,因此虚函数调用会有一丢丢(几乎可以忽略)的性能开销。

最近发表
标签列表