C++ 多态的原理

什么是多态?
所谓的多态说简单来讲就是不同的对象去完成相同的工作时,会产生不同的状态。
多态分为两种:一种是编译时的多态(静态多态),另一种是运行时的多态(动态多态)。

编译时的多态

编译时的多态的实现与静态连编有关。
(1)那么什么是静态连编呢?
所谓的连编就是将函数名和函数体的代码联系在一起的过程。而所谓的静态连编就是在编译时进行的连编。程序在编译期间,编译器通过对实参与形参的比较,对于同名的重载函数便根据参数上的差异进行区别,然后进行连编,如此就实现了编译时的多态。
(2)编译时的多态可以用两种方式来实现:

  1. 函数重载
  2. 泛型编程

运行时的多态

运行时的多态则是通过动态连编实现的,所谓的动态连编就是在运行阶段完成的连编。即当程序调用到某一个函数时,才去寻找和连接到与之对应的程序代码。

(1)构成多态的条件
  1. 必须通过基类的指针或者引用去调用函数。
  2. 基类中必须包含虚函数,且在派生类中必须完成了虚函数的重写。

问题:什么是虚函数?
成员函数前面加上关键字virtual就是虚函数。

问题:什么是虚函数的重写?
派生类中存在的虚函数与其基类中的虚函数函数名、参数、返回值都相同(协变例外),那么我们就说派生类中的虚函数重写了基类中的虚函数。而且虚函数重写也叫做虚函数的覆盖。

多态的原理

结合下面这段代码进行分析:

class Person
{
	public:
		......
		virtual void show_name();
		virtual void show_age();
		......
};
class Student:public Person
{
	public:
		......
		virtual void show_age();        //重新定义
		virtual void show_all();             //新增的虚函数
		......
}:

虚函数表(虚表):
所谓的虚函数表本质上是一个指针数组,这个数组中的每一个元素都是一个指针,而这个指针指向的是就是虚函数的地址。换句话说,虚函数表中存放的就是虚函数的地址。

虚表指针:
编译器如果发现这个类中有虚函数,那么它就会给在个类的对象中添加一个隐藏的成员,而这个隐藏成员本质上其实就是一个二级指针(vfptr),也就是说,它指向的是函数地址数组的指针,我们把这个指针叫做虚表指针。虚表指针指向的就是虚表的首地址。

问题:派生类是如何继承基类的虚函数?
对于每一个类,编译器都会为其创建一个虚函数表。也就是说,基类对象有一个虚表指针,该指针指向了基类中的虚函数表。派生类中同样也有一个虚表指针,该指针指向了派生类的虚函数表。如果派生类中提供了虚函数的新定义,那么就会在派生类中的虚函数表中添加该新虚函数的地址;如果派生类中没有新定义某个虚函数,那么就会直接在虚函数表中保存原始虚函数的地址;如果在派生类中新定义了一个虚函数,那么就会在虚函数表中直接添加该新虚函数的地址。如下图所示:
在这里插入图片描述
因为在Student类的虚函数表中没有重新定义show_name函数,所以该函数的地址和基类中的地址一致;而show_age函数在Student类中重新定义,所以地址与原来不一样;在Student类中新增加了一个虚函数show_all,所以就直接在虚函数表的后面增加了该函数的地址。

原理:
调用虚函数时,程序将首先找到存储在对象中的虚表指针,然后根据虚表指针再找到虚函数表的首地址。如果要使用类的声明中的第一个虚函数,那么就使用这个数组中的第一个函数地址,然后到该地址去执行这个函数。如果要使用类的声明中的第二个虚函数,那么就使用这个数组中的第二个函数地址,然后到该地址去执行这个函数。

一、虚函数相比于非虚函数的缺点:

1. 内存会增加
(1)每个对象都会增大,因为会多一个虚表指针
(2)编译器会为每一个类都创建一个虚表
2. 执行效率减低
(1)每一次虚函数调用时,都需要执行一个额外的操作:到虚表中查找地址

二、不能作为虚函数的函数类型:

1. 构造函数
(1)因为派生类不继承基类的构造函数,所以没意义。
(2)虚表指针是在构造函数成员初始化列表阶段才初始化的
2. 友元
因为友元不是类的成员,而虚函数必须是成员函数。
3. 全局函数
4. 静态成员函数,它没有this指针,也不是成员函数
5. 拷贝构造函数,以及赋值运算符重载(可以但是不建议作为虚函数)

三、如果基类中有虚函数,析构函数为什么最好也要声明为虚函数?
假如,有这样一句语句:Base* p=Deriver d;那么如果没有将析构函数声明为虚函数会出现什么情况呢?当我们delete p时,因为此时pBase类型,所以它只会调用基类的析构函数,也就是说只能销毁派生类d中属于基类成员的那一部分,而在派生类d中新增的成员而无法被销毁,这就造成了一个诡异的“局部销毁”的局面,从而造成了内存泄漏。因此我们将析构函数声明为虚函数,当我们delete p时,首先调用派生类的析构函数,销毁派生类d中新增的成员,然后再调用基类的析构函数,销毁从基类中继承的成员。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值