C++ 多态
卖寂寞的小男孩 人气:2前言
C++多态是在继承的基础上实现的,了解多态之前我们需要掌握一定的C++继承的知识,本文将介绍C++中多态的概念,构成条件以及用法。
1.多态的概念
多态,通俗来讲就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。比如,在买票这一行为,普通人买票是全价买票,学生买票是半价买票,而军人买票是优先买票;再比如动物园的动物叫这个行为,不同的动物叫声是不一样的。这些都是生活中多态的例子。
2.C++中多态的分类
(1)静态多态
静态多态是指在编译时实现的多态,比如函数重载,看似是调用同一个函数其实是在调用不同的。
比如我们使用cout这个对象来调用<<时:
int i=1; double d=2.2; cout<<i<<endl; cout<<d<<endl;
虽然调用的都是<<,但其实调用的是不同的操作符重载之后的函数。
函数重载在之前的文章中详细讲解过,这里就不再赘述。
(2)动态多态
动态多态也就是我们通常所说的多态,本文以下内容均为动态多态内容。动态多态是在运行中实现的,
当一个父类对象的引用或者指针接收不同的对象(父类对象or子类对象)后,调用相同的函数会调用不同的函数。
这段话也许比较绕,这里只是给出一个概念,可以结合下面的例子来进行理解。
3.多态的构成条件
(1)举例
我们先根据一个构成例子来理解多态构成的条件:
#include<iostream> #include<string> 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 p1; Student p2; Func(p1); Func(p2); }
我们先来看这样一段代码,其中子类Student继承了父类。运行起来打印的结果是:
我们在反观上述中动态多态的定义,用父类的引用或者指针(这里使用的是Person& p)来接收不同类型的对象(p1和p2),该引用或指针调用相同的函数(都调用了p.BuyTicket()),都调用了各自类中不同的函数(打印的结果不同)。我们将这一过程称为动态多态。
如果我们不传指针或者引用,那么将不构成多态(原理会在多态原理中详细解读)。
(2)两个概念
在解释多态的构成条件之前我们还需要了解两个概念。
虚函数
虚函数,即被virtual修饰的类成员函数称为虚函数。
比如上面代码中父类和子类的成员函数就是虚函数。
virtual void BuyTicket() { cout << "全价买票" << endl; }
关于虚函数还需要注意几点:
1.普通的函数不能是虚函数,只能是类中的函数。
2.静态成员函数不能加virtual
总结起来就是只能是类的非静态成员函数才能去形成虚函数。
虚函数的重写
虚函数的重写又称为虚函数的覆盖(重写是表面意义上的,覆盖是指原理上的):派生类中有一个根基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型,函数名,参数列表完全相同),称子类的虚函数重写了父类的虚函数。
//父类中的虚函数 virtual void BuyTicket() { cout << "全价买票" << endl; } //子类中的虚函数 virtual void BuyTicket() { cout << "半价买票" << endl; }
两个虚函数满足返回值类型相同,函数名相同,参数列表相同。因此子类的虚函数重写了父类的虚函数。
注意,只有虚函数才能构成重写。
(3)多态的构成条件
多态的构成满足两个条件:
1.必须通过基类的指针或者引用调用虚函数。
2.被调用的虚函数的派生类必须完成了对基类虚函数的重写。
我们在来看上面的代码,确实满足该条件:
1.使用了父类引用p来调用虚函数。
2.派生类的虚函数完成了对基类的虚函数的重写。
我们首先要明确使用多态的目的,就是使用不同的对象去完成同一个任务的时候会产生不同的结果。
如果我们拿掉以上任何一个条件都不会再构成多态,比如我们不使用指针或者引用去接收对象从而调用虚函数,而是使用对象呢?
void Func(Person p) { p.BuyTicket(); }
此时我们会发现,打印的结果发生了变化:
这是不满足我们的预期的,因为不同的对象传给了p,p调用相同的函数却打印了相同的结果。
我们还可以将更改参数列表或者将父类的virtual拿掉,发现依然不是我们想要的结果。
但是有两个特殊的情况除外:
4.虚函数重写的两个例外
(1)协变
如果我们将父类和子类中的虚函数的返回值设为不同,可能会发生如下报错:
协变指的是:派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类的虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
简单来说就是两者的返回值是父子关系的指针或者引用。
举一个例子:
class A{}; class B:public A {}; class Person { public: virtual A* BuyTicket() { A a; cout << "全价买票" << endl; return &a; } }; class Student :public Person { public: virtual B* BuyTicket() { B b; cout << "半价买票" << endl; return &b; } };
我们将上一段代码进行了改写,定义了B继承A,而在Person和Student两个类中的虚函数中将返回值分别置为A和B,由于A和B是继承关系,所以仍然可以构成多态,我们称派生类的虚函数为基类的虚函数的协变。
注意返回值必须是指针或者引用,对象不会构成协变。
(2)析构函数的重写
首先我们先回顾一下没有构成多态的析构函数调用:只需要子类对象销毁时无需手动销毁父类对象,会自动调用父类对象的析构函数。
1.如果基类的析构函数为虚函数,此时子类的析构函数无论加不加virtual,都是对父类的析构函数的重写。
2.虽然子类和父类的析构函数的函数名不同,但其实编译器对析构函数的名称进行了特殊的处理,都处理成了destructor。
下面举例说明,将Person和Student写入析构函数:
//父类中的析构函数 virtual ~Person() { cout << "~Person" << endl; } //子类中的析构函数 virtual ~Student() { cout << " ~Student" << endl; } //主函数 Person* p1 = new Person; Person* p2 = new Student; delete p1; delete p2;
构成多态的结果是,Person*类型的p1和p2,接收两个不同类型的对象即Person类型和Student类型,在调用析构函数的时候可以分开调用(子类对象调用子类的析构函数,父类对象调用父类的析构函数。)
我们将上述代码运行一下,会发现:
结果的确是如此,当析构父类对象时,调用父类的析构函数,当析构子类对象时,调用的是子类的析构函数和父类的析构函数。
如果我们不使用父类指针进行管理,而是使用对象来接收子类对象呢?
Student p2; Person p3 = p2;
此时我们发现打印的结果是:
在析构p3的时候,并没有根据按Student类的规则来进行析构。
同时,当我们将派生类的virtual去掉的时候,仍然可以构成多态,这与底层原理有关,在下面的介绍中会提及。为了统一性,不建议将virtual拿掉,C++大佬为了防止发生不必要的内存泄漏,所以设置了这一规则。这就导致所有的其实派生类的所有虚函数virtual都可以省略。这是由于其继承了基类的virtual属性,具体的还要在底层去理解,再强调一遍,尽量不要在派生类中省略virtual。
5.final与override
(1)final
限制类不被继承
但我们想要设计一个不被继承的类时,目前我们知道的有一种方法:就是将父类的构造函数设为私有(这是因为子类需要调用父类的构造函数来进行初始化)。如果使用这种方式,定义父类对象的话需要使用单例模式。
final提供了另一种方式来限制一个类不会被继承。
只需要在不想被继承的类后加final即可:
class Person final { public: virtual A* BuyTicket() { A a; cout << "全价买票" << endl; return &a; } virtual ~Person() { cout << "~Person" << endl; } };
此时如果子类去继承Person的话会报错。
限制虚函数不被重写
当我们在函数后加上final的时候,该虚函数将不能被子类中的虚函数重写,否则会发生报错。
virtual A* BuyTicket() final { A a; cout << "全价买票" << endl; return &a; }
(2)override
将override放在子类的重写的虚函数后,判断是否完成重写(重写的是否正确)
virtual B* BuyTicket(int i=10) override { B b; cout << "半价买票" << endl; return &b; }
注意:final关键字放在父类的位置,override关键字放在子类的位置。
6.抽象类
在虚函数的后面加上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫做接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象。**只有重写虚函数,派生类才能实例化出对象。**注意虽然不能实例化出对象,但是可以定义指针。
抽象类的存在本质上来说就是希望我们在派生类中重写父类的虚函数。抽象类中的虚函数一般只声明,不实现,因为没有意义。我们可以搭配override来使用。
//将父类中写入纯虚函数,父类变成抽象类 class Person { public: virtual A* BuyTicket() =0//纯虚函数 { A a; cout << "全价买票" << endl; return &a; } virtual ~Person() { cout << "~Person" << endl; } };
此时子类必须只有重写虚函数才能定义对象。通常情况下现实中没有的事物,定义成抽象类会比较合适。
虽然我们不能使用抽象类来定义对象,但是我们可以使用抽象类来定义指针。
class Car { public: virtual void Drive() = 0 { cout << " Car" << endl; } }; class Benz :public Car { public: virtual void Drive() { cout << "Benz" << endl; } void f() { cout << "f()" << endl; } }; int main() { //Car* p = nullptr; //p->Drive();//程序会崩溃 Car* a = new Benz; a->Drive(); }
我们可以使用父类指针去接收子类对象,同时调用函数。但是不能使用父类去创建对象。
7.总结
C++多态的目的在于当我们使用父类的指针或者引用去接收子类的对象后,接收不同的子类对象的父类指针或者引用调用的相同的函数产生的结果不同。
重点在于实现多态的几个条件:
一是用父类的指针或者引用来接收。
二是子类必须对父类的虚函数进行重写。
加载全部内容