多态的概念
多态就是函数调用的多种形态,使用多态能够使得不同的对象去完成同一件事时,产生不同的动作和结果
例如 我们去吃海底捞的时候 普通人去就是原价 学生去就会有学生优惠 这就叫做多态
多态的定义及实现
多态的构成条件
多态是指不同继承关系的类对象,去调用同一函数,产生了不同的行为。语法上 我们这里要满足两个条件
必须通过基类的指针或者引用调用虚函数。
我们会在文章的后面解释 为什么只能用指针或者是引用 不能使用对象
被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
还是一样 我们下面会解释 为什么是虚函数 为什么必须要重写
虚函数
被virtual修饰的类成员函数被称为虚函数。
例如下面的这段代码
class Person { // 虚函数 virtual void Print(); }; int main() { return 0; }
我们的Print就是虚函数
这里有两点需要注意的:
只有类的非静态成员函数前可以加virtual
关于这个问题 因为静态成员函数是没有this指针的
虚函数这里的virtual和虚继承中的virtual是同一个关键字,但是它们之间没有任何关系。虚函数这里的virtual是为了实现多态,而虚继承的virtual是为了解决菱形继承的数据冗余和二义性。
这个是关于virtual的用法 就不用过多解释了
虚函数的重写
虚函数的重写在语法层面上叫做重写
在原理层面上叫做覆盖 后面的例子会让大家明白这一点
它有两个必要条件
必须是虚函数
三同 即 函数名相同 参数相同 返回值相同
还是一样 我们来看代码
class Person { public: virtual void buy_ticket() { cout << "买票 - 原价" << endl; } private: }; class child : public Person { public: // 这里的virtual也可以不写 因为语法规定 只要三同 实际上这里的函数就继承了父类的虚函数属性 // 但是不管我们平时敲代码 或者写项目的时候都要加上去 保证代码的可读性 virtual void buy_ticket() { cout << "买票 - 半价" << endl; } private: }; class soldier : public Person { public: // 为了证明上面说可以省略 virtual 的正确性 这里省略之 void buy_ticket() { cout << "买票 - 优先" << endl; } private: };
现在我们通过父类的对象指针还有引用调用看看能不能完成多态
void func1(Person& p) { p.buy_ticket(); } void func2(Person* p) { p->buy_ticket(); } void test_vritual() { Person p; child c; soldier s; func1(p); func1(c); func1(s); cout << "test ------ ptr" << endl; func2(&p); func2(&c); func2(&s); } int main() { test_vritual(); return 0; }
显示效果如下
虚函数重写的两个例外
协变
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用,称为协变。
比如说我们改写下之前写的代码
我们可以看到这里它们的返回值并不相同 但是依然满足多态 可以运行 这就是协变机制
要记住的一点是 协变的返回值必须是基类或者派生类的指针或引用 不然会报错 类似这样
析构函数的重写
如果父类的析构函数为虚函数 那么只要子类的析构函数定义了 那么它就与父类中的析构函数构成重写
比如说我们看下面的代码
class a { public: virtual ~a() { cout << "~a" << endl; } }; class b : public a { public: virtual ~b() { cout << "~b" << endl; } };
其中 a和b的析构函数就构成多态
怎么证明呢? 我们再来看下面的一段代码
void func(a& p) { p.~a(); } int main() { a a1; b b1; cout << "start test" << endl; func(a1); func(b1); cout << "test end" << endl; return 0; }
运行结果如下
我们可以发现 我们输入不同的对象引用确实触发了不同的析构函数
至于为什么出现了三次析构函数 可以参考下我上一篇继承的博客
派生类对象在析构时,会先调用派生类的析构函数再调用基类的析构函数。
至于后面的三次析构则是 a1 和 b1的生命周期结束了 自动调用的
那么这里的问题就来了
父类和子类的析构函数构成重写的意义何在呢?
我们试想下面的场景
我们创建一个父类对象和一个子类对象 并且使用父类的指针指向它们
然后全部delete掉
a* a1 = new a; a* b1 = new b; delete a1; delete b1;
此时如果没有重写析构函数的话 两次析构其实都是析构的父类的
这样子就会造成一个内存泄漏的情况
而我们期望的是 delete a1 就是析构父类
delete b1 就是析构父类加子类
本质上是一种多态 所以我们要重写
记不记得我们上面继承提过一个知识点
析构函数的名字会被统一处理成destructor();
现在应该能充分理解为什么这么做的原因了吧 为了多态开路
C++11 override和final
我们从上面的博文中就可以看出 C++对于函数重写比较严格 但是我们有可能由于自身的疏忽 导致字符写反 或者返回值写错等原因无法构成重写
而这种错误要在程序运行之后才能被编译器发现 我们觉得有点太慢了
为了解决这个问题 C++中给出了两个关键字 这里我们来一个个学习下它们
final
final:修饰虚函数,表示该虚函数不能再被重写。
我们来看下面的代码
class Person { public: virtual void print() final; private: }; class child : public Person { public: void print() { ; } private: };
运行下我们可以发现
编译的时候会报错 不能够重写
override
override:检查派生类虚函数是否重写了基类的某个虚函数,如果没有重写则编译报错。
我们来看下面的两组对比
重载、覆盖(重写)、隐藏(重定义)的对比
具体的内容看上面这张图就好
抽象类
抽象类的概念
在虚函数的后面写上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类)
抽象类不能实例化出对象
为了证明这个概念 我们写出下面的代码
class person { public: virtual void print() = 0; private: }; int main() { person p; return 0;
我们可以发现 符合我们上面的结论
派生类继承抽象类后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象
比如说这样子
、
class child : public person { public: private: };
接着我们重写下虚函数试试
class child : public person { public: virtual void print() { cout << "child" << endl; } private: };
我们发现 这样子就可以运行了
抽象类既然不能实例化出对象,那抽象类存在的意义是什么?
我们说 意义有二
抽象类可以更好的去表示现实世界中,没有实例对象对应的抽象类型,比如:植物、人、动物等。
抽象类很好的体现了虚函数的继承是一种接口继承,强制子类去重写纯虚函数,因为子类若是不重写从父类继承下来的纯虚函数,那么子类也是抽象类也不能实例化出对象。
接口继承和实现继承
实现继承: 普通函数的继承是一种实现继承,派生类继承了基类函数的实现,可以使用该函数。
接口继承: 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态
总结
本文主要讲解了C++中多态的一些使用