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

Posted Redamanc

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从编译器角度理解虚函数静态绑定和动态绑定相关的知识,希望对你有一定的参考价值。

虚函数、静态绑定、动态绑定

示例代码

我们首先来看这么一段代码,设计了两个类: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)

以上是关于从编译器角度理解虚函数静态绑定和动态绑定的主要内容,如果未能解决你的问题,请参考以下文章

16. 虚函数静态绑定动态绑定

对C++静态绑定与动态绑定的理解

多态知识点

C++ 多态 : 虚函数静态绑定动态绑定单/多继承下的虚函数表

C++ 多态 : 虚函数静态绑定动态绑定单/多继承下的虚函数表

C++ 多态 : 虚函数静态绑定动态绑定单/多继承下的虚函数表