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 

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 测试:
f()
g()
h()

C++之深入解析虚函数表的实现及其内存布局_第1张图片

  • 可以看到,成功地通过实例对象的地址,得到了对象所有的类函数:

C++之深入解析虚函数表的实现及其内存布局_第2张图片

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

二、单继承下的虚函数表

① 派生类未覆盖基类虚函数

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

C++之深入解析虚函数表的实现及其内存布局_第3张图片

  • 可以看到:
    • 虚函数按照其声明顺序放于表中;
    • 父类的虚函数在子类的虚函数前面。
  • 测试代码:
#include 

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;
}
  • 测试效果:

C++之深入解析虚函数表的实现及其内存布局_第4张图片

② 派生类覆盖基类虚函数

  • 再来看一下派生类覆盖了基类的虚函数的情形,可见:
    • 虚表中派生类覆盖的虚函数的地址被放在了基类相应的函数原来的位置 (显然的,不然虚函数失去意义);
    • 派生类没有覆盖的虚函数延用基类的。
  • 测试代码:
#include 

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;
}
  • 测试效果:

C++之深入解析虚函数表的实现及其内存布局_第5张图片
C++之深入解析虚函数表的实现及其内存布局_第6张图片

三、多继承下的虚函数表

① 无虚函数覆盖

  • 如果是多重继承的话,问题就会复杂一些:
    • 每个基类都有自己的虚函数表;
    • 派生类的虚函数地址存依照声明顺序放在第一个基类的虚表最后(这点和单继承无虚函数覆盖相同)。
  • 如下所示:

C++之深入解析虚函数表的实现及其内存布局_第7张图片

  • 测试代码:
#include 
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)();

	/* 获取对象的内存布局信息 */
	// 虚表地址一
	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);

	/* 输出对象的内存布局信息 */
	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;
}
  • 测试效果:

C++之深入解析虚函数表的实现及其内存布局_第8张图片

② 派生类覆盖基类虚函数

  • 再来看一下派生类覆盖了基类的虚函数的情形,可见:
    • 虚表中派生类覆盖的虚函数的地址被放在了基类相应的函数原来的位置;
    • 派生类没有覆盖的虚函数延用基类的。
  • 如下所示,注意这里只给出了类的定义,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;
};
  • 测试效果:

C++之深入解析虚函数表的实现及其内存布局_第9张图片

四、钻石型虚继承

  • 该继承还是遵循上述的所有原则,直接来测试。测试代码如下:
// 虚基指针所指向的虚基表的内容:
// 1. 虚基指针的第一条内容表示的是该虚基指针距离所在的子对象的首地址的偏移
// 2. 虚基指针的第二条内容表示的是该虚基指针距离虚基类子对象的首地址的偏移

#pragma vtordisp(off)
#include 
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++ 语义,操作会有一定的隐患,使用时要注意。

你可能感兴趣的:(C/C++,单继承下的虚函数表,多继承下的虚函数表,钻石型虚继承,虚函数表的使用原则和安全性问题)