多态是C++面向对象的三大特性之一
多态分为两类:
- 静态多态:函数重载和运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
区别:
- 静态多态地址早绑定,编译阶段确定函数地址
- 动态多态地址晚绑定,运行阶段确定函数地址
下面通过案例来讲解多态的基本使用
#include<bits/stdc++.h>
using namespace std;
class Animal {
public:
// speak 函数就是一个虚函数
//在函数前面加上关键字 virtual 函数就会变成虚函数,那么编译器在编译的时候就不能确定函数调用了,也就对应了上文所说的地址晚绑定的情况
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat :public Animal
{
public:
void speak()
{
cout << "小猫在说话" << endl;
}
};
class Dog :public Animal
{
public:
void speak()
{
cout << "小狗在说话" << endl;
}
};
//我们传入什么对象,那么就调用什么对象的函数
//如果函数的地址在编译阶段就能确定,那就是静态,反之是动态,这里就是一个动态
/*多态满足条件:
* 1、有继承关系
* 2、子类重写父类中的虚函数,重写:函数返回值类型、函数名、参数列表 完全一致称为重写
* 多态的使用:
* 父类指针或引用指向子类对象
*/
void dospeak(Animal& animal)
{
animal.speak();
}
int main()
{
Cat c;
dospeak(c);
Dog d;
dospeak(d);
return 0;
}
那么虚函数又是怎么实现的呢?为什么明明形参是 Animal& 类型的,但是传入Cat或Dog类型的对象也能成功调用呢?以及我们这么做的意义又在哪里呢?我们一一来解析。
虚函数是怎么实现的以及为什么传Cat/Dog类型也能成功
首先我们把虚函数还原,让他成为一个正常的函数
class Animal {
public:
void speak()
{
cout << "动物在说话" << endl;
}
};
那么我们来看一看这个Animal类的sizeof是多少?很明显是1哈(不信的小伙伴可以自己试一试哈)。
那么为什么是1呢,我们知道类中的非静态成员函数是不属于类的对象上的,而类本身就占了一个字节,所以结果很显然是1。这个时候我们让它变成一个虚函数,也就是加上virtual关键字,再来看一看这个Animal类的sizeof是多少
class Animal {
public:
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
答案是4(不信的同学可以自己试一试哈,当然有些人可能是8,这和电脑有关系)。
那么这次加上virtua关键字怎么又变成4了嘞,难道说一个virtual占了4字节吗?不用想,显然是我们的类中多了点东西,才会让它变成4。为了看看这个多了的东西是什么,我们打开vs的开发人员命令行提示符,看一看Animal类的对象模型
欸,确实是4个字节,但是那个vfptr是个什么东西?下面那个vftable又是什么?
虚函数指针,虚函数表
上面的vfptr是一个虚函数指针(也叫虚函数表指针),指向的是下面的那个vftable虚函数表,其中vftable中存放的是虚函数的函数地址,也就是那个&Animal::speak。
那么说到这里,或许也就有些思路了。
在上面提到过,为什么是Animal&类型,但是传Cat/Dog类型也能调用,答案来了
首先我们要知道一件事情,那就是C++允许父子之间进行类型转换,它不需要强制类型转换。
然后,我们来看一看Cat的对象模型就知道是怎么调用到Cat的speak函数的了,同时我们把Cat中的speak函数删了,看看为什么要在子类中重写父类的虚函数
这是没删的:
这是删了的:
很明显Cat里面也有一个vfptr(继承嘛,该有的还是有),它指向的是Cat的vftable,但是这个时候我们可以看到,在没删speak函数的里面记录的地址变成了&Cat::speak,而删掉了speak函数的里面记录的还是&Animal::speak,那么我们也就知道了为什么传Cat给Animal&也能调用到Cat的speak了,因为如果子类中重写了父类的虚函数,那么在vftable里面子类的speak函数地址就会覆盖掉父类中的speak函数地址,那么也就能正确找到Cat的speak函数,反之则还是父类的speak,那么到这里,多态是如何实现的也就差不多了(注意多态的使用条件,上方代码注释中有),接下来就是为何要使用多态。
为什么要使用多态?
多态的优点:
- 代码组织结构清晰
- 可读性强
- 利于前期和后期的扩展和维护
这一般是在大项目里体现的更加明显,我们目前可能会认为用了多态不是多此一举吗,这代码量明显增加了啊,其实在后期的功能扩展以及维护中,我们就能体现出多态的好处了。但是不妨碍有人会有这种疑惑,比如我使用了多态时,父类中的虚函数我好像很少去调用它,或者是说父类中的虚函数的函数实现好像没什么必要。接下来我们说一说抽象类duo
纯虚函数、抽象类
首先我们说一说什么是纯虚函数,在多态中,通常父类的虚函数实现是毫无意义的,主要都是调用子类中重写的内容,这个时候我们可以将虚函数改为纯虚函数
class Animal {
public:
virtual void speak() = 0;
};
这样就是一个纯虚函数,很简单。同时,当一个类中有了纯虚函数,这个类也叫做抽象类
抽象类的特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
当你使用了抽象类来进行多态,这个时候抽象类中的纯虚函数就更像是一个模板或者叫做接口,当你在以后想要拓展新功能的时候,直接在子类中重写就可以了,出了问题也可以直接定位到子类中去修改。
虚析构、纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构(我们前面讲了多态的使用是通过父类的指针或引用来指向子类对象的)
解决方式:将父类中的析构改为虚析构或纯虚析构
虚析构和纯虚析构的共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
区别:如果是纯虚析构,那么该类为抽象类,无法实例化对象
上代码,注释部分为虚析构的调用,注意在子类中依然要重写属于子类的析构(这里千万不要与父类的析构同名,只要写自己的析构就可以了)
虚析构语法:virtual ~类名(){}
纯虚析构语法:virtual ~类名() = 0; 类名::~类名(){}
class Animal {
public:
virtual void speak() = 0;
/*virtual ~Animal()
{
cout << "Animal的纯虚析构函数调用" << endl;
}*/
virtual ~Animal() = 0;
};
Animal::~Animal()
{
cout << "Animal的纯虚析构函数调用" << endl;
}
我们可以看到纯虚析构有点不太一样,它有点类似与静态成员的写法(类内声明,类外初始化),当然这里类外要加上作用域,告知编译器属于哪一个类的析构。
到这里基本的多态也就差不多了,如果要熟练掌握多态,还需深入学习,感谢大家观看!