C++封装、继承(单继承)、多态详细分析。

系列文章目录


文章目录

  • 系列文章目录
  • 摘要
  • 一、基本概念
  • 二、多态的分类
  • 三、多态的实现
    • 3.1 类型兼容与函数重写
    • 3.2 动态联编与静态联编
    • 3.3 虚函数
    • 3.4 动态多态的实现过程
  • 总结
  • 参考文献


摘要

多态性特征是 C++中最为重要的一个特征,熟练使用多态是学好 C++的关键,而理解多态的实现机制及实现过程则是熟练使用多态的关键。文章在分析多态性基本属性的基础上,结合具体程序实例重点分析了动态多态的实现机制,并结合虚函数编原理分析了动态多态的实现过程。
关键词: C++; 多态性; 虚函数


一、基本概念

封装、继承和多态是面向对象设计的 3 大特点。

  • 封装就是把客观事物抽象得到的数据和行为封装成一个整体,在
    C++中,实现数据和行为封装的程序单元就叫类。封装就是将代码模块化,实现了类内部对象的隐蔽。
  • 继承是由已经存在的类创建新类的机制,体现在类的层次关系中,子类拥有父类中的数据和方法,子类继承父类的同时可以修改和扩充自己的功能。
  • 多态是指父类的方法被子类重写、可以各自产生自己的功能行为。封装和继承的目的是代码的重用,多态就是实现接口重用,即“一个接口,多种方法”。

相比封装和继承,多态因其复杂性、灵活性更难以掌握和理解。

二、多态的分类

C++中利用类继承的层次关系来实现多态,通常是把具有通用功能的声明存放在类层次高的地方,而把实现这一个功能的不同方法放在层次较低的类中,C++语言通过子类重定义父类函数来实现多态。
多态通常分为两种: 通用多态特定多态


三、多态的实现

在 C++中利用类继承的层次关系来实现多态,通常是把具有通用功能的声明存放在类层次高的地方,而把实现这一个功能的不同方法放在层次较低的类中,C++语言通过子类重定义父类函数来实现多态

3.1 类型兼容与函数重写

  • C++中的继承遵循了类型兼容性原则
    即当子类以 Public方式继承父类时,将继承父类的所有属性和方法,因此,可以变相的理解成子类是一种特殊的父类,可以使用子类对象初始化父类,也可以使用父类的指针或引用来调用子类的对象。
  • C++中的函数重写
    在程序设计过程中,很多时候会出现这样一种情况,子类继承父类的 A 函数,但父类的 A 函数不能满足子类的需求,此时需要在子类中对 A 函数进行重写。C++中的函数重写是指: 函数名、参数、返回类型均相同。

如果程序中类型兼容性原则遇到了函数重写会怎么样,调用父类的 A 函数还是子类中重写的 A函数,类型兼容与函数重写之间的关系可以用以下程序代码阐释
代码示例:

#include<iostream>
using namespace std;class Animal // 父类
{
public:void Speak(){cout << "动物在说话" << endl;}
};
class Dog : public Animal // 子类
{
public:void Speak(){cout << "小狗在汪汪叫" << endl;}
};
int main()
{// 第一种定义Dog dog;dog.Speak();dog.Animal::Speak();// 第二种定义Animal animal1 = dog;animal1.Speak();// 第三种定义Animal* animal2 = &dog;animal2->Speak();return 0;
}

运行截图:
在这里插入图片描述
上述程序中定义了 Animal 和 Dog 两个类,其中,Dog 类以 Public 方式继承了 Animal 类,并且重写了
Speak( ) 方法。

  1. 根据程序运行结果不难看出: main( )函数中定义的 Dog 类对象 dog 的调用方法 dog.Speak( )
    是通过子类对象的 Speak( ) 函数来实现小狗在汪汪叫功能。
  2. dog.Animal: : Speak( ) 是子类对象通过使用操作符作用域调用父类的 Speak( ) 函数来实现:
    动物在说话。定义的 Animal 的对象 animal1 通过调用拷贝构造函数,把 dog 的 数 据 拷 贝 到 animal1
    中,animal1 仍为父类对象,所以animal1.Speak( )执行的结果是动物在说话。
  3. 最终定义了一个指向 Animal 类的指针 animal2,将派生类对象 dog 的地址赋给父类指针 animal2,利用该变量调用animal2 –>speak ( ) 方法。得到的结果是: 动物在说话。

原因
a) C++编译器进行了类型转换,允许父类和子类之间进行类型转换,即父类指针可以直接指向子类对象。根据赋值兼容,编译器认为父类指针指向的是父类对象,因此,编译结果只可能是调用父类中定义的同名函数。
b) 在此时,C++认为变量animal2中保存的就是 Animal 对象的地址,即编译器不知道指针 animal2指向的是一个什么对象,编译器认为最安全的方法就是调用父类对象的函数,因为父类和子类肯定都有相同的 Speak( )函数。因此,在 main() 函数中执行 animal2 –>Speak( ) 时,调用的是 Animal 对象的 Speak( ) 函数。

3.2 动态联编与静态联编

  • 以上程序出现这种情况的原因涉及 C++在具体编译过程中函数调用的问题,这种确定调用同名函数的哪个函数的过程就叫做联编( 又称绑定) 。在C++中联编就是指函数调用与执行代码之间关联的过程,即确定某个标识符对应的存储地址的过程,在C++程序中,程序的每一个函数在内存中会被分配一段存储空间,而被分配的存储空间的起始地址则为函数的入口地址。
  • 按照程序联编所进行的阶段,联编可分为两种:静态联编和动态联编。静态联编就是在程序的编译与连接阶段就已经确定函数调用和执行该调用的函数之间的关联。在生成可执行文件中,函数的调用所关联执行的代码是确定好的,因此,静态联编也称为早绑定动态联编是在程序的运行时根据具体情况才能确定函数调用所关联的执行代码,因此,动态联编也称为晚绑定
  • 当类型兼容原则与函数重写发生冲突时,程序员希望根据程序设计的子类对象类型来调用子类对象的函数,而不是编译器认为的调用父类的对象函数。也就是说,如果父类指针(引用) 指向( 引用) 父类的对象时,程序就应该调用父类的函数,如果父类指针( 引用) 指向( 引用)子类的对象时,程序就应该调用子类的函数。这一功能可以通过动态联编实现。与静态联编相比,动态联编是在程序运行阶段,根据成员函数基于对象的类型不同,编译的结果就不同,这就是动态多态。动态多态的基础是虚函数。虚函数是用来表现父类和子类成员函数的一种关系。

3.3 虚函数

虚函数的定义方法是用关键字 virtual 修饰类的成员函数,虚函数的定义格式:

 virtual〈返回值类型〉〈函数名〉( 〈形式参数表〉) { <函数体>} 

在类的层次结构中,成员函数一旦被声明为虚函数,那么,该类之后所有派生出来的新类中其都是虚函数。父类的虚函数在派生类中可以不重新定义,若在子类中没有重新改写父类的虚函数,则调用父类的虚函数。对兼容性与函数重写程序,进行适当的修改,将父 类 Animal 中 的 Speak ( ) 函数使用关键子Virtual 将其定义为虚函数,代码如下所示。

#include<iostream>
using namespace std;
class Animal // 父类
{
public:virtual void Speak() //用virtual 关键子定义 Speak() 为虚函数{cout << "动物在说话" << endl;}
};
class Dog : public Animal // 子类 Dog以public 方式继承了 Animal
{
public:void Speak() //重写了 Speak() 函数{cout << "小狗在汪汪叫" << endl;}
};
int main()
{Dog dog;dog.Speak();dog.Animal::Speak();Animal animal1 = dog;animal1.Speak();Animal* animal2 = &dog;animal2->Speak();return 0;
}

运行截图:
在这里插入图片描述

Animal * animal2 = &dog,animal2.Speak( ) 时,由于在父类 Animal 的 Speak( ) 函数前加关键字 Virtual,
使得 Speak( ) 函数变成虚函数,编译器在编译的时候,发现 animal 类中有虚函数,此时,编译器会为每个包含虚函数的类创建一个虚函数表,该表是一个一维数组,在这个数组中存放每个虚函数的地址,这样就实现了动态联编,也就是晚绑定。也就实现了前面说的当调用父类指针( 引用) 指向( 引用) 子类对象函数时,调用的是子类对象的函数,实现了动态多态。通过分析发现,要想实现动态多态
要满足以下 3个条件:

  1. 必须存在继承关系,程序中的 Dog 类以public 的方式继承了 Animal 类。
  2. 继承关系中必须要有同名的虚函数。在两个类中 Speak( ) 函数为同名虚函数,子类重写父类的虚函数。
  3. 存在父类的指针或引用调用子类该虚函数。

了解多态是如何实现的之前,先要了解虚函数的调用原理,虚函数的调用原理和普通函数不一样,编译器在程序编译的时候,发现类中有关键字 virtual 的虚函数时,编译器会自动为每个包含虚函数的类创建一个虚函数表用来存放类对象中虚函数的地址,并同时创建一个虚函数表指针指向该虚函数表[6]。每个类使用一个虚函数表,每个类对象用一个指向虚表地址的虚表指针。父类对象包含一个指针指向父类所有虚函数的地址,子类对象也包含一个指向独立地址的指针。
如果子类没有重新定义虚函数,该虚函数表将保存函数原始版本的地址,如果子类提供了虚函数的新定义,该虚函数表将保存新函数的地址。示例程序中定义了两个类 A 和 B,类 B 继承自类 A,父类 A
中定义了两个虚函数,子类 B 中重写了其中一个虚函数,代码如下所示:

class A
{
public:virtual void fun1(){cout << " fun1 是类 A 虚函数";}virtual void fun2(){cout << " fun2 是虚类 A 函数";}
};
class B : public A
{
public:virtual void fun1(){cout << " fun1 是类 B 的虚函数";}
};

分析上述程序,对于父类 A 中的两个虚函数 fun1( ) 和 fun2( ) ,由于子类 B 重写了类 A 中的 fun1( ) 函
数,就导致子类 B 的虚函数表的第一个指针指向的是类 B 的 fun1( ) 的函数而不是父类 A 的 fun1( ) 函数,
具体如下表所示:

类 A 的虚函数表类 B 的虚函数表
0: 指向类 A 的 fun1 的指针0: 指向类 B 的 fun1 的指针
1: 指向类 A 的 fun2 的指针1: 指向类 A 的 fun2 的指针

3.4 动态多态的实现过程

编译器进行编译程序时发现有 virtual 声明的函数,就会在这个类中产生一个虚函数表。即使子类中没有用 virtual 定义虚函数,由于父类中的定义,子类通过继承后仍为虚函数。程序中 Animal 类和 Dog 类都包含一个虚函数 Speak( ) ,因此,编译器会为这两个类都建立一个虚函数表,将虚函数地址存放到该表中。
在这里插入图片描述

编译器在为每个类创建虚函数表的同时,还为每个类的对象提供了一个虚函数表指针( vfptr) ,虚函数表指针指向了对象所属类的虚表。根据程序运行的对象类型去初始化虚函数表指针。虚函数表指针在没有初始化的情况下,程序是无法调用虚函数的。虚函数表的创建和虚函数表指针的始化是在构造函数中实现的,在构造子类对象时,先调用父类的构造函数,并初始化父类的虚函数指针,指向父类的虚函数表,当子类对象执行构造函数时,子类对象的虚函数表指针也被初始化,指向子类的虚函数表。实现了在调用虚函数时,就能够找到正确的函数,如下图所示。
在这里插入图片描述

总结

多态性作为面向对象程序设计语言的 3 大要素之一,因其灵活性、伸缩性和复杂性而难以掌握。本文着重分析多态的分类、特征及动态多态的实现机制和原理,但本文对于动态多态的分析仅仅局限于单继承的情况,对于多继承的情况原理基本相同,本文未作过多说明。

参考文献

[1]李家宏,孙庆英.C++多态性的实现过程[J].无线互联科技,2023,19(02):131-134.

网址链接

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

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

相关文章

原创 | 数据的确权、流通、入表与监管研究(一):数据与确权

作者&#xff1a;张建军&#xff0c;中国电科首席专家&#xff0c;神州网信技术总监 本文约7100字&#xff0c;建议阅读10分钟 本文主要介绍数据与数据分类、数据确权规则、数据的所有权与其他权利等方面内容&#xff0c;并进行案例分析。 2022年12月发布的《关于构建数据基础制…

Linux 和 macOS 的主要区别在哪几个方面呢?

(꒪ꇴ꒪ )&#xff0c;Hello我是祐言QAQ我的博客主页&#xff1a;C/C语言&#xff0c;数据结构&#xff0c;Linux基础&#xff0c;ARM开发板&#xff0c;网络编程等领域UP&#x1f30d;快上&#x1f698;&#xff0c;一起学习&#xff0c;让我们成为一个强大的攻城狮&#xff0…

uniapp实战 —— 弹出层 uni-popup (含vue3子组件调父组件的方法)

效果预览 弹出的内容 src\pages\goods\components\ServicePanel.vue <script setup lang"ts"> // 子组件调父组件的方法 const emit defineEmits<{(event: close): void }>() </script><template><view class"service-panel"…

C# .NET平台提取PDF表格数据,并转换为txt、CSV和Excel表格文件

处理PDF文件中的内容是比较麻烦的事情&#xff0c;特别是以表格形式呈现的各种数据。为了充分利用这些宝贵的数据资源&#xff0c;我们可以通过程序提取PDF文件中的表格&#xff0c;并将其保存为更易于处理和分析的格式&#xff0c;如txt、csv、xlsx&#xff0c;从而更方便地对…

leetcode面试经典150题——35 螺旋矩阵

题目&#xff1a; 螺旋矩阵 描述&#xff1a; 给你一个 m 行 n 列的矩阵 matrix &#xff0c;请按照 顺时针螺旋顺序 &#xff0c;返回矩阵中的所有元素。 示例&#xff1a; 输入&#xff1a;matrix [[1,2,3],[4,5,6],[7,8,9]] 输出&#xff1a;[1,2,3,6,9,8,7,4,5] 提示&…

12月8日作业

使用手动连接&#xff0c;将登录框中的取消按钮使用qt4版本的连接到自定义的槽函数中&#xff0c;在自定义的槽函数中调用关闭函数&#xff1b;将登录按钮使用qt5版本的连接到自定义的槽函数中&#xff0c;在槽函数中判断u界面上输入的账号是否为"admin"&#xff0c;…

kafka学习笔记--安装部署、简单操作

本文内容来自尚硅谷B站公开教学视频&#xff0c;仅做个人总结、学习、复习使用&#xff0c;任何对此文章的引用&#xff0c;应当说明源出处为尚硅谷&#xff0c;不得用于商业用途。 如有侵权、联系速删 视频教程链接&#xff1a;【尚硅谷】Kafka3.x教程&#xff08;从入门到调优…

Day54力扣打卡

打卡记录 出租车的最大盈利&#xff08;动态规划&#xff09; 链接 class Solution:def maxTaxiEarnings(self, n: int, rides: List[List[int]]) -> int:d defaultdict(list)for start, end, w in rides:d[end].append((start, end - start w))f [0] * (n 1)for i in…

文章解读与仿真程序复现思路——电力自动化设备EI\CSCD\北大核心《考虑源网荷效益的峰谷电价与峰谷时段双层优化模型》

这个标题涉及到电力定价和能源效益的优化模型。让我来分解一下&#xff1a; 峰谷电价&#xff1a;这是一种电力定价策略&#xff0c;即在一天内不同时间段设定不同的电价。通常&#xff0c;高峰时段&#xff08;需求高&#xff09;的电价相对较高&#xff0c;而低谷时段&#x…

人工智能学习9(LightGBM)

编译工具&#xff1a;PyCharm 文章目录 编译工具&#xff1a;PyCharm lightGBM原理lightGBM的基础使用案例1&#xff1a;鸢尾花案例2&#xff1a;绝对求生玩家排名预测一、数据处理部分1.数据获取及分析2.缺失数据处理3.数据规范化4.规范化输出部分数据5.异常数据处理5.1删除开…

Child Mind Institute - Detect Sleep States(2023年第一次Kaggle拿到了银牌总结)

感谢 感谢艾兄&#xff08;大佬带队&#xff09;、rich师弟&#xff08;师弟通过这次比赛机械转码成功、耐心学习&#xff09;、张同学&#xff08;也很有耐心的在学习&#xff09;&#xff0c;感谢开源方案&#xff08;开源就是银牌&#xff09;&#xff0c;在此基础上一个月…

基于Lucene的全文检索系统的实现与应用

文章目录 一、概念二、引入案例1、数据库搜索2、数据分类3、非结构化数据查询方法1&#xff09; 顺序扫描法(Serial Scanning)2&#xff09;全文检索(Full-text Search) 4、如何实现全文检索 三、Lucene实现全文检索的流程1、索引和搜索流程图2、创建索引1&#xff09;获取原始…

模板与泛型编程

函数模板 显示实例化 区别定义与声明 T是模板形参 int是模板实参 inpunt是函数形参 3是函数实参 显示实例化 模板必须实例化可见 翻译单元一处定义原则 与内联函数异同 引入原因&#xff1a;函数模板是为了编译器两个阶段的处理 内联函数是为了能在编译期展开 模板实参的类…

Ignis - Interactive Fire System

Ignis - 点火、蔓延、熄灭、定制! 全方位火焰系统。 这个插件在21年的项目中使用过很好用值使用概述 想玩火吗?如果想的话,那么Ignis就是你的最佳工具。有了Ignis,你可以把任何物体、植被或带皮带骨的网状物转换为可燃物体,它就会自动着火。然后,火焰可以蔓延,点燃其他物…

【docker 】centOS 安装docker

官网 docker官网 github源码 卸载旧版本 sudo yum remove docker \docker-client \docker-client-latest \docker-common \docker-latest \docker-latest-logrotate \docker-logrotate \docker-engine 安装软件包 yum install -y yum-utils \device-mapper-persistent-data…

【优选算法系列】【专题二滑动窗口】第四节.30. 串联所有单词的子串和76. 最小覆盖子串

文章目录 前言一、串联所有单词的子串 1.1 题目描述 1.2 题目解析 1.2.1 算法原理 1.2.2 代码编写 1.2.3 题目总结二、最小覆盖子串 2.1 题目描述 2.2 题目解析 2.2.1 算法原理 2.2.2 代码编写 …

浅谈5G基站节能及数字化管理解决方案的设计与应用-安科瑞 蒋静

截至2023年10月&#xff0c;我国5G基站总数达321.5万个&#xff0c;占全国通信基站总数的28.1%。然而&#xff0c;随着5G基站数量的快速增长&#xff0c;基站的能耗问题也逐渐日益凸显&#xff0c;基站的用电给运营商带来了巨大的电费开支压力&#xff0c;降低5G基站的能耗成为…

actitivi自定义属性(二)

声明&#xff1a;此处activiti版本为6.0 此文章介绍后端自定义属性解析&#xff0c;前端添加自定义属性方法连接&#xff1a;activiti自定义属性&#xff08;一&#xff09;_ruoyi activiti自定义标题-CSDN博客 1、涉及到的类如下&#xff1a; 简介&#xff1a;DefaultXmlPar…

在 JavaScript 中导入和导出 Excel XLSX 文件:SpreadJS

在 JavaScript 中导入和导出 Excel XLSX 文件 2023 年 12 月 5 日 使用 MESCIUS 的 SpreadJS 将完整的 JavaScript 电子表格添加到您的企业应用程序中。 SpreadJS 是一个完整的企业 JavaScript 电子表格解决方案&#xff0c;用于创建财务报告和仪表板、预算和预测模型、科学、工…

图的搜索(一):广度优先搜索算法和深度优先搜索算法

图的搜索&#xff08;一&#xff09;&#xff1a;广度优先搜索算法和深度优先搜索算法 本章主要记录了图的搜索算法&#xff0c;和可以解决图的基本问题——最短路径问题的算法。本章主要对图搜索的相关算法进行了介绍&#xff1a;广度优先搜索算法、深度优先搜索算法。 下一…