虚指针、虚表

本文深入解析C++中虚函数与多态性的实现原理,通过实例代码详细阐述了虚函数表、虚指针的概念及其工作方式。特别关注了多继承场景下虚函数表的构造和使用。
部署运行你感兴趣的模型镜像

我们都知道虚函数是c++实现多态性的体现,虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。

我们来看一段代码:

#include<iostream>
using namespace std;
class A
{
public:
	A(int a1)
	{
		a = a1;
	}
	virtual void fun1()
	{
		cout << "基类虚函数" << endl;
	}
	virtual void fun2()
	{

	}
private:
	int a;
};
class B :public A
{
public:
	B(int a1, int b1) :A(a1)
	{
		b = b1;
	}
	virtual void fun1()
	{
		cout << "派生类虚函数" << endl;
	}
	virtual void fun2()
	{

	}
private:
	int b;
};
void print1(A *p)
{
	p->fun1();
}
void print2(A &p)
{
	p.fun1();
}
void print3(A p)
{
	p.fun1();
}
int main()
{
	A aa(10);
	B bb(20, 30);
	aa.fun1();
	bb.fun1();
	print1(&aa);
	print1(&bb);
	print2(aa);
	print2(bb);
	print3(aa);
	print3(bb);
	system("pause");
	return 0;

}

其实不难看出,程序运行结果如下:

print3(bb);并不是通过基类指针或引用来传参,所以调用的是基类的fun1()。

那么sizeof(aa)的大小是多少呢? 答案是8,包括一个整型变量a和一个虚指针。

这个时候我们就引入虚指针的概念,我们在vs编译器上调用监视窗口

我们可以看到对象aa中不仅有私有成员a,还有一个vfptr指针,这个指针就是虚指针,指向的是一个虚表,虚表中存放得是类中所有虚函数的地址。

我们可以看到,vfptr指向一个虚表,虚表中存放着fun1()和fun2()函数的地址。

如下图所示:

虚函数表最后一个元素是0.

我们可以通过下面的代码来打印虚表:

#include<iostream>
using namespace std;
class A
{
public:
	virtual void fun1()
	{
		cout << "A::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "A::fun2()" << endl;
	}
};
class B :public A
{
public:
	virtual void fun1()
	{
		cout << "B::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "B::fun2()" << endl;
	}
};
typedef void (*FUNC)();

void printVtable(int *vfptr)
{
	cout << "虚表地址:" << vfptr << endl;
	for (int i = 0; vfptr[i] != 0; i++)   //虚表最后一个元素是0
	{
		printf("第%d个虚函数地址:0x%x,->", i, vfptr[i]);
		FUNC f = (FUNC)vfptr[i];
		f(); //调用该虚函数
	}
	cout << endl;
}

int main()
{
	A a;
	B b;
	int *vfptr1 = (int*)(*(int*)(&a));
	int *vfptr2 = (int*)(*(int*)(&b));
	printVtable(vfptr1);
	printVtable(vfptr2);
	system("pause");
	return 0;
}

虚指针vfptr需要我们自己通过对象的地址来找到,虚指针的地址是对象地址的前四个字节即 (int*)(&a),然后再将其解引用即为vfptr ,但是虚指针是int * 类型,所以我们需要在把其强转成int * 即为(int *)(*(int*)(&a))。

程序运行结果:

我们可以看得出来,基类对象a有一个虚指针,指向的是基类的虚表,存放基类的虚函数地址。派生类对象b同样拥有自己的虚指针,指向的是派生类的虚表,存放的是派生类全部虚函数地址。上述情况是单继承,那么如果有一个类同时继承两个类,那么虚指针和虚表又是如何的呢? 我们继续看下一段代码:

#include<iostream>
using namespace std;
class A
{
public:
	virtual void fun1()
	{
		cout << "A::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "A::fun2()" << endl;
	}
protected:
	int a;

};
class B
{
public:
	virtual void fun1()
	{
		cout << "B::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "B::fun2()" << endl;
	}
protected:
	int b;
};
class C :public A, public B
{
public:
	virtual void fun1()
	{
		cout << "C::fun1()" << endl;
	}

	virtual void fun3()
	{
		cout << "C::fun3()" << endl;
	}
protected:
	int c;

};
typedef void(*FUNC)(); //声明一个函数指针类型FUNC

void printTable(int *vfptr)
{
	cout << "虚表地址:" << vfptr << endl;
	for (int i = 0; vfptr[i] != 0; i++)
	{
		printf("第%d个虚函数地址:%p->", i, vfptr[i]);
		FUNC f = (FUNC)(vfptr[i]);
		f();  //调用这个函数
	}
	cout << endl;
}
int main()
{
	C c;
	cout << sizeof(c) << endl;
	int *vfptr1 = (int*)(*((int*)(&c)));
	printTable(vfptr1);
	int *vfptr2 = (int*)(*(int*)(((char*)(&c)) + sizeof(A)));
	printTable(vfptr2);
	system("pause");
	return 0;

}

程序运行结果:

我们看到对象c的大小为20字节,原因是继承父类的两个整型,自己的整型变量,还有两个虚指针,8+4+8=20.。而且第二个虚表指针存放在第一个父类全部成员的下面,如图:

知道虚指针的存放位置后,我们就可以将第二个虚指针表示出来,第二个虚表指针在&c后需要加上第一个父类的大小,但是由于指针+1,加的是所指类型的大小,所以需要把&a强转为char*,或者+sizeof(A)/4,
    int *vfptr2 = (int*)(*(int*)(((char*)(&c)) + sizeof(A)));

通过程序运行结果我们可以得出结论,第一个虚指针指向一个继承父类A的虚表,里面存放的是派生类虚函数地址,需要我们注意的一点是,fun3()是派生类自己定义的虚函数,而不是重写父类的,所以它存放在第一个虚表后面。同样,第二个虚表指针也指向一个继承父类B的虚表,存放着虚函数地址(除了派生类自己定义的虚函数fun3())。我们可以通过fun2()来判断,虚指针到底指向的是继承A还是B的虚表。

 

您可能感兴趣的与本文相关的镜像

ACE-Step

ACE-Step

音乐合成
ACE-Step

ACE-Step是由中国团队阶跃星辰(StepFun)与ACE Studio联手打造的开源音乐生成模型。 它拥有3.5B参数量,支持快速高质量生成、强可控性和易于拓展的特点。 最厉害的是,它可以生成多种语言的歌曲,包括但不限于中文、英文、日文等19种语言

虚表指针(Virtual Table Pointer,简称 vptr)是面向对象编程语言(如 C++)中用于支持多态性的一种机制。它在对象的运行时行为中扮演着关键角色,尤其是在虚函数的动态绑定过程中。 ### 虚表指针的作用 虚表指针的主要作用是为每个具有虚函数的对象提供一个指向其所属类的虚函数表(VTable)的指针。虚函数表是一个数据结构,包含了该类所有虚函数的实际地址。当一个类继承另一个类并重写其虚函数时,子类会拥有自己的虚函数表,其中可能包含对父类虚函数的覆盖实现。这种机制使得通过基类指针或引用调用虚函数时,能够根据对象的实际类型调用正确的函数实现,从而实现运行时多态[^3]。 ### 虚表指针的工作原理 在对象创建时,编译器会自动为其分配一个虚表指针。这个指针指向了该对象所属类的虚函数表。虚函数表通常存储在程序的只读内存区域,并且在编译时就已经确定。虚函数表中的每一项对应一个虚函数,保存着该函数的地址。当通过虚函数调用接口调用函数时,程序首先通过虚表指针找到虚函数表,然后根据虚函数在表中的索引定位到具体的函数地址,并执行该函数[^4]。 例如,当一个类 `Base` 拥有一个虚函数 `virtual void func()`,而类 `Derived` 继承自 `Base` 并重写了 `func()` 方法,那么 `Derived` 类型的对象将拥有一个指向 `Derived` 类虚函数表的虚表指针。当通过 `Base` 类型的指针调用 `func()` 时,实际执行的是 `Derived` 类中的 `func()` 方法,因为虚表指针确保了正确的虚函数表被访问[^1]。 ### 示例代码 以下是一个简单的示例代码,展示了虚函数和虚表指针如何工作: ```cpp #include <iostream> class Base { public: virtual void func() { std::cout << "Base::func" << std::endl; } }; class Derived : public Base { public: void func() override { std::cout << "Derived::func" << std::endl; } }; int main() { Derived d; Base* basePtr = &d; basePtr->func(); // 调用 Derived::func return 0; } ``` 在这个例子中,尽管 `basePtr` 是 `Base` 类型的指针,但它指向的是 `Derived` 类型的对象。由于 `func()` 是虚函数,通过 `basePtr` 调用 `func()` 时,实际调用的是 `Derived` 类的 `func()` 方法。这是因为 `basePtr` 实际上指向了一个 `Derived` 对象,而该对象的虚表指针指向了 `Derived` 类的虚函数表,从而保证了正确的函数被调用[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值