C++:多态的内容和底层原理

news2024/11/19 16:33:13

文章目录

  • 多态的概念
  • 多态的定义
    • 虚函数
    • ```override```和```final```关键字
    • 重载、覆盖、隐藏
  • 抽象类
    • 抽象类的定义
    • 接口继承和实现继承
  • 多态的原理解析
    • 虚函数表

本篇总结C++中多态的基本内容和原理实现和一些边角内容

多态的概念

首先要清楚多态是什么,是用来做什么的?

多态从字面意思来讲,就是多种形态,完成一个事情,不同的人去完成会有不同的结果和状态,这样的情况就叫做多态

多态的定义

多态是不同继承关系的类对象,在调用一个函数的时候会产生不同的行为,比如同样是买票这个操作,普通人就是全票,学生就是半票,本篇的例子也会从这个例子出发,进行多态中具体的距离和深层次的理解

构成多态的条件:

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,并且派生类要对基类的虚函数进行重写
#include <iostream>
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 p;
	Student s;
	func(p);
	func(s);
}

上面是对多态的最初始定义,也是很基础的定义,从中可以看出多态的基本用法和实现的功能

虚函数

虚函数的定义:

虚函数通俗来说,就是被virtual修饰的类成员函数就是虚函数

虚函数的重写:

虚函数的重写就是,当派生类中有一个和基类完全相同的虚函数,那么就称之为子类的虚函数重写了基类的虚函数,虽然子类可以不加virtual,但是并不标准,最好加上

虚函数的例外:

  1. 协变
    协变就是,派生类重写基类虚函数的时候,与基类虚函数返回值类型不同,比如基类的虚函数返回的是基类成员的指针和引用,派生类返回的是指针和引用的时候,也算是虚函数重写,这种情况就叫做协变

  2. 析构函数重写
    如果基类的析构函数是虚函数,那么派生类的析构函数默认会和基类的析构函数构成重写,虽然名字和函数名不同,但是依旧是,这是因为编译器进行编译后,把析构函数的名称统一处理为destructor,这样也算是重写

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "普通票全价" << endl;
	}
	virtual Person* f()
	{
		return new Person;
	}
	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};

class Student :public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "学生票半价" << endl;
	}
	virtual Student* f()
	{
		return new Student;
	}
	virtual ~Student()
	{
		cout << "~Student()" << endl;
	}
};

void func(Person& p)
{
	p.BuyTicket();
}

overridefinal关键字

C++11中,引入了两个关键字,这两个关键字就是用来辅助进行虚函数多态的多种复杂情形,避免出现疏忽而导致错误的情况出现:

final:修饰虚函数,表示这个虚函数不能被重写了

class Student :public Person
{
public:
	virtual void BuyTicket() final
	{
		cout << "学生票半价" << endl;
	}
	virtual Student* f()
	{
		return new Student;
	}
	virtual ~Student()
	{
		cout << "~Student()" << endl;
	}
};

class Child :public Student
{
public:
	virtual void BuyTicket()
	{
		cout << "小孩免票" << endl;
	}
};

在这里插入图片描述
override:检查派生类虚函数是否重写了基类某个虚函数,如果没有就报错

class Person
{
public:
	/*virtual*/ void BuyTicket()
	{
		cout << "普通票全价" << endl;
	}
	virtual Person* f()
	{
		return new Person;
	}
	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};

class Student :public Person
{
public:
	virtual void BuyTicket() override
	{
		cout << "学生票半价" << endl;
	}
	virtual Student* f()
	{
		return new Student;
	}
	virtual ~Student()
	{
		cout << "~Student()" << endl;
	}
};

class Child :public Student
{
public:
	virtual void BuyTicket() override
	{
		cout << "小孩免票" << endl;
	}
};

void func(Person& p)
{
	p.BuyTicket();
}

在这里插入图片描述
多态在使用的过程中是十分复杂的,因此使用时需要注意逻辑能否清楚的表示,可能只是稍微变了一点点内容,就使得整个意思全然变换,下面对比一下继承多态中的一些概念:

重载、覆盖、隐藏

  1. 重载指的是,函数名在一个作用域,并且函数名相同,参数不同的情况,那么这两个函数就构成了函数重载,编译器在进行处理的时候会根据参数形成不同的函数表,由此来对应不同的情况
  2. 重写指的是,两个函数在基类和派生类的作用域下,前提是函数名、参数、返回值都一样的情况下,如果是虚函数,那么就构成了重写,其中子类可以不写virtual,可以理解为虚函数的属性被从基类中继承了下来,但是并不推荐这样写,其中要注意特殊情况,比如协变和析构函数的情况
  3. 隐藏指的是,两个函数在基类和派生类的作用域下,当函数名相同的时候,如果不符合重写的定义那么就是重定义了,比如在继承中见到的很多种情况

抽象类

抽象类的定义

在虚函数后面写上等于0,就说明这个函数是纯虚函数,有纯虚函数的类就叫做抽象类,抽象类的特点是不可以实例化出一个具体的对象,而派生类被继承后也不能实例化对象,只有在重写了虚函数的前提下,才能实例化对象

纯虚函数体现了派生类要重写的这个规则,同时也体现出了接口继承的概念

接口继承和实现继承

  1. 接口继承(Interface Inheritance)是指从一个纯虚基类(pure virtual base class)继承而来,目的是为了实现一个类的接口,使得派生类必须实现该接口中定义的所有纯虚函数。接口继承的主要目的是实现类的接口复用,它并不关心实现细节。在接口继承中,派生类只需要实现基类中定义的纯虚函数,不需要关心基类中其他的数据和函数
class Shape 
{
public:
    virtual void draw() = 0; // 纯虚函数
};

class Circle : public Shape 
{
public:
    void draw() override 
    {
        // 实现圆形的绘制
    }
};

class Square : public Shape 
{
public:
    void draw() override 
    {
        // 实现正方形的绘制
    }
};
  1. 实现继承(Implementation Inheritance)是指从一个普通的基类(非纯虚基类)继承而来,目的是为了实现基类中已有的函数或数据。实现继承的主要目的是实现代码复用,它关心基类中的实现细节。在实现继承中,派生类会继承基类中所有的成员函数和数据成员,并且可以重写这些函数以改变它们的行为
class Person 
{
public:
    void sayHello() 
    {
        std::cout << "Hello, I am a person." << std::endl;
    }
};

class Student : public Person 
{
public:
    void sayHello() override 
    {
        std::cout << "Hello, I am a student." << std::endl;
    }
};

int main() 
{
    Student s;
    s.sayHello(); // 输出: "Hello, I am a student."
    return 0;
}

接口继承是指派生类只继承了基类的接口(也就是纯虚函数),而没有继承基类的实现。这种方式使得派生类必须实现基类中的所有纯虚函数,从而使得派生类和基类的实现是分离的,实现了接口和实现的分离。这种继承方式常常用于实现抽象类和接口,强制要求派生类实现接口中的所有函数

实现继承是指派生类继承了基类的接口和实现,包括数据成员和函数实现。这种方式使得派生类可以复用基类的代码,从而减少了代码的重复编写,同时也保证了派生类和基类的一致性。但是,这也意味着派生类和基类的实现是紧密耦合的,基类的修改可能会影响到派生类的行为

多态的原理解析

虚函数表

对于一个使用了多态的类,创建一个对象看其内部的内容:

在这里插入图片描述
会发现这当中和预想的结果并不一样,原因就在于这当中多了一个数组,这个指针数组实际上是叫做虚函数表指针数组,严格意义来说,这个数组中存放的是虚函数的函数地址,虚函数表也叫做虚表,那么问题来了:为什么要这么设计呢?

下面来做实验,对前面的类进行改造:

Person类中加一个虚函数和一个普通函数,而在Student类中只重写一个虚函数:

class Person
{
public:
	// Person类中有两个虚函数和一个普通函数
	virtual void BuyTicket()
	{
		cout << "普通票全价" << endl;
	}
	virtual void func1()
	{
		cout << "void func1()" << endl;
	}
	void func2()
	{
		cout << "void func2()" << endl;
	}
private:
	int _person; // 定义一个变量
};

class Student :public Person
{
	// 继承类中只重写一个虚函数,剩下的不进行重写
public:
	virtual void BuyTicket()
	{
		cout << "学生票半价" << endl;
	}
private:
	int _student; // 定义一个变量
};

void func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person p;
	Student s;
	func(p);
	func(s);
}

实验结果如下:

在这里插入图片描述
对实验结果进行分析得出下面的结论:

  1. 派生类的Student对象中也有一个虚表指针,其中是由两个部分组成的,一个是父类成员和自己的成员,虚表指针中也是存在的一部分是自己的成员
  2. 在基类和派生类中的虚表地址是不一样的,但是在虚表的具体内部中会发现,有一个函数指针地址是一样的,还有一个不一样,那么说明在Student类中重写的函数发生了改变,因此虚函数的重写才叫做覆盖,覆盖指的就是虚表中对于虚函数的覆盖
  3. 对于虚表内的内容,只有被继承下来的虚函数才会放到虚表中,其余函数不会放入虚表中
  4. 虚函数表本质上就是一个存放虚函数指针的指针数组,这与一开始的结论是一样的

虚函数表的生成过程:

  1. 基类中的虚表拷贝到派生类的虚表中
  2. 如果派生类中重写了虚表的某个函数,那么就进行覆盖的过程
  3. 派生类自己新有的虚函数按照在类内的次序放到派生类虚表的最后

虚函数和虚表的存储位置:

虚函数存放在虚表,虚表存放在对象中,这样的回答是错误的!

虚表中存的是虚函数的指针,并不是虚函数,虚函数和普通的函数是一样的,都是存放在代码段中,只是将它的指针放到了虚表中,而在对象中存放的是虚表的指针,也不是虚表,虚表在vs下也是存放在代码段的位置中

多态的调用原理:

在知道了虚表的存在和原理后,其实可以理解前面的一些内容了

当指向的对象是Person类的时候,此时会在Person类的虚表中找到对应的函数并进行调用,当对象是Student类的时候,原理相同,借助这个原理就实现了多态,用不同的对象去运行会产生不同的结果,而多态的函数调用也不是直接确认的,而是在运行的过程中,在对象的内部自动取识别,去获取的

动态绑定和静态绑定

静态绑定也叫做前期绑定或者是早绑定:在程序编译期间就确定了程序的行为,也叫做静态多态,比如说函数重载就是比较典型的例子

动态绑定也叫做后期绑定或者是晚绑定:在程序运行期间,根据具体拿到的类型来确定程序的具体行为和调用的具体函数,比如说动态多态就是这样的例子

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1124507.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Windows桌面便笺 - 置顶任务TODO - 便利贴工具

效果图 一直置顶 免费 步骤 1. 打开便笺 win10找不到自带的便签怎么办-百度经验win10找不到自带的便签怎么办,很多朋友都在问wi10找不到自带的便签怎么办&#xff0c;今天就来给大家介绍一下wi10找不到自带的便签怎么办的方法。https://jingyan.baidu.com/article/b2c186c8…

小红书品牌账号怎么运营,如何传播规划?

其实新品牌面对的肯定都是新客户&#xff0c;对于新客户来说&#xff0c;真诚永远是最大的必杀技&#xff0c;所以在这告诉各位新兴品牌&#xff0c;少点套路&#xff0c;那么小红书品牌账号怎么运营&#xff0c;如何传播规划呢&#xff1f; 一、对品牌账号进行定位 定位方面一…

第一章初识Maven与Maven安装配置——尚硅谷

文章目录 Maven是什么Maven 作为构建管理工具依赖管理使用Maven的好处JAR包的规模JAR包的来源JAR包之间的依赖关系 Maven 开发环境配置Maven的下载Maven的解压配置setting.xml配置文件指定本地仓库配置镜像仓库Maven仓库的概念 配置基础 JDK 版本配置环境变量配置JDK环境配置Ma…

现在大火的低代码是怎么回事?进来聊聊低代码

一、前言 开发过程中&#xff0c;只是觉得前端后端合起来&#xff0c;有很多冗余信息&#xff0c;被代码一遍遍重复表达&#xff0c;是一件很枯燥、无聊的事情。 这些枯燥的重复工作&#xff0c;完全可以由机器来做&#xff0c;以便解放出我们的时间&#xff0c;来做更有价值的…

谷歌云的利润增长才刚刚开始

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 总结&#xff1a; &#xff08;1&#xff09;自从Google Cloud(谷歌云&#xff09;今年开始盈利以来&#xff0c;投资者都在怀疑这种盈利能力能否持续下去。 &#xff08;2&#xff09;虽然微软Azure目前在全球的人工智能竞…

Python 学习(day04)

函数进阶 获得多个返回值

“微信小程序登录与用户信息获取详解“

目录 引言微信小程序微信登录介绍1. 微信登录的基本概念2. 微信小程序中的微信登录 微信小程序登录的wxLogin与getUserProfile的区别1. wx.login()2. wx.getUserProfile()3.两者区别 微信小程序登录的理论概念1. 微信登录流程2. 用户授权与登录态维护 微信小程序登录的代码演示…

【计算机网络】HTTP 协议的基本格式以及 fiddler 的用法

HTTP协议的基本格式如下&#xff1a; 1.请求行&#xff1a; 包括请求THHP协议的版本、请求URI&#xff08;资源路径&#xff09;和HTTP方法&#xff08;如GET、POST、PUT、DELETE等&#xff09; GET/example.html HTTP/1.1 GET表示请求方法&#xff0c;/example.html表示请求的…

视频如何批量添加水印?简单几步帮你解决问题

在如今这个短视频横行的时代&#xff0c;我们常常需要在短视频中添加个人logo或水印来保护知识产权和增加品牌曝光度。如果你有很多视频需要添加水印&#xff0c;那么手动操作将是非常耗时和繁琐的。幸运的是&#xff0c;我们可以使用一些软件来批量添加水印。在此&#xff0c;…

Kubernetes中如何使用 CNI?

一、CNI 是什么 它的全称是 Container Network Interface&#xff0c;即容器网络的 API 接口。 它是 K8S 中标准的一个调用网络实现的接口。Kubelet 通过这个标准的 API 来调用不同的网络插件以实现不同的网络配置方式。实现了这个接口的就是 CNI 插件&#xff0c;它实现了一…

MySQL查看数据库、表容量大小

1. 查看所有数据库容量大小 selecttable_schema as 数据库,sum(table_rows) as 记录数,sum(truncate(data_length/1024/1024, 2)) as 数据容量(MB),sum(truncate(index_length/1024/1024, 2)) as 索引容量(MB)from information_schema.tablesgroup by table_schemaorder by su…

手写一个PrattParser基本运算解析器4: 简述iOS的编译过程

点击查看 基于Swift的PrattParser项目 iOS项目的编译过程与PrattParser解析器 前面三篇我们看到了PrattParser解析器的工作原理, 工作过程, 我们了解到PrattParser解析器实际上是模拟了编译过程中的 词法分析 、语法分析 、语义分析 、 中间代码生成 这几个编译前端过程. 那么P…

redis 从小白到大师系列

字符串 Redis 字符串数据类型 set 字符串 /*** 设置字符串*/ $t $redis->set(o1,o1); //返回true or false var_dump($t);get字符串 /*** 获取字符串*/ $t $redis->get(o1); //返回true or false var_dump($t);结果&#xff1a; string(2) “o1” 返回 key 中字符串…

现在游戏出海有多少优势?

国内游戏市场趋于饱和&#xff0c;但是国外市场潜力仍然可观&#xff0c;因此很多人选择游戏出海&#xff0c;那么现在游戏出海有多少优势呢&#xff1f; 1、市场潜力 全球游戏市场潜力巨大&#xff0c;增长迅速。中国游戏公司具有强大的研发能力和创新能力&#xff0c;能够开…

【Java集合类面试一】、 Java中有哪些容器(集合类)?

文章底部有个人公众号&#xff1a;热爱技术的小郑。主要分享开发知识、学习资料、毕业设计指导等。有兴趣的可以关注一下。为何分享&#xff1f; 踩过的坑没必要让别人在再踩&#xff0c;自己复盘也能加深记忆。利己利人、所谓双赢。 面试官&#xff1a;Java中有哪些容器&#…

xcode15一直显示正在连接iOS17真机问题解决

前言 更新xcode15之后&#xff0c;出现了各种报错问题&#xff0c;可谓是一路打怪啊&#xff0c;解决一个报错问题又来一个。没想到到了最后还能出现一个一直显示正在连接iOS17真机的问题 一直显示正在连接iOS17真机的问题 问题截图如下&#xff1a; 解决方法 1. 打开De…

“DDD创新”文章赏析-UMLChina建模知识竞赛第4赛季第16轮

DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 参考潘加宇在《软件方法》和UMLChina公众号文章中发表的内容作答。在本文下留言回答。 只要最先答对前3题&#xff0c;即可获得本轮优胜。第4题为附加题&#xff0c;对错不影响优胜者…

MYSQL(索引+SQL优化)

索引: 索引是帮助MYSQL高效获取数据的排好序的数据结构 1)假设现在进行查询数据&#xff0c;select * from user where userID89 2)没有索引是一行一行从MYSQL进行查询的&#xff0c;还有就是数据的记录都是存储在MYSQL磁盘上面的&#xff0c;比如说插入数据的时候是向磁盘上面…

高效的 C++ JSON 解析、生成器 RapidJSON

简介 RapidJSON是一个高效的C JSON解析器和生成器。它专注于性能和易用性&#xff0c;使得处理JSON数据变得简单和快速。RapidJSON支持现代的JSON特性&#xff0c;如嵌套对象、数组、Unicode编码和注释。它的API简洁易用&#xff0c;可以轻松解析和生成JSON数据。无论你的项目需…

“菜鸟”程序员逆袭:独立开发iOS音乐应用,年底参加Amazon DeepRacer 全球锦标赛

“致一年前的小木土&#xff1a;任务完成。” 6月30日&#xff0c;在获得2023 Amazon DeepRacer自动驾驶赛车企业总决赛中国区冠军三天后的深夜&#xff0c;杜键文发了这条朋友圈&#xff0c;并配上比赛现场的9张图。 “小木土”是杜键文的网名&#xff0c;取其姓氏&#xff…