从编译器角度理解虚函数、静态绑定和动态绑定

示例代码

我们首先来看这么一段代码,设计了两个类:BaseDerive
Base类包含一个保护的成员变量:ma
以及构造函数和两个show方法(一个不带参数,一个带有参数)。
Derive类公有继承Base类:class Derive : public Base
包含一个私有的成员变量:mb
以及构造函数和一个show方法(不带有参数)。

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

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

接下来,我们通过如下代码运行该程序:

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

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

	pb->show();	
	pb->show(10);  

	cout << sizeof(Base) << endl;
	cout << sizeof(Derive) << endl;

	cout << typeid(pb).name() << endl;
	cout << typeid(*pb).name() << endl;
	
	return 0;
}

可以看到,我们先是实例化了一个派生类对象d,
接着用一个基类指针pb指向了d,
然后通过pb分别调用不带参数和带有参数的方法;
并且分别打印类型信息。

注意:
因为想要打印有关类型的信息,所以我们加入了#include <typeinfo>的头文件。

示例结果

在这里插入图片描述
可以看到结果:
因为pbBase类型的指针,所以调用的都是Base类的成员方法;
基类Base只有一个数据成员ma,所以大小只有4字节;
派生类Derive继承了ma,其次还有自己的mb,所以有8字节;
pb的类型是一个class Base *
*pb的类型是一个class Base
为了更好地理解上述过程,我们简单画图如下:
在这里插入图片描述
为什么Base *类型的指针,Derive类型的对象,调用方法的时候是Base而不是Derive呢?
原因如上图:
Derive类继承了Base类,导致了派生类的大小要比基类大,而pb的类型是基类的指针,所以通过pb调用方法时只能访问到Derive中从Base继承而来的方法,访问不到自己重写的方法(指针的类型限制了指针解引用的能力)。

对比代码

接下来我们修改代码:
在基类的两个成员方法前面加上virtual关键字:

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

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

对比结果

之后再次运行上述main代码,结果如下:
在这里插入图片描述
可以看到这次的结果:
pb调用无参show方法是调用的是Derive的方法;
pb调用带参show方法还是调用的Base的方法;
基类和派生类的大小均增加了4
pb的类型还是class Base *
但是*pb的类型变为了class Derive

虚函数

在我们添加了virtual关键字后,对应的函数就变成了虚函数
那么,一个类添加了虚函数,对这个类有什么影响呢?

  1. 首先,如果类里面定义了虚函数,那么编译阶段,编译器给这个类类型产生一个唯一的vftable虚函数表,虚函数表中主要存储的内容是:RTTI(Run-time Type Information)指针和虚函数的地址,当程序运行时,每一张虚函数表都会加载到内存的.rodata区;
  2. 一个类里面定义了虚函数,那么这个类定义的对象,在运行时,内存中会多存储一个vfptr虚函数指针,指向了对应类型的虚函数表vftable
  3. 一个类型定义的n个对象,他们的vfptr指向的都是同一张虚函数表;
  4. 一个类里面虚函数的个数,不影响对象内存的大小(vfptr),影响的是虚函数表的大小。

图示如下:(以Base为例)
在这里插入图片描述

静态绑定

静态:可以理解为:编译时期 确定的;
绑定:可以理解为:函数的调用
我们回到不加虚函数的代码,执行代码:

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

	pb->show();	

执行到pb->show()的时候,我们在此处下一个断点,进入反汇编:
在这里插入图片描述
我们可以看到:
编译时期,汇编码就已经确定了此处函数的调用(给出了作用域、函数名、函数地址等确切信息);
换句话说:在编译时期,编译器就已经确定了如何执行这一条语句。

动态绑定

动态:可以理解为:运行时期 确定的;
绑定:可以理解为:函数的调用
同样的,我们这次回到加虚函数的代码,执行代码:

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

	pb->show();	

依然在最后一句下断点,反汇编进去:
在这里插入图片描述
我们可以看到这一次,汇编码call的就不是确切的函数地址了,而是寄存器eax
那么就很好理解了:
eax寄存器里存放的是什么内容,编译阶段根本无从知晓,只能在运行的时候确定;
故,动态绑定

查看虚函数表

最后,我们还可以通过VS的工具来查看虚函数表的有关信息,操作如下:
(注意:博主使用的是VS 2019,其他版本的可能稍有不同)

  1. 首先在工具栏中找到:命令行开发者命令提示
    在这里插入图片描述

  2. 切换到当前文件的目录下:
    在这里插入图片描述

  3. 输入命令:cl XXX.cpp /d1reportSingleClassLayoutXX(第一个XXX表示源文件的名字,第二个代表你想查看的类类型,我这里就是Derive)
    在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值