C++继承与多态三:虚函数、静态绑定与动态绑定问题、虚析构函数

一、虚函数与静态绑定、动态绑定问题

虚函数: 在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数。
注意:
1.一个类里面定义了虚函数,那么编译阶段,编译器需给这个类类型产生一个唯一的vftable虚函数表。虚函数表中主要存储的内容就是RTTI指针和虚函数的地址。当程序运行时,每一张虚函数表都会加载到内存的.rodata区(只读数据区)。
2.一个类里面定义了虚函数,那么这个类定义的对象,其运行时,内存中开始部分,多存储一个vfptr虚函数指针,指向相应类型的虚函数表vftable。一个类型定义的n个对象它们的vfptr指向都是同一张虚函数表。
3.一个类里面虚函数的个数不影响的对象内存大小,影响的是虚函数表的大小。
4.如果派生类中的方法和基类继承来的某个方法,返回值、函数名、参数列表都相同,而且基类的方法是virtual虚函数,那么派生类的这个方法自动处理成虚函数,即覆盖关系。

静态绑定(函数调用):编译时期的函数的调用,绑定的是对普通函数的调用。
动态绑定(函数调用):运行时期的函数的调用,绑定的是对虚函数的调用。

覆盖: 基类和派生类方法,返回值,函数模已经参数列表都相同,而且基类的方法是虚函数。那么派生类的方法自动处理成虚函数,它们之间称为覆盖关系。

案例1:静态绑定:我们先来看一个简单例子。

//Author:Mr.Rain
#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;//4
	cout << sizeof(Derive) << endl;//8

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

	return 0;
}

我们来看一看汇编指令:
在这里插入图片描述

pb->show();//静态绑定  call Base::show(01612DAh)
pb->show(10);//静态绑定 call Base::show(01612B2h)

编译期间将高级源代码编译为汇编码,指定了call Base::show(01612DAh)与call Base::show(01612B2h),即编译期间指定了函数的调用,这就是是静态绑定。
打印结果为:
在这里插入图片描述
案例2:动态绑定:我们向基类中的成员方法添加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;
};

添加了virtual关键字,将其变为虚函数,那么到底发生了什么事情
       如果一个类里面定义了虚函数,那么编译阶段,编译器需给这个类类型产生一个唯一的vftable虚函数表。虚函数表中主要存储的内容就是RTTI指针和虚函数的地址。 基类与派生类中vftable表如图所示:
在这里插入图片描述
在这里插入图片描述
我们再次来打印一下:
在这里插入图片描述
我们看下上面动态绑定与静态绑定执行流程:
分析一下:pb->show();
pb->show();编译阶段发现show()为Base类型,到基类作用域查看Base::show(),若show()为普通函数,就进行静态绑定call Base::show();若编译阶段指针为Base类型,到基类作用域查看Base::show(),发现show()为虚函数,就进行动态绑定。将虚函数表的地址vfptr放入eax寄存器,将vfptr存的地址的4字节内存&Derive::show()地址放入ecx寄存器,寄存器的地址我们不知道。

1.mov eax,dword ptr[pb] 将虚函数表的地址vfptr放入eax寄存器
2.mov ecx,dword ptr[eax] 将vfptr存的地址的4字节内存&Derive::show()地址放入ecx
3.call ecx 调用ecx,取虚函数地址

只有在运行时候才知道寄存器的地址,找到哪个地址调用哪个函数,这就是静态绑定。pb->show(int)同理,是动态绑定。

sizeof变化的原因:
多了virtual,即虚函数会多vfptr指针,因此sizeof()大小也会变。

pb的类型:Base->有没有虚函数
如果Base没有虚函数,*pb识别的就是编译时期的类型。 *pb就是Base类型;
如果Base有虚函数,*pb识别的就是运行时期的类型:RTTI类型,即Derive类型;

我们也可以通过VS命令来查看:
cl -d1reportSingleClassLayout(输出对象内存布局信息)
在这里插入图片描述
在这里插入图片描述
那么我们了解了虚函数,哪些函数不能实现成虚函数呢?
1.要成为虚函数,函数地址就要记录在虚函数表中,即虚函数能产生函数地址,存储在vftable中。
2.vfptr指针需要依赖对象,对象必须存在。找到虚函数表才能找到虚函数地址,虚函数表存储在虚函数指针vfptr中,虚函数指针vfpte在对象的内存中存放。

构造函数:(调用任何函数都是静态绑定的)
1.构造函数不能称为虚函数
2.构造函数中调用虚函数也不会发生静态绑定。

static静态成员方法:静态成员方法调用不依赖对象,因此也不能成为虚函数。

二、虚析构函数

析构函数:可以成为虚函数,调用时候对象存在。
虚析构函数:在析构函数前加上virtual关键字。

什么时候需要把基类的析构函数必须实现成虚函数?
       基类的指针(引用)指向堆上new出来的派生类对象的时候,delete调用析构函数的时候,必须发生动态绑定,否则会导致派生类的析构函数无法调用。
来看一下这段代码:

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

class Derive : public Base
{
public:
	Derive(int data):Base(data), mb(data),ptr(new int(data))
	{
		cout << "Derive()" << endl;
	}
	~Derive()
	{
		delete ptr;
		cout << "~Derive() " << endl;
	}
private:
	int mb;
	int *ptr;
};

int main()
{
	Base *pb = new Derive(10);
	pb->show();//静态绑定pb Base*   *pb Derive
	delete pb;

	return 0;
}

执行结果:执行出现问题
在这里插入图片描述
执行出现问题:派生类的析构函数没有被调用到,内存泄露。
在这里插入图片描述
我们来分析一下问题: pb的类型是Base类型,因此delete调用析构函数先去Base中找Base::~Base(),对于析构函数的调用就是静态绑定,之间编译,没有机会调用派生类的析构函数,最后发生内存泄露。
解决方案: 将基类的析构函数定义为虚析构函数,派生类的析构函数自动成为虚函数。 pb的类型是Base类型,调用析构时去Base中找Base::~Base发现它为虚函数,发生动态绑定。派生类的虚函数表中:&Derive:: ~derive,用派生类析构函数将自己部分进行析构,再调用基类的析构函数将基类部分析构。

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

class Derive : public Base
{
public:
	Derive(int data):Base(data), mb(data),ptr(new int(data))
	{
		cout << "Derive()" << endl;
	}
	~Derive()
	{
		delete ptr;
		cout << "~Derive() " << endl;
	}
private:
	int mb;
	int *ptr;
};

执行结果:成功析构。
在这里插入图片描述

三、深入动态绑定问题

虚函数的调用一定就是动态绑定吗?
在类的构造函数当中,调用虚函数,也是静态绑定。构造函数中调用其他函数,包括虚函数,不会发生动态绑定。

注意:
1.用对象本身调用虚函数,是静态绑定。
2.动态绑定:虚函数前面必须是指针或引用调用才能发生动态绑定:基类指针指向基类对象,基类指针指向派生类对象,都是动态绑定。
3.如果不是通过指针或者引用来调用虚函数,那就是静态绑定。

案例1:我们来看个例子:发生的是静态绑定

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;
}

转到反汇编:发现其为静态绑定
在这里插入图片描述
得出结论:用对象本身调用虚函数,是静态绑定。

案例2:基类指针指向基类对象

Base b;
Derive d;

Base *pb1 = &b;//基类指针指向基类对象
pb1->show();

转到反汇编:为动态绑定
在这里插入图片描述

案例3:基类指针指向派生类对象

Base b;
Derive d;

Base *pb2 = &d;//基类指针指向派生类对象
pb2->show();

转到反汇编:为动态绑定
在这里插入图片描述
案例4:基类指针指向基类对象,基类指针引用派生类对象

Base &rb1 = b;
rb1.show();
Base &rb2 = d;
rb2.show();

与指针一样:都是动态绑定。

案例5:派生类指针调用派生类对象,派生类引用调用派生类对象

Derive *pd1 = &d;
pd1->show();
Derive &rd1 = d;
rd1.show();

转到反汇编:都是动态绑定。

案例6:强制类型转换

Derive *pd2 = (Derive*)&b;
pd2->show();

动态绑定:但最终调用的是Base::show();
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值