文章目录

  • 零.前言
  • 1.多态的概念
  • 2.C++中多态的分类
    • (1)静态多态
    • (2)动态多态
  • 3.多态的构成条件
    • (1)举例
    • (2)两个概念
      • 虚函数
      • 虚函数的重写
    • (3)多态的构成条件
  • 4.虚函数重写的两个例外
    • (1)协变
    • (2)析构函数的重写
  • 5.final与override
    • (1)final
      • 限制类不被继承
      • 限制虚函数不被重写
    • (2)override
  • 6.抽象类
  • 7.总结

零.前言

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++多态的目的在于当我们使用父类的指针或者引用去接收子类的对象后,接收不同的子类对象的父类指针或者引用调用的相同的函数产生的结果不同。
重点在于实现多态的几个条件:
一是用父类的指针或者引用来接收。
二是子类必须对父类的虚函数进行重写。
三。。。三连支持一下博主叭球球了。

C++多态的用法详解相关推荐

  1. 什么是多态,Python多态及用法详解

    什么是多态,Python多态及用法详解 在面向对象程序设计中,除了封装和继承特性外,多态也是一个非常重要的特性,本节就带领大家详细了解什么是多态. 我们都知道,Python 是弱类型语言,其最明显的特 ...

  2. java与python多态的区别_什么是多态,Python多态及用法详解

    在面向对象程序设计中,除了封装和继承特性外,多态也是一个非常重要的特性,本节就带领大家详细了解什么是多态. 我们都知道,Python 是弱类型语言,其最明显的特征是在使用变量时,无需为其指定具体的数据 ...

  3. python的继承用法_【后端开发】python中继承有什么用法?python继承的用法详解

    本篇文章给大家带来的内容是关于python中继承有什么用法?python继承的用法详解,有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助. 面向对象三大特征 1.封装:根据职责将属性和方法 ...

  4. extern用法详解(转)

    extern用法详解(转)       1 基本解释 extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义. 另外,extern ...

  5. extern用法详解

    [转]extern用法详解 Posted on 2011-08-16 11:15 单鱼游弋 阅读(98) 评论(0)编辑收藏 1 基本解释 extern可以置于变量或者函数前,以标示变量或者函数的定义 ...

  6. python的继承用法_python中继承有什么用法?python继承的用法详解

    本篇文章给大家带来的内容是关于python中继承有什么用法?python继承的用法详解,有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助. 面向对象三大特征 1.封装:根据职责将属性和方法 ...

  7. java设计模式观察者模式吗_Java设计模式之观察者模式原理与用法详解

    Java设计模式之观察者模式原理与用法详解 本文实例讲述了Java设计模式之观察者模式原理与用法.分享给大家供大家参考,具体如下: 什么是观察者模式 可以这么理解: 观察者模式定义了一种一对多的依赖关 ...

  8. Python中self用法详解

    Python中self用法详解 https://blog.csdn.net/CLHugh/article/details/75000104 首页 博客 学院 下载 图文课 论坛 APP 问答 商城 V ...

  9. std::function用法详解

    std::function用法详解 代码在:VCCommon/functionDemo std::function 简介 类模板 std :: function 是一个通用的多态函数包装器. std ...

最新文章

  1. ref和out的区别
  2. python读什么英文-django的英文读法是什么
  3. Spring(四)——AOP、Spring实现AOP、Spring整合Mybatis、Spring中的事务管理
  4. spring boot整合freemarker及freemarker基础语法超详细讲解
  5. 30秒无需编码完成一个REST API服务
  6. [SQLite]使用记录
  7. python中自定义模块导入飘红_hadoop streaming 中跑python程序,自定义模块的导入
  8. JSON_dump和load
  9. Java中Math3 各种随机数生成器的使用(Random Generator)
  10. Unix Windows
  11. 系统设计-HIPO图
  12. React小书没提到但是很有用的基础知识
  13. 谷歌邮箱登录服务器设置
  14. beanshell断言_Beanshell断言
  15. 【数学建模】模型的评价、模型的推广与改进
  16. 前端单位的解读和换算px/%/em/rem/vh/vm/vim/vmax
  17. 计算机方向 会议级别
  18. android多个微信支付,想用快速开关一键收付款?Android 版微信没适配但你可以自己做...
  19. 混合模式程序集是针对“v1.1.4322”版的运行时生成的,在没有配置其他信息的情况下,无法在 4.0 运行时中加载该程序集。
  20. 力扣707设计链表(单链表,JavaScript)

热门文章

  1. BAT软件开发岗位面试题汇总
  2. 5个视频素材网站,高清、免费、可商用
  3. nrf51822学习之定时器的探究
  4. [转基因报告:崔永元美国转基因调查纪录片
  5. TDD 的原理和场景
  6. Mybatis出现空指针异常解决方法
  7. 指标的统一管理和分析平台
  8. 解决 The run destination 设备 is not valid for Testing the Xcode doesn’t support iPhone4s’s iOS 15.1
  9. 2022年熔化焊接与热切割考试题库及模拟考试
  10. 我的书单(持续更新)