我的编程空间,编程开发者的网络收藏夹
学习永远不晚

C++:多态讲解

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

C++:多态讲解

多态

1.多态的概念

多态的概念:同样的一个行为不同的对象去完成时会产生不同的状态

例子:拿买票举例,军人、学生、普通人(子类)都是(父类),但军人买票可以优选选票,学生买票可以半价,普通人买票就要全价了。因此要实现多态必先继承



2.多态的定义和实现

2.1多态构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如上面的例子定义父类Person,让子类Student继承Preson。Person对象买票全价,Student对象买票半价。

已经形成了继承关系,实现多态还需要以下两个条件

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

大家先记住这两个条件,后面讲解虚函数和多态原理后大家就明白了。


2.2虚函数

虚函数:即被virtual修饰的类成员函数称为虚函数。

class Person {public://这里和菱形虚拟继承公用了关键字,但两者是没有关联的virtual void BuyTicket() { cout << "买票-全价" << endl;}};

2.3虚函数的重写(覆盖)

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
重写比较好理解,覆盖有点原理层面的意思。

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;}

谈一谈实现继承和接口继承:

  1. 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
  2. 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数

虚函数重写的例外:

  1. 协变(基类与派生类虚函数返回值类型不同)
    派生类重写基类虚函数时,与基类虚函数返回值类型可以不同,但必须是父子关系的指针或者引用(也可以是其它父子类),称为协变。
class A{};class B : public A {};class Person {public: virtual A* f() {return new A;}};class Student : public Person {public: virtual B* f() {return new B;}};
  1. 析构函数的重写(基类与派生类析构函数的名字不同)
    析构函数看似不同名,违背了虚函数重写的规则,但为了保证资源的正确释放,编译器会对析构函数做特殊处理,编译后会统一处理为destructor()。
class Person {public:virtual ~Person() { cout << "~Person()" << endl; }};class Student : public Person {public:virtual ~Student() { cout << "~Student()" << endl; }};// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函//数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。int main(){Person* p1 = new Person;Person* p2 = new Student;  //如果父类析构不设计为虚函数的话可能造成内存泄漏delete p1;delete p2;return 0;}

2.4 C++11 override 和 final

C++对虚函数重写的要求比较严格,有时会因为字母次序不同等导致无法构成重写,但这种情况编译器不会报错,等发现运行结果不对再来矫正就太麻烦了。故C++11引入了override和final两个关键字,可以帮助用户检测是否重写。

  1. final:修饰虚函数,表示该虚函数不能再被重写。
// final用来修饰虚函数用处不大,设计虚函数本就是为了让子类重写实现多态class Car{public:virtual void Drive() final {}};class Benz :public Car{public:virtual void Drive() { cout << "Benz-舒适" << endl; }   //这里会报错};//final还可以用来修饰类,被修饰的类不能被继承class A final  {//……};class B : public A  //这里会报错{};
  1. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car {public:virtual void Drive() {}};class Benz :public Car {public:virtual void Drive() override { cout << "Benz-舒适" << endl; }};

2.5重载、重写(覆盖)、隐藏(重定义)的对比

  • 重载:(1)两个函数在同一作用域内。  (2)函数名相同,参数列表必须有区别。
  • 隐藏:(1)两个函数分别在基类和派生类的作用域。  (2)函数名相同即可。
  • 重写:(1)两个函数分别在基类和派生类的作用域。  (2)函数名、参数、返回值都必须相同(除开例外)  (3)两个函数必须都是虚函数。

上面的关系中,隐藏的条件比重写简单,也就是说两个基类和派生类的同名函数不构成重写就构成隐藏

//重载//在同一作用域并且参数列表有区别,属于重载void fun(int a){}void fun(double a){}//隐藏//两个同名函数在基类和派生类作用域内,不构成重写就属于隐藏class A{void fun(int a){}};class B : public A{void fun(int a){}};//重写//两个同名函数在基类和派生类作用域内,并且都是虚函数//函数名、参数列表、返回值都必须一致(除去例外),才能构成重写class C{virtual void fun(int a){}};class D : public C{virtual void fun(int a){}};



3.抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

//抽象类的应用场景:比如图形就可以设计为抽象类//三角形、正方形等继承了该抽象类后重写虚函数,就形成一个具体的类,可以实例化class Graphics{public:virtual double GetArea() = 0{}//抽象类里面可以设计图形共有的接口,比如求面积等};class Square : public Graphics  //正方形{public:virtual double GetArea(){return side_length * side_length;}private:int side_length = 5;};class rotundity : public Graphics  //圆形{public:virtual double GetArea(){return 3.14 * radius * radius;}private:int radius = 5;};int main(){Square sq;rotundity ro;cout << "正方形面积:" << sq.GetArea() << endl;cout << "圆形面积:" << ro.GetArea() << endl;}



4.多态的原理

//sizeof(Base)是多少? --答案是8(我是32位程序),除了一个int还有一个指针变量class Base{public:virtual void Func1(){cout << "Func1()" << endl;}private:int _b = 1;};

在这里插入图片描述

我们再看看这个指针指向的内容:
在这里插入图片描述
我就直接说了,这个指针指向的是一张表(指针数组),表里面存储的是虚函数的函数地址这个表称为虚函数表(简称虚表),这个对象中的指针称为虚表指针。至于为什么不直接在对象中存函数地址,主要是是节省空间(所有对象可公用虚表)。为什么这样设计呢?我们接着往下看。

//编译器是在编译阶段就确定了调用是否满足多态//(1)对于满足多态的调用,编译器通过找虚表指针,接着找到虚表中函数地址进行调用//     多态调用也只是傻傻的执行指令而已//(2)对于不构成多态的普通调用,编译器通过函数名和参数类型就可以确定调用那个函数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 Mike;Func(Mike);Student Johnson;Func(Johnson);//这里不构成多态,对象调用而不是指针或引用,按正常调用规则即可Mike.BuyTicket();  return 0;}

这里可以结合汇编代码来理解
在这里插入图片描述
虚函数重写其实就是继承父类后,拿自己的虚表去覆盖父类的虚表,因此重写也叫覆盖


5.单继承和多继承关系的虚函数表

PS:这一部分不是特别重要。

5.1单继承

有前面的分析,单继承只需要说一点即可:对于那些未重写的虚函数,也是存在虚表中的,至于存储在第一个位置还是最后,就看编译器的实现了。


5.2多继承

class A{public:virtual void fun(int a){}};class B{public:virtual void fun(int b){}};class C : public A, public B  //继承A、B两个类{public:virtual void fun(int c){}};int main(){C c;A* p1 = new C;B* p2 = new C;p1->fun();p2->fun();delete p1, p1 = nullptr;delete p2, p2 = nullptr;return 0;}

在这里插入图片描述

上面的代码中C继承了A、B,并重写了fun(),覆盖两个位置,因此有两个虚表,这也是为了考虑A、B中不同名的虚函数,因此没有合并为一个虚表。但我们发现这两个虚表指向的内容是不一样的!!!
这实际是一种封装,代码中p1和p2最终调用的其实是同一个函数,p2在调用到真正的函数前做了一件事情,那就是调整指针变量指向
在这里插入图片描述


5.3菱形继承和多态

之前说过实际之中应该避免菱形继承,菱形虚拟继承加多态会让对象模型变得异常复杂,这里不细讲,感兴趣的可以看看下面文章:

来源地址:https://blog.csdn.net/2301_76269963/article/details/133843272

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

C++:多态讲解

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

2024-04-02

C# 多线程详细讲解

多线程是指在一个程序中同时执行多个线程,每个线程可以独立执行不同的任务。在 C# 中,可以使用 System.Threading 命名空间中的类来创建和管理多线程。在 C# 中,创建多线程有两种方式:使用 Thread 类或者使用 Thre
2023-09-09

C++cin不同状态详细讲解

cin是C++编程语言中的标准输入流对象,即istream类的对象。cin主要用于从标准输入读取数据,这里的标准输入,指的是终端的键盘。此外,cout是流的对象,即ostream类的对象,cerr是标准错误输出流的对象,也是ostream类的对象
2022-11-13

【C++】多态

🌇个人主页:平凡的小苏 📚学习格言:命运给你一个低的起点,是想看你精彩的翻盘,而不是让你自甘堕落,脚下的路虽然难走,但我还能走,比起向阳而生,我更想尝试逆风翻盘。 🛸C++专栏:C++内功
2023-08-17

C++BoostMetaStateMachine定义状态机超详细讲解

Boost是为C++语言标准库提供扩展的一些C++程序库的总称。Boost库是一个可移植、提供源代码的C++库,作为标准库的后备,是C++标准化进程的开发引擎之一,是为C++语言标准库提供扩展的一些C++程序库的总称
2022-12-08

C#多态性怎么理解

这篇文章主要讲解了“C#多态性怎么理解”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“C#多态性怎么理解”吧!C#多态性的内涵实际就是通过继承,一个类可以用作多种类型:可以用作它自己的类型、任
2023-06-17

【C++】多态(下)

文章目录 1.单继承中的虚函数表整体代码用程序打印虚表如何寻找到虚表地址虚表存在哪里? 2.多继承中的虚函数表整体代码寻找虚表地址注意事项多继承重写后的func1地址为什么不同?ptr1调用函数——一次jmpptr2 调用函数
2023-08-17

C语言动态内存分配图文讲解

给数组分配多大的空间?你是否和初学C时的我一样,有过这样的疑问。这一期就来聊一聊动态内存的分配,读完这篇文章,你可能对内存的分配有一个更好的理解
2023-01-17

C++动态规划中关于背包问题讲解

可能有些读者有接触过动态规划,可能也有一些读者以前完全不知道动态规划这个东西,别担心,我这篇文章会为读者做一个入门,好让读者掌握这个重要的知识点
2023-03-15

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录