多态的基本语法
多态分为两类
1.静态多态:函数重载和运算符重载属于静态多态,复用函数名。
2.动态多态:派生类和虚函数实现运行时多态。
静态多态和动态多态的区别:
1.静态多态的函数地址早绑定,编译阶段确定函数地址。
2.动态多态的函数地址晚绑定,运行阶段确定函数地址。
下面我们来看:
在这里插入代码片
#include<iostream>
using namespace std;
class Animal
{
public:
void speak()
{
cout<<"Animal在叫"<<endl;
}
};
class Cat : public Animal
{
public:
void speak()
{
cout<<"Cat在叫"<<endl;
}
};
void DoSpeak(Animal &animal) //Animal & animal = Cat
{
animal.speak();
}
void test01()
{
Cat cat;
DoSpeak(cat);
}
int main()
{
test01();
return 0;
}
结果:
这里我们本来是想传入cat这个对象的地址,输出“Cat在叫”,但是我们写的是函数地址早绑定,所以无论输入什么类,函数传进去的地址都是Animal的地址。如果我们想让Cat说话,就不能使用函数地址早绑定,要在运行阶段再进行函数地址绑定,也就是晚绑定。
解决方法: 在Animal中的Speak()函数前加上关键字virtual,使其变成虚函数,
下面我们再来看一下结果:
下面我们再来补充一个Dog类,
在这里插入代码片
#include<iostream>
using namespace std;
class Animal
{
public:
virtual void speak()
{
cout<<"Animal在叫"<<endl;
}
};
class Cat : public Animal
{
public:
void speak()
{
cout<<"Cat在叫"<<endl;
}
};
class Dog : public Animal
{
public:
void speak()
{
cout<<"Dog在叫"<<endl;
}
};
void DoSpeak(Animal &animal) //Animal & animal = Cat
{
animal.speak();
}
void test01()
{
Cat cat;
DoSpeak(cat);
Dog dog;
DoSpeak(dog);
}
int main()
{
test01();
return 0;
}
结果我们来看:
这里面的这一块代码
在这里插入代码片
void DoSpeak(Animal &animal) //Animal & animal = Cat
{
animal.speak();
}
我们想传入cat,就传入cat的地址;我们传入dog的地址,就传入dog的地址;这是就体现了多态,传入哪一个对象,就走哪一块代码,没有固定了。
下面我们来总结一下动态多态:
1.首先我们要满足一个继承关系;
2.子类要重写父类的虚函数;
!!!重写:函数的返回值类型、函数的名称、函数的参数列表要完全相同;
动态多态使用:
父类的指针或引用 执行子类对象。
引用:Animal & animal = Cat
Animal *p = new Cat;
多态的原理剖析
在这里插入代码片
class Animal
{
public:
virtual void speak()
{
cout<<"Animal在叫"<<endl;
}
};
在加上virtual关键字后,父类函数的内部结构就发生了改变,内部相当于多了一个虚函数指针(vfptr)。
v:virtual ; f:function; ptr:pointer;
虚函数指针指向一个虚函数表(vftable),虚函数表内储存着虚函数地址,当子类继承父类后,会将父类的一份数据继承下来,那么虚函数表中同样也是父类虚函数的地址,当我们在子类中进行重写虚函数时,就会使子类虚函数的地址覆盖掉原来父类虚函数的地址。
这就是多态为什么必须要满足继承和重写这两个步骤。
多态的优点
1.代码的组织结构清晰;
2.可读性强;
3.方便前期和后期的拓展以及维护;
纯虚函数和抽象类
在多态中,通常父类中的虚函数式毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数写为纯虚函数。
纯虚函数语法:
virtual 返回值类型 函数名 (参数列表) = 0;
当类中有了纯虚函数,这个类也称为抽象类;
抽象类特点:
1.无法实例化对象;
2.子类必须重写抽象类中的纯虚函数,否则也属于抽象类,子类也无法实例化对象;
下面我们用多态的形式写一个 :制作饮品
在这里插入代码片
#include<iostream>
using namespace std;
class AbstructDrinking
{
public:
//煮水
virtual void boil() = 0;
//冲泡
virtual void Brew() = 0;
//倒入杯中
virtual void Pour() = 0;
//加入辅料
virtual void Add() = 0;
void operation()
{
boil();
Brew();
Pour();
Add();
}
};
class Tea : public AbstructDrinking
{
//煮水
virtual void boil() {
cout<<"煮农夫山泉"<<endl;
}
//冲泡
virtual void Brew() {
cout<<"冲泡茶叶"<<endl;
}
//倒入杯中
virtual void Pour() {
cout<<"倒入茶杯中"<<endl;
}
//加入辅料
virtual void Add() {
cout<<"加入枸杞"<<endl;
}
};
class Coffee : public AbstructDrinking
{
//煮水
virtual void boil() {
cout<<"煮百岁山"<<endl;
}
//冲泡
virtual void Brew() {
cout<<"冲泡咖啡"<<endl;
}
//倒入杯中
virtual void Pour() {
cout<<"倒入玻璃杯中"<<endl;
}
//加入辅料
virtual void Add() {
cout<<"加入牛奶和糖"<<endl;
}
};
void DoWork(AbstructDrinking *abc)
{
abc->operation();
delete abc;
abc = NULL;
}
void test01()
{
DoWork(new Tea);
cout<<"----------------------------"<<endl;
DoWork(new Coffee);
}
int main()
{
test01();
return 0;
}
感受一下多态带来的好处!!!
虚析构和纯虚析构
多态使用时,如果子类有属性开辟到堆区,那么父类指针在释放时无法调用子类的析构代码;
解决方式:将父类的析构改为虚析构或者纯虚析构;
虚析构和纯虚析构的共性:
1:可以父类指针释放子类对象;
2.都需要有具体的实现;
虚析构和纯虚析构的区别:
如果是纯虚析构,该类也属于抽象类,不能实例化对象;
父类指针在析构时不会调用子类中的析构函数,导致子类如果有堆区属性,出现内存泄漏;
利用虚析构可以解决;
纯虚析构语法:]
类内:声明
virtual ~ 函数名()= 0;
在类外:实现
类名::~函数名()
{
cout<<“纯虚析构的实现”<<endl;
}
✨纯虚析构必须要代码实现,否则就会报错;
如果子类没有堆区属性,可以不写虚析构或纯虚析构;