本篇要分享的内容是多态,以下为本篇目录。
目录
[2. 多态的定义及实现](#2. 多态的定义及实现)
[4.C++11 override和final](#4.C++11 override和final)
1.多态的概念
通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会
产生出不同的状态。
比如旅行景点的成人票是全票,儿童半票,军人优先购票;
又比如拼多多的红包,新用户就会获得很多福利;而老用户的红包福利只有一点点。
像这样不同的身份可以产生不同的行为和结果,也是一种多态行为。
2. 多态的定义及实现
用一段简单的代码来认识多态
cpp
#include<iostream>
using namespace std;
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "买票-半价" << endl;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
可以看到在多态中引入了一个新的概念: 虚函数(即在函数前加上virtual)。
并且在函数测试中使用父类创建了对象,并且调用函数,这时要注意我们在main函数中分别用父类和子类创建了对象,并调用了Func函数。
在之前的继承中我们知道,父类的引用即可传父类对象,也可以传子类对象
这时运行代码观察结果

可以看到这里调用了两个函数:
普通人买票全价,学生买票半价;
但是当我们将切片改为传值传参,而不是传引用传参时结果就会调用同一个函数

可以看到这里去掉了&符号
运行结果如下

可以看到调用到的时一个函数
那这就证明出形成多态的条件:
①.虚函数重写
②.父类的指针或者函数去调用虚函数
以上条件只要有一个不满足,就会变成普通调用,就会去看对象的类型
3.虚函数
在之前的继承中我们学到过虚继承的关键字也是virtual,但是和我们今天在多态中所学的虚函数是没有关系的,如同取地址符号&,和引用符号&,虽然符号相同,但是是有区别的。
要构建多态的第一个条件就是构成虚函数重写,那么构成虚函数重写也是有条件的:
继承关系中两个父子类关系的虚函数,函数名、参数、返回值,都要相同,才能完成虚函数的重写。
当然,virtual只能修饰成员函数,它的作用是修饰成员函数来构成多态,在类外使用当然是会直接报错的。

但是上述的三同(函数名、参数、返回值相同)又有一个例外,称之为协变:返回值可以不同,但是必须是父子类关系的指针或引用
如图

可以看到我们修改了函数的返回值是父子关系类的指针,结果也会构成多态。
但是在如上的这中情况下,子类又可以不加virtual

为什么要这样设计呢?
这里的函数需要重写,重写就是重复实现,也可以认为在父类中的虚函数,子类与父类同名的虚函数virtual也会继承下来。
以上的两种特殊用法是需要记忆的,是可以使用,但是在我们自己编写代码中最好还是使用三同来完成多态。
接下来则是析构函数,有如下场景
cpp
class Person {
public:
~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
~Student() { cout << "~Student()" << endl; }
};
int main()
{
Person* p1 = new Person;
delete p1;
Person* p2 = new Student;
delete p2;
return 0;
}
这里的分别在两个类中定义了两个析构函数,为方便观察我们输出他们。
再在main函数中分别定义两个指针,并且分别指向父类和子类并析构
运行结果如下

可以看到创建的两个指针指向的类不同,但是却同时调用了父类的析构函数,原因是在没有使用virtual修饰函数的情况下,所有的析构函数都被编译器命名为:destroy(),所以在两个不同的类中相当与重写了析构函数。
因为子类不能正常调用析构函数,所以有可能会造成内存泄漏。
所以我们需要使用虚函数就可以使他们分别调用自己类中的析构函数

(子类调用析构函数是先子后父)。
4.C++11 override和final
在C++11中更新了两个关键字final和override;
4.1final关键字
1.final修饰类时,这个类不能被继承
2.final修饰虚函数时,这个虚函数不能被重写
用法如下
cpp
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() { cout << "Benz-舒适" << endl; }
};
此处实在Car类中的虚函数后加上final {};

可以看到使用final之后,这个虚函数就不能被重写了
并且这个函数必须是虚函数

这里报错很明显,final不能修饰非虚函数。

在Car类后加上final之后这个类就不能被继承了 。
4.2override关键字
override用来修饰派生类的虚函数,用来检测是否完成重写。
以下是override的使用位置
cpp
class Car
{
public:
virtual void Drive()
{}
};
class Benz :public Car
{
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
如果没有完成重写就会报错
虚函数一定是要重写的,否则虚函数是没有意义的。
5.抽象类
5.1抽象类的概念
上面我们提到过虚函数。在虚函数后面加上=0,则这个函数为纯虚函数
那么此时,包含整个纯虚函数的类叫做抽象类,也叫做接口类,抽象类不能实例化对象。


可以看到上图的Car中有纯虚函数,Car就变成了抽象类,此时Car也就不能定义对象,但是可以定义指针。

上图中我们定义了新的类,并且继承了抽象类Car,可以看到此时新的类也不能实例化对象了;
但是我们可以将纯虚函数重写,这样就可以实例化对象了

所以有以下使用场景
cpp
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
void Drive()
{
cout << "Benz" << endl;
}
};
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
void func(Car* c)
{
c->Drive();
}
int main()
{
func(new Benz);
func(new BMW);
/*Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();*/
}
在以上代码中我们发现在func函数中的参数定义了抽象类的指针对象c,利用c去调用类中重写的函数。
同时在main函数中调用函数,不难看出指向哪个子类就调用的是哪个子类重写的函数。
所以我们可以得出的结论是抽象类强制了子类去重写。
5.2接口继承和实现继承
普通函数的继承是一种实现继承
虚函数的继承是一种接口继承
用代码说明

首先是实现继承,我们在父类中简单写了一个输出函数,他会直接继承到子类中,也就是说子类可以直接对func函数直接进行调用,可以理解为一种函数的复用。
而虚函数的接口继承,相当于需要在子类中重写函数的实现,但是调用的参数,或者说是接口,还是父类的接口,所以我们才需要重写虚函数。
最后用一道小题来了解接口继承
cpp
class A
{
public:
virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
virtual void test(){ func();}
};
class B : public A
{
public:
void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
B*p = new B;
p->test();
return 0;
}
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
首先观察main函数中使用B类创建了指针p;
使用p去调用test,test在父类中,所以test中的this指针是指向A类
在test中又调用了func函数
因为func在A类中被定义为虚函数,并且在B类中重写,在加上虚函数为接口继承,
虚函数只重写函数体内容,而接口还是父类的接口
所以答案为:B->1
以上就是本篇要分享的关于多态的概念和简单实用,本人水平有限,尽管不遗余力但本篇的内容仍有不足,还请读者指正,感谢您的阅读。