深入探索C++对象模型_(4)虚函数表

虚函数表指针

  • 如果某个类中声明了虚函数,那么编译器会在该类对象以及该类的派生类对象中插入一个指针变量,此变量就是虚函数指针
  • 虚函数表指针的位置可能在对象开头,也可能在对象末尾,取决于编译器
// project100.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
using namespace std;

class A
{
public:
	int i; //4字节
	virtual void testfunc() {}  //虚函数,vptr4字节。
};

int main()
{
	//虚函数表指针位置分析
	//类:有虚函数,这个类会产生一个虚函数表。
	//类对象,有一个指针,指针(vptr)会指向这个虚函数表的开始地址。
	A aobj;
	int ilen = sizeof(aobj);
	cout << ilen << endl;  //8字节

	char *p1 = reinterpret_cast<char *>(&aobj); //类型转换,硬转 &aobj这是对象aobj的首地址。
	char *p2 = reinterpret_cast<char *>(&(aobj.i));
	if (p1 == p2) //说明aobj.i和aobj的位置相同,说明i在对象aobj内存布局的上边。虚函数表指针vptr在下边
	{
		cout << "虚函数表指针位于对象内存的末尾" << endl;
	}
	else
	{
		cout << "虚函数表指针位于对象内存的开头" << endl;
	} 
	return 1; 
}

在这里插入图片描述

虚函数表

虚函数表是怎么工作的?

  • 一个声明有虚函数的类,其对象的虚函数表会按一定布局(如何布局后面再说),保存其虚函数的入口地址;当其派生类重载了某个虚函数时,派生类虚函数表中的对应项就会被替代
#include <iostream>
using namespace std;

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

int main()
{
	//继承关系作用下虚函数的手工调用			
	cout << sizeof(Base) << endl;	
	cout << sizeof(Derive) << endl;

	Derive *d = new Derive(); //派生类指针。
	long *pvptr = (long *)d;  //指向对象的指针d转成了long *类型。
	long *vptr = (long *)(*pvptr); //(*pvptr) 表示pvptr指向的对象,也就是Derive本身。Derive对象是4字节的,代表的是虚函数表指针地址。

    cout << "父类的虚函数表地址为:" << vptr << endl;
	for (int i = 0; i <= 4; i++) //循环5次;
	{
		printf("vptr[%d] = 0x:%p\n", i, vptr[i]);
	}

	typedef void(*Func)(void); //定义一个函数指针类型
	Func f = (Func)vptr[0]; //f就是函数指针变量。 vptr[0]是指向第一个虚函数的。
	Func g = (Func)vptr[1];
	Func h = (Func)vptr[2];
	/*Func i = (Func)vptr[3];
	Func j = (Func)vptr[4];*/

	f();
	g();
	h();
	//i();

	Base *dpar = new Base();
	long *pvptrpar = (long *)dpar;
	long *vptrpar = (long *)(*pvptrpar);
    
    cout << "子类的虚函数表地址为:" << vptrpar << endl;
	for (int i = 0; i <= 4; i++) //循环5次;
	{
		printf("vptr Base[%d] = 0x:%p\n", i, vptrpar[i]);
	}

	Func fpar = (Func)vptrpar[0]; 
	Func gpar = (Func)vptrpar[1];
	Func hpar = (Func)vptrpar[2];

	cout << "--------------------" << endl;
	fpar(); 
	gpar();
	hpar();

	return 1; 
}
  • 输出
8
8
vptr[0] = 0x:0x55f0ee30f3dc
vptr[1] = 0x:0x55f0ee30f484
vptr[2] = 0x:0x55f0ee30f44c
vptr[3] = 0x:(nil)
vptr[4] = 0x:0x55f0ee311da8
Base::f()
Derive::g()
Base::h()
vptr Base[0] = 0x:0x55f0ee30f3dc
vptr Base[1] = 0x:0x55f0ee30f414
vptr Base[2] = 0x:0x55f0ee30f44c
vptr Base[3] = 0x:0x7f7d8199ae98
vptr Base[4] = 0x:0x55f0ee310070
--------------------
Base::f()
Base::g()
Base::h()
  • Derive类对象的虚函数表中,父类Base中的g()入口地址被取代了,而其他函数的入口地址则没有变化;可见,在子类中重载的虚函数会使虚函数表中的对应项被修改.

虚函数表的布局?

在这里插入图片描述+ 由图可见:
+ 父类对象和子类对象的vptr各自指向不同的vtbl
+ 这两个表的第一项和第三项代表了没有被子类重载的虚函数,指向了相同的函数入口地址
+ 这两个表的第二项各自指向了不同的函数入口

  • 得出结论(不仅仅从图中总结):
    • 一个类只有包含虚函数才会存在虚函数表,同属于一个类的对象共享虚函数表,但是有各自的vptr(虚函数表指针),当然所指向的地址(虚函数表首地址)相同
    • 父类中有虚函数就等于子类中有虚函数。话句话来说,父类中有虚函数表,则子类中肯定有虚函数表
    • 如果子类中完全没有新的虚函数,则我们可以认为子类的虚函数表和父类的虚函数表内容相同。但仅仅是内容相同,这两个虚函数表在内存中处于不同位置.换句话说,这是内容相同的两张表。
    • 超出虚函数表部分内容不可知

多重继承的类对象虚函数指针是怎么布局的?

  • 对于某个多重继承的类,这个类会有多个虚函数表,其对象内会有多个虚函数表指针
  • 在这种类的对象中,对应于各个基类的vptr(即是指向各个基类虚函数表(这个表跟基类原来的虚函数表不是同一个地址,只是相同内容的表)的虚函数表指针)会按照继承顺序依次放置在类对象内存空间中
  • 子类的自己的虚函数入口地址跟第一个基类的虚函数地址放在同一个表中,子类虚函数项紧跟第一个基类的虚函数项其后;
#include <iostream>
using namespace std;

//基类1
class Base1
{
public:
	virtual void f()
	{
		cout << "base1::f()" << endl;
	}
	virtual void g()
	{
		cout << "base1::g()" << endl;
	}
};

//基类2
class Base2
{
public:
	virtual void h()
	{
		cout << "base2::h()" << endl;
	}
	virtual void i()
	{
		cout << "base2::i()" << endl;
	}
};

//子类
class Derived :public Base1, public Base2
{
public:
	virtual void f() //覆盖父类1的虚函数
	{
		cout << "derived::f()" << endl;
	}
	virtual void i() //覆盖父类2的虚函数
	{
		cout << "derived::i()" << endl;
	}

	//如下三个我们自己的虚函数
	virtual void mh() 
	{
		cout << "derived::mh()" << endl;
	}
	
	virtual void mi()
	{
		cout << "derived::mi()" << endl;
	}

	virtual void mj()
	{
		cout << "derived::mj()" << endl;
	}
};



int main()
{
	//第四节  多重继承虚函数表分析
	//多重继承
	cout << sizeof(Base1) << endl;
	cout << sizeof(Base2) << endl;
	cout << sizeof(Derived) << endl;

	Derived ins; 
	Base1 &b1 = ins; //多态
	Base2 &b2 = ins;
	Derived &d = ins;

	typedef void(*Func)(void);
	long *pderived1 = (long *)(&ins);
	long *vptr1 = (long *)(*pderived1); //取第一个虚函数表指针。

	long *pderived2 = pderived1 + 1; //跳过4字。
	long *vptr2 = (long *)(*pderived2); //取第二个虚函数表指针。
	
	/* 第一个基类与子类的虚函数表,子类自己的虚函数入口放在第一个基类的虚函数入口后面 */
	Func f1 = (Func)vptr1[0]; //0x00ab15d7 {project100.exe!Derived::f(void)}
	Func f2 = (Func)vptr1[1]; //0x00ab15f0 {project100.exe!Base1::g(void)}
	Func f3 = (Func)vptr1[2]; //0x00ab15cd {project100.exe!Derived::mh(void)}
	Func f4 = (Func)vptr1[3]; //0x00ab15ff {project100.exe!Derived::mi(void)}
	Func f5 = (Func)vptr1[4]; //0x00ab15eb {project100.exe!Derived::mj(void)}
	Func f6 = (Func)vptr1[5]; //非正常
	Func f7 = (Func)vptr1[6];
	Func f8 = (Func)vptr1[7];
	
	/* 第二个基类的虚函数表 */
	Func f11 = (Func)vptr2[0]; //0x00ab15af {project100.exe!Base2::h(void)}
	Func f22 = (Func)vptr2[1]; //0x00ab15b9 {project100.exe!Derived::i(void)}
	Func f33 = (Func)vptr2[2]; //非正常
	Func f44 = (Func)vptr2[3];

	b1.f();
	b2.i();
	d.f();
	d.i();
	d.mh();
	d.g();

	//----------------
	cout << "-----------------" << endl;
	f1();
	f2();
	f3();
	f4();
	f5();
	cout << "-------------" << endl;
	f11();
	f22();
	return 1; 
}
  • 输出:
8
8
16
derived::f()
derived::i()
derived::f()
derived::i()
derived::mh()
base1::g()
-----------------
derived::f()
base1::g()
derived::i()
derived::mh()
derived::mi()
-------------
base2::h()
derived::i()

虚函数表(vtbl)和虚函数表指针(vprt)是在什么时候被创建的?

  • vptr跟着对象走,所以对象什么时候创建出来,vptr就什么时候创建出来,所以是运行时建立;
    • 对于这有虚函数的类,在编译的时候,编译器会往相关的构造函数中增加 为vptr赋值的代码,这是在编译期间编译器为构造函数增加的;当运行到生成类对象的代码时,会调用类的构造函数,执行到类的构造数中的给vptr赋值的代码,这样这个类对象的vptr(虚函数表指针)就有值了
  • 虚函数表是编译器在编译期间(不是运行期间)就为每个类确定好了对应的虚函数表vtbl的内容
  • 下面这张图是在一个可执行程序中,vprtvtbl他们所处的内存区域以及其关系:
    在这里插入图片描述+ 简单分析:
    • 程序在运行时,如果需要new一个带虚函数的类对象,那么就会在栈区留下一个该类的指针,并在堆区申请这个类对象所需的内存(称为实例内存)
    • 在实例内存中,会有若干个vptr指向不同的虚函数表(虚函数表的个数由继承的基类数目决定),这些表处于只读数据段,编译时生成
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值