【C++】—— 多态(下)之多态的实现原理

上篇博客 多态(上) 我们介绍了多态的相关概念,以及抽象类及纯虚函数。接下来我们来看看多态是怎么实现的:

一、多态的实现原理

1.1 虚函数表

这里我们先来看一道经常会遇到的面试和笔试题

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};

int main()
{
	printf("%d\n", sizeof(Base));
	return 0;
}

多态的实现原理
通过观察上幅图我们可以发现

  • 未加关键字virtual时,对象b是4个字节
  • 加上了关键字virtual以后b对象是8bytes,除了 _b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做 虚函数表指针(v代表virtual,f代表function)。
  • 而一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。(区别于菱形虚拟继承中的 虚基表)参见博客 菱形继承与菱形虚拟继承

这里可能就会有人问这个虚函数表中到底存放的是什么呢,下面我们就具体来分析一下
这里我们稍微修改一下我们刚刚的代码,给它添加了 一个虚函数Fun2()和一个普通函数Fun3()一个派生类Derive,并计算了两个类的大小。

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};
class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	printf("Base:%d\n", sizeof(Base));
    printf("Derive:%d\n", sizeof(Derive));

	return 0;
}

这是两个类的大小,以及在监视环境下所看到了对象中所存储的数据
多态的实现原理

通过观察和测试,我们可以知道以下几个问题:

  1. 派生类对象d中也有一个虚表指针,d对象由两部分组成,一部分是父类继承下来的成员,一部分是自己的成员,派生类的虚表指针是存在自己的成员部分的。
  2. 基类对象b和派生类对象d的虚表指针是不一样的,我们可以看到派生类对基类的Fun1()函数进行了重写,所以d的虚表中存储的是重写过后的Derive::Fun1(),所以虚函数的重写也叫覆盖
  3. 派生类将Fun2继承下来,未进行重写,直接放进了虚表中,Fun3也继承下来了,但是因为不是虚函数,没有放进虚表中。
  4. 虚函数表的本质是一个存放虚函数指针的指针数组,这个指针数组最后放了一个nullptr
  5. 总结一下派生类虚表的生成:a.先将基类虚表内容拷贝一份放在派生类自己的虚表当中。b.如果派生类重写了基类的函数,就用派生类自己的函数对虚表基类的函数进行覆盖。c.派生类自己新增的函数,按照声明顺序增加在派生类虚表的最后。
  6. 虚表中存的是虚函数的指针,虚函数和普通函数一样是存储在代码段的,而虚表也是存储在代码段的。
  7. 同一个类的不同对象的虚表是一样的,一个类在内存中只有一份虚表。

1.2 多态的实现原理

多态
多态的实现原理

注:看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。

1.3 动态绑定和静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态
  3. 我们在C语言中用到的printf和在C++中用到的cout都属于多态的静态绑定。

二、单继承与多继承的虚函数表

2.1 单继承中的虚函数表

class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};
class Derive : public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
private:
	int b;
};

在这里插入图片描述

观察下图中的监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。那么我们如何查看d的虚表呢?下面我们使用代码打印出虚表中的函数。

typedef void(* VFPTR) ();//将fun函数定义为一个虚函数指针VFPTR

class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};
class Derive : public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
private:
	int b;
};


void PrintVTable(VFPTR* vTable)//打印出虚函数表中存储的虚函数指针
{
	cout << "虚表地址->" << vTable << endl;

	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf("第%d个虚函数的地址:0x%x->", i, vTable[i]);
		VFPTR f = vTable[i];
    //调用一下存在虚表对应地址的函数,输出对应函数的实现,就能知道调用的是哪个函数了
		f();
	}
}
int main()
{
	Base b;
	Derive d;

	VFPTR* vTableb = (VFPTR*)(*(int*)&b);//取对象前四个字节就是虚表指针
	PrintVTable(vTableb);

	VFPTR* vTabled = (VFPTR*)(*(int*)&d);
	PrintVTable(vTabled);

	return 0;
}`

单继承中的虚函数表
虚函数表
2.2 多继承中的虚函数表

相同的方法打印多继承中的虚函数表中虚函数的地址,这里就不赘述了,直接给代码和结果吧

class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1;
};
class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
	int b2;
};
class Derive : public Base1, public Base2 
{
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d1;
};

typedef void(* VFPTR) ();
void PrintVTable(VFPTR* vTable)
{
	cout << "虚表地址->" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf("第%d个虚函数地址:0x%x->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
}
int main()
{
	Derive d;

	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb1);

	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintVTable(vTableb2);

	return 0;
}

多继承中的虚函数表

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值