C++ 面试必问:深入理解虚函数表

点击蓝字

31f477efddaa543bede5580458928fbb.png

关注我们

深入理解C++ 虚函数表

C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数

Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()
 
b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()

这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。

比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。

本文将详细介绍虚函数表的实现及其内存布局。

虚函数表概述

虚函数表是指在每个包含虚函数的类中都存在着一个函数地址的数组。当我们用父类的指针来操作一个子类的时候,这张虚函数表指明了实际所应该调用的函数。

C++的编译器保证虚函数表的指针存在于对象实例中最前面的位置,这样通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

按照上面的说法,来看一个实际的例子:

#include <iostream>

using namespace std;

class Base {
public:
	virtual void f() { cout << "f()" << endl; }
	virtual void g() { cout << "g()" << endl; }
	virtual void h() { cout << "h()" << endl; }
};

int main()
{
	Base t;
	(     ((void(*)())*((int*)(*((int*)&t)) + 0))   )     ();
	(     ((void(*)())*((int*)(*((int*)&t)) + 1))   )     ();
	(	  ((void(*)())*((int*)(*((int*)&t)) + 2))	)     ();
	return 0;
}

经过VS2017,x86测试:

043cf222bc66f88cbd32e413be0ecd76.png

d0831c5f26590a60c5f375e69b794815.png


我们成功地通过实例对象的地址,得到了对象所有的类函数。

de57d6ccb861c059a06eb6194dd20de6.png
main定义Base类对象t,把&b转成int *,取得虚函数表的地址vtptr就是:(int*)(&t),然后再解引用并强转成int * 得到第一个虚函数的地址,也就是Base::f()即(int*)(*((int*)&t)),那么,第二个虚函数g()的地址就是(int*)(*((int*)&t)) + 1,依次类推。

单继承下的虚函数表

派生类未覆盖基类虚函数

下面我们来看下派生类没有覆盖基类虚函数的情况,其中Base类延用上一节的定义。从图中可看出虚函数表中依照声明顺序先放基类的虚函数地址,再放派生类的虚函数地址。

390fa46f1e8e6fe7cd816e5ed8ca1add.png


可以看到下面几点:

1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。

测试代码:

#include <iostream>

using namespace std;

class Base {
public:
	virtual void f() { cout << "f()" << endl; }
	virtual void g() { cout << "g()" << endl; }
	virtual void h() { cout << "h()" << endl; }
};

class Devired :public Base{
public:
	virtual void x() { cout << "x()" << endl; }
};

int main()
{
	Devired t;
	(((void(*)())   *((int*)(*((int*)&t)))))   ();

	(((void(*)())*((int*)(*((int*)&t)) + 1)))     ();

	(((void(*)())*((int*)(*((int*)&t)) + 2)))     ();
	//(((void(*)())*((int*)(*((int*)&t)) + 3)))     ();

	return 0;
}

测试效果:

83499b09944e972e85ff940f72345af0.png


派生类覆盖基类虚函数

再来看一下派生类覆盖了基类的虚函数的情形,可见:

  • 虚表中派生类覆盖的虚函数的地址被放在了基类相应的函数原来的位置 (显然的,不然虚函数失去意义)

  • 派生类没有覆盖的虚函数延用基类的

测试代码:

#include <iostream>

using namespace std;

class Base {
public:
	virtual void f() { cout << "f()" << endl; }
	virtual void g() { cout << "g()" << endl; }
	virtual void h() { cout << "h()" << endl; }
};

class Derive :public Base{
public:
	virtual void x() { cout << "x()" << endl; }
	virtual void f() { cout << "Derive::f()" << endl; }
};

int main()
{
	Derive t;
	(((void(*)())   *((int*)(*((int*)&t)))))   ();

	(((void(*)())*((int*)(*((int*)&t)) + 1)))     ();

	(((void(*)())*((int*)(*((int*)&t)) + 2)))     ();
	//(((void(*)())*((int*)(*((int*)&t)) + 3)))     ();

	return 0;
}

测试效果:

daa89ddf6db182b0a78035c20f0b1b08.png

ba045c654172839f4d43d4bbb5418531.png

多继承下的虚函数表

无虚函数覆盖

如果是多重继承的话,问题就变得稍微复杂一丢丢,主要有几点:

  • 每个基类都有自己的虚函数表

  • 派生类的虚函数地址存依照声明顺序放在第一个基类的虚表最后(这点和单继承无虚函数覆盖相同),具体见下图所示:

93f41a317f8ddd6308e12d95b064e056.png


测试代码

#include <iostream>
class Base
{
public:
	Base(int mem1 = 1, int mem2 = 2) : m_iMem1(mem1), m_iMem2(mem2) { ; }

	virtual void vfunc1() { std::cout << "In vfunc1()" << std::endl; }
	virtual void vfunc2() { std::cout << "In vfunc2()" << std::endl; }
	virtual void vfunc3() { std::cout << "In vfunc3()" << std::endl; }

private:
	int m_iMem1;
	int m_iMem2;
};

class Base2
{
public:
	Base2(int mem = 3) : m_iBase2Mem(mem) { ; }
	virtual void vBase2func1() { std::cout << "In Base2 vfunc1()" << std::endl; }
	virtual void vBase2func2() { std::cout << "In Base2 vfunc2()" << std::endl; }

private:
	int m_iBase2Mem;
};

class Base3
{
public:
	Base3(int mem = 4) : m_iBase3Mem(mem) { ; }
	virtual void vBase3func1() { std::cout << "In Base3 vfunc1()" << std::endl; }
	virtual void vBase3func2() { std::cout << "In Base3 vfunc2()" << std::endl; }

private:
	int m_iBase3Mem;
};

class Devired : public Base, public Base2, public Base3
{
public:
	Devired(int mem = 7) : m_iMem1(mem) { ; }
	virtual void vdfunc1() { std::cout << "In Devired vdfunc1()" << std::endl; }

private:
	int m_iMem1;
};

int main()
{
	// Test_3
	Devired d;
	int *dAddress = (int*)&d;
	typedef void(*FUNC)();

	/* 1. 获取对象的内存布局信息 */
	// 虚表地址一
	int *vtptr1 = (int*)*(dAddress + 0);
	int basemem1 = (int)*(dAddress + 1);
	int basemem2 = (int)*(dAddress + 2);

	int *vtpttr2 = (int*)*(dAddress + 3);
	int base2mem = (int)*(dAddress + 4);

	int *vtptr3 = (int*)*(dAddress + 5);
	int base3mem = (int)*(dAddress + 6);

	/* 2. 输出对象的内存布局信息 */
	int *pBaseFunc1 = (int *)*(vtptr1 + 0);
	int *pBaseFunc2 = (int *)*(vtptr1 + 1);
	int *pBaseFunc3 = (int *)*(vtptr1 + 2);
	int *pBaseFunc4 = (int *)*(vtptr1 + 3);

	(FUNC(pBaseFunc1))();
	(FUNC(pBaseFunc2))();
	(FUNC(pBaseFunc3))();
	(FUNC(pBaseFunc4))();
	// .... 后面省略若干输出内容,可自行补充
	return 0;
}

测试效果:

2cf91b9ce465926909407d05216921f2.png


派生类覆盖基类虚函数

我们再来看一下派生类覆盖了基类的虚函数的情形,可见:

  • 虚表中派生类覆盖的虚函数的地址被放在了基类相应的函数原来的位置

  • 派生类没有覆盖的虚函数延用基类的

代码如下所示,注意这里只给出了类的定义,main函数的测试代码与上节一样:

class Devired : public Base, public Base2, public Base3
{
public:
	Devired(int mem = 7) : m_iMem1(mem) { ; }
	virtual void vdfunc1() { std::cout << "In Devired vdfunc1()" << std::endl; }
	virtual void vfunc1() { std::cout << "In Devired vfunc1()" << std::endl; }
	virtual void vBase2func1() { std::cout << "In Devired vfunc1()" << std::endl; }

private:
	int m_iMem1;
};

测试效果

710094f7239959d278a510b38439606d.png

钻石型虚继承

该继承还是遵循上述的所有原则,我们直接来测试。

测试代码

// 测试四:钻石型虚继承

//虚基指针所指向的虚基表的内容:
//	1. 虚基指针的第一条内容表示的是该虚基指针距离所在的子对象的首地址的偏移
//	2. 虚基指针的第二条内容表示的是该虚基指针距离虚基类子对象的首地址的偏移

#pragma vtordisp(off)
#include <iostream>
using std::cout;
using std::endl;

class B
{
public:
	B() : _ib(10), _cb('B') {}

	virtual void f()
	{
		cout << "B::f()" << endl;
	}

	virtual void Bf()
	{
		cout << "B::Bf()" << endl;
	}

private:
	int _ib;
	char _cb;
};

class B1 : virtual public B
{
public:
	B1() : _ib1(100), _cb1('1') {}

	virtual void f()
	{
		cout << "B1::f()" << endl;
	}

#if 1
	virtual void f1()
	{
		cout << "B1::f1()" << endl;
	}
	virtual void Bf1()
	{
		cout << "B1::Bf1()" << endl;
	}
#endif

private:
	int _ib1;
	char _cb1;
};

class B2 : virtual public B
{
public:
	B2() : _ib2(1000), _cb2('2') {}

	virtual void f()
	{
		cout << "B2::f()" << endl;
	}
#if 1
	virtual void f2()
	{
		cout << "B2::f2()" << endl;
	}
	virtual void Bf2()
	{
		cout << "B2::Bf2()" << endl;
	}
#endif
private:
	int _ib2;
	char _cb2;
};

class D : public B1, public B2
{
public:
	D() : _id(10000), _cd('3') {}

	virtual void f()
	{
		cout << "D::f()" << endl;
	}

#if 1
	virtual void f1()
	{
		cout << "D::f1()" << endl;
	}
	virtual void f2()
	{
		cout << "D::f2()" << endl;
	}

	virtual void Df()
	{
		cout << "D::Df()" << endl;
	}
#endif
private:
	int _id;
	char _cd;
};

int main(void)
{
	D d;
	cout << sizeof(d) << endl;
	return 0;
}

测试效果

1>class D	size(52):
1>	+---
1> 0	| +--- (base class B1)
1> 0	| | {vfptr}
1> 4	| | {vbptr}
1> 8	| | _ib1
1>12	| | _cb1
1>  	| | <alignment member> (size=3)
1>	| +---
1>16	| +--- (base class B2)
1>16	| | {vfptr}
1>20	| | {vbptr}
1>24	| | _ib2
1>28	| | _cb2
1>  	| | <alignment member> (size=3)
1>	| +---
1>32	| _id
1>36	| _cd
1>  	| <alignment member> (size=3)
1>	+---
1>	+--- (virtual base B)
1>40	| {vfptr}
1>44	| _ib
1>48	| _cb
1>  	| <alignment member> (size=3)
1>	+---
1>
1>D::$vftable@B1@:
1>	| &D_meta
1>	|  0
1> 0	| &D::f1
1> 1	| &B1::Bf1
1> 2	| &D::Df
1>
1>D::$vftable@B2@:
1>	| -16
1> 0	| &D::f2
1> 1	| &B2::Bf2
1>
1>D::$vbtable@B1@:
1> 0	| -4
1> 1	| 36 (Dd(B1+4)B)
1>
1>D::$vbtable@B2@:
1> 0	| -4
1> 1	| 20 (Dd(B2+4)B)
1>
1>D::$vftable@B@:
1>	| -40
1> 0	| &D::f
1> 1	| &B::Bf
1>

总结

几个原则

单继承

  • 虚表中派生类覆盖的虚函数的地址被放在了基类相应的函数原来的位置

  • 派生类没有覆盖的虚函数就延用基类的。同时,虚函数按照其声明顺序放于表中,父类的虚函数在子类的虚函数前面。

多继承

每个基类都有自己的虚函数表
派生类的虚函数地址存依照声明顺序放在第一个基类的虚表最后

安全性问题

当我们直接通过父类指针调用子类中的未覆盖父类的成员函数,编译器会报错,但通过实验,我们可以用对象的地址访问到各个子类的成员函数,就违背了C++语义,操作会有一定的隐患,当我们使用时要注意这些危险的东西!

*声明:本文于网络整理,版权归原作者所有,如来源信息有误或侵犯权益,请联系我们删除或授权事宜。

2789d9c50167c1767b3d2875567e946a.png

4c3d2eff4b4f2c0892337fba196c78e7.gif

戳“阅读原文”我们一起进步

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值