218-C++继承与多态(虚函数、静态绑定、动态绑定)

1、静态绑定

在这里插入图片描述
在这里插入图片描述

我们知道, 高级的源代码首先要被编译成汇编码,然后汇编码被汇编器编译成机器码。
我们转成汇编看看
在这里插入图片描述
是编译阶段就已经确定好的函数调用,生成指令了,指定哪个作用域哪个名字的哪个函数了。

编译器看到调用方法的pb指针是基类类型的,它就去基类类型里面去查看这个方法,直接进行调用,这就是静态绑定。

在这里插入图片描述

#include <iostream>
#include <typeinfo>
using namespace std;

class Base
{
public:
	Base(int data = 10) :ma(data) {}
	void show() { cout << "Base::show()" << endl; }//#1
	void show(int) { cout << "Base::show(int)" << endl; }//#2
protected:
	int ma;
};

class Derive : public Base
{
public:
	Derive(int data = 20) :Base(data), mb(data) {}
	void show() { cout << "Derive::show()" << endl; }//#3
private:
	int mb;
};


int main()
{
	Derive d(50);
	Base* pb = &d;

	pb->show();		//静态(编译时期)的绑定(函数调用) Call Base::show (0EA1037h)
	pb->show(10);	//静态绑定	 Call Base::show (0EA12F3h) 

	cout << sizeof(Base) << endl;	//4
	cout << sizeof(Derive) << endl;	//8

	cout << typeid(pb).name() << endl;	//class Base*
	cout << typeid(*pb).name() << endl;	//class Base

	/*
	Base::show()
	Base::show(int)
	4
	8
	class Base *
	class Base
	*/

	return 0;
}

2、虚函数

我们在基类Base中的两个show方法的最前面加上virtual关键字
这2个show函数就变成虚函数了
在这里插入图片描述

一个类添加了虚函数,对这个类有什么影响?

总结一:

  • 一个类里面定义了虚函数,那么在编译阶段,编译器给这个类类型产生一个唯一的vftable虚函数表虚函数表中主要存储的内容就是RTTI指针和虚函数的地址。当程序运行时,每一张虚函数表都会加载到内存的.rodata区(这张表是只能读而不能改的)。
    在这里插入图片描述

  • RTTI指针指向一个类型字符串(这张表是哪个类型产生的,这个字符串就是哪个类型的),这张表是编译Base类产生的,所以这个RTTI指向的是一个Base类型的字符串。

  • 另外,虚函数表还存储一个整数(偏移量),偏移量在大部分情况下等于0(0表示vfptr在对象内存中的偏移量,因为vfptr的排列优先级非常高,在没有虚继承的情况下,vfptr永远在对象的前4个字节放着,所以就是在对象的起始部分,就是0)。

  • 虚函数表还存储一个整数(地址),类里有2个虚函数,所以在这里存了这2个虚函数的地址。

虚函数表结构如下:(一个类型产生一个虚函数表)
在这里插入图片描述
在编译器终端,先看Derive类:
在这里插入图片描述
在这里插入图片描述
再看Base类:
在这里插入图片描述
在这里插入图片描述
上面我们的虚函数表中的虚函数排列有问题:

  • 纠正一下,应该是从上到下的覆盖关系

在这里插入图片描述
派生类将基类中的show()给覆盖掉了,如下所示:
在这里插入图片描述

运行的时候,定义了b1这个Base类型的对象。
b1对象的内存大小不仅仅是4个字节了。
不仅存储一个ma,还要存储一个vfptr虚函数指针(指向Base类型的虚函数表)
在这里插入图片描述
现在基类定义的对象占8个字节大小了。

我们再定义一个b2的Base类型对象,内存也是8字节哦,但是它们两个都是Base类型,所以它们两个的虚函数指针vfptr都是指向Base类型的虚函数表,指向的是同一个虚函数表。
在这里插入图片描述
总结二:

  • 一个类里面定义了虚函数,那么这个类定义的对象,其运行时,内存中开始部分,多存储一个vfptr虚函数指针,指向相应类型的虚函数表vftable。
  • 一个类型定义的n个对象,它们的vfptr指针指向的都是同一张虚函数表。

总结三:

  • 一个类里面的虚函数的个数,不影响对象的内存大小(都是只有一个vfptr指针),但是影响的是虚函数表的大小(多存一个函数,虚函数表会变大)。

接下来我们来看看派生类有什么变化?
总结四:
如果派生类中的方法,和基类继承来的某个方法,返回值、函数名、参数列表都相同,而且基类的方法是virtual虚函数,那么派生类的这个方法,自动处理成虚函数。

那么这两个函数的关系就是覆盖的关系!

3、覆盖

在这里插入图片描述
图中的最上面的那个show方法和最下面的那个show方法是覆盖的关系。
派生类的show方法被自动处理成虚函数了。

我们定义2个派生类对象d1和d2
现在派生类自己也有虚函数了,从记录Base也继承过来2个虚函数;

在编译阶段
我们会个Derive这个类型产生虚函数表,因为从基类继承来了虚函数。
虚函数表先放的是RTTI指针,RTTI指向的是类型字符串(Derive类型)。
然后存储0
然后存储虚函数的地址。
因为从基类继承来2个虚函数,所以记录的是继承来的这2个虚函数的地址
在这里插入图片描述
但是编译器转眼一看,这个派生类里面对这个基类的show方法进行了覆盖了,或者叫做重写了,重写了这个同名的覆盖方法。
在这里插入图片描述
编译器就知道,不要基类的那个show方法了,因为派生类把这个不带参数的show方法重写了,所以在派生类的虚函数表里面,就要放派生类重写的show方法的虚函数地址了,这就是覆盖关系。
在这里插入图片描述
覆盖: 虚函数表中的虚函数地址的覆盖!

但是派生类并没有对基类的带int参数的show虚函数进行重写,所以派生类的虚函数表还是存储的是基类的带int参数的show虚函数地址。

派生类对象的内存结构:新增一个虚函数指针,指向当前类型的虚函数表。
在这里插入图片描述
覆盖:

  • 基类和派生类的方法,返回值、函数名以及参数列表都相同,而且基类的方法是虚函数,那么派生类的方法就自动处理成虚函数,它们之间成为覆盖关系。

4、动态绑定

在这里插入图片描述

编译阶段,编译器看pb是基类Base类型,然后就会跑到基类的作用域里面看这个不带参数的show是什么情况,如果发现这个show是普通函数,就进行静态绑定,直接生成call:Base::show,也就是说,经过编译就知道,这里无论如何调用的都是基类Base下的show方法!

但是,如果在编译阶段,编译器发现pb是Base类型的指针, 然后它跑去Base的作用域下去查看show,发现这个show是虚函数,如果发现show是虚函数,就进行动态绑定了:
在这里插入图片描述

把pb指向的派生类对象的前4个字节(虚函数指针,即虚函数的地址)放到eax寄存器,再将eax内容放到edx中, edx放的是虚函数的地址;再通过edx+4找到show这个函数在虚函数表中的位置,call eax调用函数

call eax我们不知道最终调用的是哪个函数,寄存器放的是最终从虚函数表中取出来的虚函数的地址。
但是到底是哪个虚函数的地址,只有在运行的时候,通过指令在虚函数表里面找到谁的地址,调用的就是哪个函数。

在编译阶段生成的指令中,无法判断最终调用的是哪个函数,我们称作动态绑定
在这里插入图片描述
样的这句代码,编译器在编译的时候看到这个指针pb是Base类型的,跑去Base类型的作用域下查看,发现这个带int类型的show方法是虚函数,就得进行动态绑定了。和刚才叙述的情况一样。
在这里插入图片描述

虚函数的定义的顺序和在虚函数的表的位置有关。先定义的放在虚函数表的前面。

	cout << sizeof(Base) << endl;//8
	cout << sizeof(Derive) << endl;//12

	cout << typeid(pb).name() << endl;//class Base*
	/*
	pb的类型:编译器查看是Base -> 然后看它有没有虚函数
	如果Base没有虚函数,*pb识别的就是编译时期的类型  *pb <=> Base类型
	如果Base有虚函数,*pb识别的就是运行时期的类型 --》就是RTTI类型 在虚函数表中存着 
	通过pb->d(vfptr)->Derive vftable 因为指向的是派生类对象,
	所以最终访问的就是class Derive 虚函数表 访问的就是派生类类型
	*/ 
	cout << typeid(*pb).name() << endl;//class Derive 

在这里插入图片描述

总结

我们在回答时,可以从指令上说:

  • 静态绑定: 在编译时期绑定,对普通函数的调用;生成的指令,直接call具体的函数;
  • 动态绑定: 在运行时期绑定,对虚函数的调用;call的是一个寄存器,寄存器存放的是什么地址,只有在运行的时候才会知道!
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

liufeng2023

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值