一.概述
C++的多态和Java中的类似,都是使用父类来接收子类,但需要注意的是,C++中使用多态接收子类引用,这个引用只能调用父类和子类重名的函数,即虚函数。不能调用子类的特有函数
1.分类
-
静态多态(编译时多态,早绑定)
包括函数重载、运算符重载
-
动态多态(运行时多态,晚绑定)
主要就是虚函数实现的多态,也是这里主要讲的
2.动态多态的实现条件
动态多态不能调用子类的特有函数⚠
- 父类声明虚函数
- 子类重写父类的虚函数
- 将父类指针指向子类对象,通过父类指针访问虚函数
二.虚函数实现多态
1.虚函数
(1)作用
**虚函数的作用:**让子类对虚函数进行重新定义 -> 函数重写
- 若类中声明了虚函数,且子类重新定义了虚函数,当父类指针或父类引用操作子类对象调用函数时,系统会自动调用派生类中的对虚函数的重写代替父类中的虚函数
(2)注意事项
- 构造函数不能声明为虚函数,但析构函数可以声明为虚函数
- 虚函数不能是静态成员函数
- 友元函数不能声明为虚函数,但虚函数可以作为另一个类的友元函数
- 虚函数只能是类的成员函数,不能是类外的普通函数
(3)格式
虚函数的声明方式是在成员函数的返回值类型前添加virtual
关键字
class 类名{
权限控制符:
virtual 函数返回值类型 函数名 (参数列表);
//...其它成员
};
2.实现多态(函数重写)
C++的函数重写只是对虚函数中的虚函数地址进行了覆盖,父类中的虚函数并没有被覆盖,依旧可以通过作用域运算符对其进行访问
将父类的Speak()
方法定义成了虚函数,在子类中又对其进行了重写,所以在后面使用父类指针接收子类指针并使用接收的父类指针调用虚函数时,调用的是子类的Speak()
方法
代码:
#include
using namespace std;
class Animal{
public:
//声明Animal类的Speak方法为虚函数
virtual void Speak(){
cout << "动物说话" < Speak();//狗说话
}
int main(){
Dog* dog = new Dog;
Test(dog);
}
结果:
狗说话
3.虚函数的原理
(1)概述
虚函数的底层类似于虚继承,都存在着一个指针(虚函数指针),指向一张虚函数表,如果子类没有对虚函数进行重写,那么该表记录的就还是重父类继承下来的虚函数,但如果子类对虚函数进行了重写,那么子类重写虚函数的地址就会替代该表中的地址
**场景:**使用父类指针接收子类对象时,如果子类有重写虚函数,那么调用的将会是子类重写的虚函数
- 原因:虚函数的访问都是需要先从虚函数指针访问虚函数表获取地址再访问的,虽然父类的指针只能访问父类区域的数据,但父类区域的虚函数指针所指向虚函数表中的虚函数地址已经被替换,所以其指向的会是子类重写的虚函数地址
(2)图示
这就是上面虽然是使用父类的
Animal *
调的Speak,最终调用的却是子类的Speak的原因
只覆盖了虚函数表的引用,没覆写方法
- 只是修改(覆盖)了虚函数表,从父类继承过来的代码依旧是存在的,所以可以通过加作用域对其进行访问
三.纯虚函数-抽象类
1.概述
(1)简述&目的
**简述:**纯虚函数就类似于Java中的抽象函数,它只声明不实现,具体的实现由继承它的子类进行,相较于虚函数省略了父类无意义的实现
**目的:**设计类的接口
(2)注意事项:
- 一旦类中有纯虚函数,那么这个类就是抽象类
- 抽象类不能实例化对象
- 抽象类必须被继承,同时子类必须重写父类的所有纯虚函数,否则子类也是抽象类
2.格式
纯虚函数子类必须对其进行重写
在虚函数的基础上,去除其函数体,然后右边添加= 0
即可
class Animal {
public:
virtual void Speak() = 0;
};
3.示例
代码:
定义了一个抽象函数AbstractDrinking,并声明制作饮品的通用函数,子类Coffee继承父类,重写了父类中的抽象方法
//抽象制作饮品
#include
using namespace std;
class AbstractDrinking {
public:
//烧水
virtual void Boil() = 0;
//冲泡
virtual void Brew() = 0;
//倒入杯中
virtual void PourInCup() = 0;
//加入辅料
virtual void PutSomething() = 0;
//规定流程
void MakeDrink() {
this->Boil();
Brew();
PourInCup();
PutSomething();
}
};
//制作咖啡
class Coffee : public AbstractDrinking {
/*重写父类的抽象函数*/
public:
//烧水
virtual void Boil() {
cout << "烧水!" << endl;
}
//冲泡
virtual void Brew() {
cout << "冲泡咖啡!" << endl;
}
//倒入杯中
virtual void PourInCup() {
cout << "将咖啡倒入杯中!" << endl;
}
//加入辅料
virtual void PutSomething() {
cout << "加入牛奶!" << endl;
}
};
//业务函数
void DoBussiness(AbstractDrinking *drink) {
drink->MakeDrink();
delete drink;
}
int main() {
DoBussiness(new Coffee);
}
结果:
煮农夫山泉!
冲泡咖啡!
将咖啡倒入杯中!
加入牛奶!
四.虚析构函数
1.概述
简述:为析构函数添加virtual
关键字,就可以让析构函数变成虚析构函数了
**应用:**当需要使用父类的指针,释放整个子类的空间时,就需要将父类的析构函数变成虚析构函数(原理见下)
2.格式
在类析构函数函数名的前面添加virtual关键字,就可以使其变成虚析构函数了
class Father{
public:
//定义虚析构函数
virtual ~Father(){
}
};
3.父指针析构子类的原理
**前提:**析构的顺序 子类 -> 成员 -> 父类
- 虚析构函数只是保证最终调用的子类的析构函数,至于父类的析构,是编译器自动调用的
**原理:**当父类的析构函数被声明为虚析构函数后,子类继承父类析构函数后,该虚函数指针指向的虚函数表中的析构函数地址就会变成子类的析构函数,所以当父类指针通过虚函数指针调用析构函数时,就会调用到子类的析构函数,之后编译器就会自动调用成员、父类的析构,进而完成对整个在子类空间的释放
子类中虚函数指针指向的析构函数会自动替换成子类的析构,促使通过虚函数指针调用的是子类的析构
![image-20240316204606062](https://honescos.oss-cn-hangzhou.aliyuncs.com/img/image-20240316204606062z9e7LtMrIP2024031620468be6327678e70c6a.png)
4.示例
代码:
#include
using namespace std;
class Animal {
public:
//声明Animal类的Speak方法为虚函数
virtual void Speak() {
cout << "动物说话" << endl;
}
virtual ~Animal() {
cout << "Animal析构函数" << endl;
}
};
class Dog :public Animal {
public:
//重写父类的虚函数
void Speak() {
cout << "狗说话" << endl;
}
~Dog() {
cout << "Dog析构函数" << endl;
}
};
void Test(Animal* p) {//使用父类指针接收子类指针
p->Speak();//狗说话
delete p;
}
int main() {
Dog* dog = new Dog;
Test(dog);
}
**结果:**先调用的是子类的析构,然后编译器会自动调用父类的析构
狗说话
Dog析构函数
Animal析构函数
五.纯虚析构函数
1.概述
(1)简述&作用
**简述:**纯虚析构函数主要用于在父类定义一个析构函数接口,提示子类必须提供析构函数,从而确保在删除派生类对象时能够正确地调用基类的析构函数
- 即使子类没有提供析构,也不会出现警告,仅仅起个提示作用
**作用:**让C++的抽象类更像接口😂(C++没有提供接口,只能靠虚函数迂回实现)
/*类内只有声明,没有任何实现*/
class Animal {
public:
virtual void Speak() = 0;
virtual ~Animal() = 0;
};
(2)纯虚析构和虚析构:
- 虚析构:
virtual
关键字修饰,有函数体,不会导致父类变成抽象类 - 纯虚析构:
virtual
关键字修饰,函数声明=0,函数体在类外实现,会导致父类成为抽象类
2.格式
纯虚函数只是一个语法,所以,尽管在类中他被声明为了纯虚函数,但在类外,还是需要对他进行定义的,否则会造成内存泄露
class Animal {
public:
//类内将其声明为一个纯虚析构函数
virtual ~Animal() = 0;
};
//类外实现析构函数
Animal::~Animal() {
cout << "Animal析构函数" << endl;
}
3.示例
就我的感觉,它唯一的作用就是让抽象类更像一个接口
#include
using namespace std;
class Animal {
public:
virtual void Speak() = 0;
virtual ~Animal() = 0;
};
Animal::~Animal() {
cout << "Animal析构函数" << endl;
}
class Dog :public Animal {
public:
//重写父类的虚函数
void Speak() {
cout << "狗说话" << endl;
}
~Dog() {
cout << "Dog析构函数" << endl;
}
};
void Test(Animal* p) {//使用父类指针接收子类指针
p->Speak();//狗说话
delete p;
}
int main() {
Dog* dog = new Dog;
Test(dog);
}
六.重载、重定义、重写
重载: 函数名相同,参数的顺序、类型、数量不同,就可以构成重载
重定义: 出现在有继承的情况下,子类定义了与父类同名的函数(该函数不是虚函数),对参数的顺序、类型、数量没有要求,会屏蔽父类中所有的重名方法
- 可以通过附加作用域运算符进行访问
重写: 出现在有继承的情况下,子类重写了父类的虚函数,要求参数的顺序、类型、数量与虚函数一致
- 实际是覆写了重父类继承过来的虚函数表,将它内虚函数的地址替换子类重写的虚函数的地址
- 父类的虚函数依旧是继承了的,可以通过附加作用域运算符进行访问