【c++】——由虚函数引发的静态绑定和动态绑定

一、虚函数

1、初识添加了虚函数的类

在上一篇博客中大家已经了解到了继承的相关知识吧,在这一篇文章中,我们首先写一个继承。

#include<iostream>
#include<typeinfo>
using namespace std;
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;
};
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;
}

运行结果如下:
在这里插入图片描述

观察上述结果,结合上一篇博客的知识。我们可以分析一下原因。
因为pb是基类指针,由此先调用基类没有参数的show构造函数再调用有参数的show构造函数。
因为基类只有一个成员变量ma,所以大小为4个字节,而派生类由于继承了继承的ma,再加上自己的成员变量mb,所以大小是8个字节
因为pb是基类指针,所以他的类型是class Base*。class Base 指针解引用的类型,就类似于指针指向的类型

以上是我们常规的继承方法,但是如果我们在基类的两个show()方法前面加上virtual关键字过后,如下代码,运行结果又会有什么变化呢?

我们来好好探究一番吧~

virtual void show() { cout << "Base::show()" << endl; }
	virtual void show(int) { cout << "Base::show(int)" << endl; }

(1)一个类里面定义了虚函数,那么编译阶段,编译器需给这个类型产生一个唯一的vftable(虚函数表),表中主要存储的内容就是RTTI指针和虚函数地址,当程序运行时,每一张虚函数表都会加载到.rodata区

下图为Base类型对应的一张虚函数表:
在这里插入图片描述
为了理解这张虚函数表,我们先来说一说里面的含义
RTTI:run-time type ifformation 运行时的类型信息
**0:**是vfptr在对象内存中的偏移量,因为vfptr的优先级是很高的,所以一般都在前四个字节存在,因此偏移量一般也都为0

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

例如,下图为Base 类定义的对象b1和b2他们的内存图:
在这里插入图片描述
由上图我们可以看出
3)一个类里面虚函数的个数不影响对象的内存大小(vfptr),影响的是虚函数表的大小

从我们的代码可见,现在我们把基类里面的show方法都定义成了虚函数,但是没有改变派生类里面的show方法,那么我们再用派生类定义两个对象d1和d2他们的内存图如下:
在这里插入图片描述
由此我们可以得出
(4)如果派生类中的方法和基类继承来的某个方法,返回值、函数名、参数名都相同,而且基类的方法是virtual虚函数,那么派生类的这个方法自动处理成虚函数

最后我们来分析指针类型的改变吧~

cout << typeid(pb).name() << endl;
cout << typeid(*pb).name() << endl;

首先看pb的类型可以得到其为Base,再看Base里面有没有虚函数

  1. 如果Base没有虚函数,*pb识别的就是编译时期的类型 *pb == Base类型

  2. 如果Base有虚函数,*pb识别的就是运行时期的类型 RTTI类型。RTTI类型存放在虚函数数表中,在这里pb是d(vfptr)指向自己派生类的虚函数表,该虚函数表RTTI类型就是 class Derive

    所以有了虚函数过后,运行结果如下:
    在这里插入图片描述

2、哪些函数不能实现成虚函数

在回答这个问题之前,我们首先要来明确一下虚函数的依赖

  1. 虚函数能产生地址,存储在vftable当中
  2. 对象必须存在,因为对象存在才有vfptr才能找到vftable才能找到虚函数地址

由以上的依赖特点,我们就可以很容易的给出问题的答案。
第一种:构造函数
不能在构造函数前面加virtual因为构造函数是构造完成后对象才产生
特点:
构造函数中(调用的任何函数,都是静态绑定),调用虚函数也不会发生动态绑定
派生类对象构造过程 先调用的是基类的构造函数,才调用派生类的构造函数

第二种:static静态成员方法
因为其方法的调用不依赖对象

3、虚析构函数

从上述的讲解,我们知道了构造函数是不能成为虚函数的,但是,析构函数是可以的。
因为其调用的时候对象是存在的

我们实现下列代码:

class Base 
{
public:
	Base(int data) :ma(data) { cout << "Base()" << endl; }
	~Base() { cout << "~Base()" << endl; }
	virtual void show() { cout << "Base::show()" << endl; }
protected:
	int ma;
};
class Derive :public Base
{
public:
	Derive(int data)
	   :Base(data), mb(data)
	{
		delete ptr;
		cout << "Derive()" << endl;
	}
	~Derive()
	{
		cout << "~Derive()" << endl;
	}
private:
	int mb;
	int* ptr;
};
int main()
{
	Base* pb = new Derive(10);
	pb->show();
	delete pb;
	return 0;
}

运行结果如下:
在这里插入图片描述
仔细观察,我们会发现他是错的,因为派生类对象没有析构函数
在派生类里面,我们有一个ptr指向了外部内存,但是派生类的析构函数没有被调用到。

因为,pa->Base Base::~Base 对于析构函数的调用,就是静态绑定。只调用了基类的析构没有调用派生类的析构call Base::~Base

解决方法:
将基类的析构函数实现成虚函数类的析构函数是virtual虚函数,那么派生类的析构函数自动成为虚函数。可以想象成一种同名函数的覆盖。对于析构函数的调用,就是动态绑定。

运行结果如下:
在这里插入图片描述
由此,我们可以总结出什么时候把基类的析构函数必须实现成虚函数——基类的指针(引用)指向堆上new出来的派生类对象,delete pb(基类的指针),它调用析构函数的时候必须发生动态绑定否则会导致派生类的析构函数无法调用

二、动态绑定和静态绑定

1、动态绑定和静态绑定的具体实现

有了上述对一个类添加了虚函数,对这个类有什么影响的总结。接下来,我们就来好好的研究一下当语句pb->show();执行时发生了什么吧~

情况一:静态绑定
pb->Base Base::show 如果发现show是普通函数,就进行静态绑定 执行指令为:

  • call Base::show(01612DAh)

情况二:动态绑定
pb->Base Base::show 如果发现show是虚函数,就进行动态(运行时期)绑定(函数的调用)
执行的指令为:

  • mov eax, dword ptr[pb]
    把对象前四个字节虚函数表的地址放入寄存器里面
  • mov ecx, dword ptr[eax]
    把eax存的四个字节的内存(派生类的show方法)放入ecx寄存器
  • call ecx(虚函数的地址)
    call的为一个寄存器,在编译阶段无法确定调用的是哪一个函数,只有在运行的才知道寄存器里面放的是什么地址

2、虚函数和动态绑定

通过上述对哪些函数不能实现成虚函数的讨论,我们很容易的发现,**不是虚函数的调用一定就是动态绑定,**因为在类的构造函数中调用虚函数但是是静态绑定

所以,我们来讨论一下,哪种情况才是动态绑定吧~
首先我们来实现这样一个继承的代码:

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

情况一:用对象本身调用虚函数,是静态绑定
在主函数里面实现如下调用

int main()
{
	Base b;
	Derive d;
	b.show();//call Base::show
	d.show();//call Derive::show
	return 0;
}

这样实现的是静态绑定,因为,如果实行动态绑定就是用对应的虚函数指针访问对应的虚函数表得到虚函数地址,和静态绑定都是一样的

情况二:指针调用虚函数是动态绑定

在主函数里面实现如下调用

int main()
{
	Base b;
	Derive d;
	Base* pb1 = &b;
	pb1->show();

	Base* pb2 = &d;
	pb2->show();
	return 0;
}

情况三:由引用变量调用虚函数是动态绑定

在主函数里面实现如下调用

int main()
{
	Base b;
	Derive d;
	Base& rb1 = b;
	rb1.show();
	Base& rb2 = d;
	rb2.show();
	return 0;
}

情况四:函数通过指针或者引用变量调用,才发生动态绑定

在主函数里面实现如下调用

int main()
{
	Base b;
	Derive d;
	Derive* pd1 = &d;
	pd1->show();
	Derive& rd1 = d;
	rd1.show();
	return 0;
}

总结:如果不是通过指针或者引用变量来调用虚函数,那就是静态绑定

发布了98 篇原创文章 · 获赞 9 · 访问量 3664

猜你喜欢

转载自blog.csdn.net/qq_43412060/article/details/105224354