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

【C++】多态

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

北京

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

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

看不清楚,换张图片

免费获取短信验证码

【C++】多态

🌇个人主页:平凡的小苏
📚学习格言:命运给你一个低的起点,是想看你精彩的翻盘,而不是让你自甘堕落,脚下的路虽然难走,但我还能走,比起向阳而生,我更想尝试逆风翻盘
🛸C++专栏C++内功修炼基地
> 家人们更新不易,你们的👍点赞👍和⭐关注⭐真的对我真重要,各位路 过的友友麻烦多多点赞关注。 欢迎你们的私信提问,感谢你们的转发! 关注我,关注我,关注我,你们将会看到更多的优质内容!!

在这里插入图片描述

一、多态的概念

1.1、概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态

举个栗子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票

1.2、多态的定义及实现

1.2.1、多态的构成条件

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

在继承中构成多态需要两个必要条件

  1. 必须通过基类的指针或者引用调用虚函数

  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

#includeusing namespace std;class Person {public:virtualvoid BuyTicket() const { cout << "买票-全价" << endl; }};class Student : public Person {public:void BuyTicket() const { cout << "买票-半价" << endl; }};void func(const Person* p){p->BuyTicket();}int main(){Person pp;func(&pp);Student st;func(&st);return 0;}
  • 多态,不同对象传递过去,调用不同函数

  • 多态调用看的指向的对象

  • 普通对象,看当前者类型

在这里插入图片描述

注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用.

1.3、虚函数重写的例外

1.3.1协变

  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. 析构函数的重写(基类与派生类析构函数的名字不同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则**,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

为什么需要让析构函数构成重写呢?如下场景

class Person {public:virtual void BuyTicket() { cout << "买票-全价" << endl; }~Person() { cout << "~Person()" << endl; }};class Student : public Person {public:virtual void BuyTicket() { cout << "买票-半价" << endl; }~Student() {cout << "~Student()" << endl;delete[] ptr;}protected:int* ptr = new int[10];};int main(){Person* p = new Person;p->BuyTicket();delete p;p = new Student;p->BuyTicket();delete p; // p->destructor() + operator delete(p)// 这里我们期望p->destructor()是一个多态调用,而不是普通调用return 0;}

注意:如果我们不让析构函数进行重写的话,它就构成了隐藏了,所以就调用不到子类的析构函数 这样会导致在子类进行堆申请的空间得不到释放,导致了内存泄漏。

1.4、C++11override和final

C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

  1. final:修饰虚函数,表示该虚函数不能再被重写
class Car{public:    virtual void Drive() final {}};class Benz :public Car{public:    virtual void Drive() {cout << "Benz-舒适" << endl;}};

final:修饰类,表示不能被继承

class A final{public:private:};class B : public A{};int main(){return 0;}

override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

class Car{public:    virtual void Drive(){}};class Benz :public Car {public:    virtual void Drive() override {cout << "Benz-舒适" << endl;}};

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

在这里插入图片描述

二、多态的原理

多态的条件中:

2.0、父类的指针和引用

问题:为什么不是子类的指针和引用。为什么不是父类的对象

因为父类的指针和引用能够指向父类或者子类,而子类的指针和引用只能指向子类

#includeusing namespace std;class Person {public:virtual void BuyTicket() { cout << "买票-全价" << endl; }virtual void Func1() {cout << "Person::Func1()" << endl;}virtual void Func2() {cout << "Person::Func2()" << endl;}//protected:int _a = 0;};class Student : public Person {public:virtual void BuyTicket() { cout << "买票-半价" << endl; }private:virtual void Func3(){//_b++;cout << "Student::Func3()" << endl;}protected:int _b = 1;};int main(){Person ps;Student st;st._a = 10;ps = st;Person* ptr = &st;Person& ref = st;return 0;}

问题:为什么不是父类的对象

因为子类赋值给父类对象切片不会拷贝虚表,如果拷贝虚表那么父类的对象虚表中是父类虚函数还是子类虚函数就不确定了

注意:所以不能是父类的对象,而父类的指针和引用为什么可以呢,因为子类继承了父类的虚表当发生重写后,他就会指向子类的虚函数,如下图:

在这里插入图片描述

虚函数的重写

虚函数的重写是继承了接口,重写了实现。因为重写后,指向父类调用父类的函数,指向子类就可以调用子类的函数。

2.1、虚函数表

#includeusing namespace std;typedef void(*FUNC_PTR) ();class Person {public:virtual void BuyTicket() { cout << "买票-全价" << endl; }virtual void Func1() {cout << "Person::Func1()" << endl;}virtual void Func2() {cout << "Person::Func2()" << endl;}//protected:int _a = 0;};class Student : public Person {public:virtual void BuyTicket() { cout << "买票-半价" << endl; }private:virtual void Func3(){//_b++;cout << "Student::Func3()" << endl;}protected:int _b = 1;};// 打印函数指针数组// void PrintVFT(FUNC_PTR table[])void PrintVFT(FUNC_PTR* table){for (size_t i = 0; table[i] != nullptr; i++){printf("[%d]:%p->", i, table[i]);FUNC_PTR f = table[i];f();}printf("\n");}int main(){Person ps;Student st;int vft1 = *((int*)&ps);PrintVFT((FUNC_PTR*)vft1);int vft2 = *((int*)&st);PrintVFT((FUNC_PTR*)vft2);return 0;}

在这里插入图片描述

通过观察好像student里面没有func3,这时候我们需要通过内存窗口来查看它的地址

在这里插入图片描述

从图中可以看出多出了一个地址,我们有理由相信它是func3的地址,那么代码验证结果如下图所示:
在这里插入图片描述

那么最后一个全0是什么呢

虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

总结

总结一下派生类的虚表生成:

a.先将基类中的虚表内容拷贝一份到派生类虚表中

b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数

c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

2.2、虚表的存放位置

class Person {public:virtual void BuyTicket() { cout << "买票-全价" << endl; }virtual void Func1() {cout << "Person::Func1()" << endl;}virtual void Func2() {cout << "Person::Func2()" << endl;}//protected:int _a = 0;};class Student : public Person {public:virtual void BuyTicket() { cout << "买票-半价" << endl; }private:virtual void Func3(){//_b++;cout << "Student::Func3()" << endl;}protected:int _b = 1;};int main(){Person ps;Student st;int a = 0;printf("栈:%p\n", &a);static int b = 0;printf("静态区:%p\n", &b);int* p = new int;printf("堆:%p\n", p);const char* str = "hello world";printf("常量区:%p\n", str);printf("虚表1:%p\n", *((int*)&ps));printf("虚表2:%p\n", *((int*)&st));return 0;}

在这里插入图片描述

由图可知:虚表我们更愿意认为它存在代码区,因为它的地址与代码区是最接近的。

2.3、动态绑定与静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为也称为静态多态,比如:函数重载

  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态

2.4、多继承的虚函数表

#includeusing namespace std;typedef void(*FUNC_PTR) ();class Base1 {public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }private:int b1;};class Base2 {public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }private:int b2;};class Derive : public Base1, public Base2 {public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }private:int d1;};void PrintVFT(FUNC_PTR* table){for (size_t i = 0; table[i] != nullptr; i++){printf("[%d]:%p->", i, table[i]);FUNC_PTR f = table[i];f();}printf("\n");}int main(){Derive d;cout << sizeof(d) << endl;int vft1 = *((int*)&d);//int vft2 = *((int*)((char*)&d+sizeof(Base1)));Base2* ptr = &d;int vft2 = *((int*)ptr);PrintVFT((FUNC_PTR*)vft1);PrintVFT((FUNC_PTR*)vft2);return 0;}

上面的代码中:Derive对象的大小是多少了,如下图所示:

在这里插入图片描述

20是因为Derive继承了Base1和Base2的虚表指针,并且它们分别有一个变量,所以Base1是8,Base2是8,而Derive是4,加起来就为20

还有一个问题是:我们未重写了func3,那么func3是存在Base1的虚表离还是Base2的虚表里

在这里插入图片描述

多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

三、抽象类

3.1、概念

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

class Person {public:    virtual void Func()=0    {        //纯虚函数一般只声明,不实现。因为没有对象    }};

子类继承了父类的纯虚函数,子类也变成了抽象类,同样不能实例化出对象。 除非子类重写纯虚函数,子类才能实例化出对象。

在这里插入图片描述

注意:抽象类的作用是强制子类进行重写。

四、关于多态的问答题

什么是多态?

多态指多种形态。不同的对象完成同一件事情,但是结果不同。例如公交刷卡行为:成人刷卡全价,学生刷卡半价。亦或是不同的客户来消费,金卡会员8折,银卡会员9折,普通会员无折扣。

什么是重载、重写(覆盖)、重定义(隐藏)?

函数重载:1.两个函数必须再同一个作用域2.函数名相同、参数列表不同,返回值没有要求

重写:1.两个函数必须位于子类和父类2.函数名、参数列表、返回值必须相同(协变除外)、两个函数均为虚函数

隐藏:1.两个函数必须位于子类和父类2.函数名相同,不构成重写就是隐藏

多态的实现原理?

对于多态的实现原理,必须先从构成多态的条件说起:
1、必须通过父类对象的引用或指针当做形参调用虚函数。2、子类必须完成对父类虚函数的重写且被调用的函数是虚函数。

子类和父类的虚函数表指针、虚函数表、重写的虚函数的地址均不相同,我们传入一个父类对象,它使用的是源自父类的虚函数,传入一个从子类切片而来的父类对象,这个对象中的虚函数是子类重写的虚函数。虽说这两个都是父类对象,但是对象体内的虚函数并不是同一个,所以会产生不同的行为,这便是多态的原理。

inline函数可以是虚函数吗?

inline可以是虚函数。调用时,如果不构成多态,这个函数就保持inline属性。如果构成多态,就不具备inline属性,因为多态是要在运行时去对象的虚函数表里面找虚函数,所以在编译时,不能使用inline进行展开。

静态成员可以是虚函数吗?

不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

构造函数可以是虚函数吗?

不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。

析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

可以,并且最好把基类的析构函数定义成虚函数。用于处理子类对象交给父类的指针管理的情况

对象访问普通函数快还是虚函数更快?

首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

虚函数表是在什么阶段生成的,存在哪的?

虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。

什么是抽象类?抽象类的作用?

抽象类又称接口类。包含纯虚函数的类被称为抽象类,在虚函数后边加个 = 0,这个虚函数就被叫做纯虚函数抽象类不能实例化出对象。在现实世界中没有对应的实物,就可以定义为抽象类。例如职能类、Person类等。

抽象类体现接口继承的关系。子类继承抽象类后,也变成了抽象类。这就强制用户对纯虚函数进行重写,对虚函数的重写是一种接口继承,子类会继承虚函数的函数名及缺省值,但不会继承实现。

来源地址:https://blog.csdn.net/VHhhbb/article/details/131948690

免责声明:

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

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

【C++】多态

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

下载Word文档

猜你喜欢

【C++】多态

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

【C++】多态(下)

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

C#多态如何实现

小编给大家分享一下C#多态如何实现,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!C#实现多态主要有3种方法,虚方法,抽象类,接口1 虚方法在父类的方法前面加关键字
2023-06-14

C#多态性是什么

这篇文章主要介绍“C#多态性是什么”,在日常操作中,相信很多人在C#多态性是什么问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”C#多态性是什么”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!多态是面向对象编
2023-06-17

C++多态如何使用

本文小编为大家详细介绍“C++多态如何使用”,内容详细,步骤清晰,细节处理妥当,希望这篇“C++多态如何使用”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。多态多态离不开继承,首先来定义一个基类 Animal,里面
2023-07-02

c++多态如何实现

多态是面向对象编程中允许对象具有不同形式或行为的一种机制。c++ 中的多态通过虚函数、抽象类、纯虚函数和动态绑定实现。虚函数允许派生类重新定义基类方法,抽象类包含必须在派生类中重新定义的虚函数,纯虚函数没有任何实现,只存在于抽象类中,而动态
c++多态如何实现
2024-04-22

C/C++多态原理实例分析

本篇内容介绍了“C/C++多态原理实例分析”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!多态面向对象编程有三大特性:继承、封装和多态。其中,
2023-07-02

C++中怎么实现多态

C++中怎么实现多态,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。实现了C++多态 2 5 1 6#include < iostream> using namespace st
2023-06-17

C#多态性怎么理解

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

C#多态是什么意思

本篇内容介绍了“C#多态是什么意思”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!一、什么是C#多态?面向对象程序设计中的另外一个重要概念是多
2023-06-17

如何实现C#继承与C#多态

这篇文章主要讲解了“如何实现C#继承与C#多态”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“如何实现C#继承与C#多态”吧!在C#中实现OOP思想,丝毫不逊色于Java,下面我通知两句话来帮
2023-06-17

c#基础学习之多态

多态(Polymorphism)按字面的意思就是“多种状态”。在面向对象语言中,接口的多种不同的实现方式即为多态
2022-11-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动态编译

目录