【C++】深入解析 C++ 多态机制:虚函数、动态绑定与抽象类的精髓

个人主页: 起名字真南的CSDN博客

个人专栏:

  • 【数据结构初阶】 📘 基础数据结构
  • 【C语言】 💻 C语言编程技巧
  • 【C++】 🚀 进阶C++
  • 【OJ题解】 📝 题解精讲

目录

  • 📌 前言
  • 📌1 多态
    • ✨ 1.1 `多态`的概念
  • 📌 2 `多态`的定义及实现
    • ✨ 2.1 实现多态所需要的条件
      • 🚀 2.1.1 实现多态的两个`重要条件`
      • 🚀 2.1.2 `虚函数`
      • 🚀 2.1.3 虚函数的`重写/覆盖`
      • 🚀 2.1.4 多态场景的选择题
      • 🚀 2.1.5 override和final关键字
      • 🚀 2.1.6 重载/重写/隐藏的对比
  • 📌 3 纯虚函数和抽象类
    • ✨ 3.1 纯虚函数(Pure Virtual Function)
    • ✨3.2 抽象类(Abstract Class)
    • ✨总结
  • 📌 4 多态的原理
    • ✨ 4.1 函数表指针
    • ✨ 4.2 多态的原理
      • 🚀 4.2.1 多态是如何实现的
      • 🚀 4.2.2 虚函数表
      • 虚函数表(Virtual Table)
      • 虚函数表的工作原理
      • 结构示例

📌 前言

在C++编程中,多态是面向对象设计(OOP)的核心特性之一,也是提高代码灵活性和可扩展性。通过虚函数和动态绑定,多态可以是代码在运行时根据对象的不同调用实现各自的作用,适应更复杂的业务需求。
然而多态不仅限于简单的继承和重写,它涉及虚函数表,动态绑定,菱形继承,虚继承等。

📌1 多态

✨ 1.1 多态的概念

多态的概念: 通俗来讲就是多种状态,多态编译分为运行时多态(动态绑定)和编译时多态(静态绑定)。是根据将实参传给形参的参数匹配分别是在编译时确定的和运行时确定的。

📌 2 多态的定义及实现

✨ 2.1 实现多态所需要的条件

多态是继承关系下的类对象,在调用同一函数的时候所产生的不同的行为

🚀 2.1.1 实现多态的两个重要条件

  1. 必须指针或者引用调用函数

    要实现多态效果,第一必须是基类的指针或者引用,因为只有基类的指针和引用才能即指向派生类对象,又指向基类对象。

  2. 被调用的函数必须是虚函数

    第二派生类必须对基类的虚函数进行重写/覆盖,只有经过重写和覆盖,派生类才能有不同的函数。

🚀 2.1.2 虚函数

类的成员函数前面加上virtual修饰才能称为是虚函数,不是成员函数不能加virtual修饰

class Person
{
public:virtual void BuyTicket(){cout<<"买票-全价" << endl;}
};

🚀 2.1.3 虚函数的重写/覆盖

虚函数的重写/覆盖:派生类中有一个跟基类完全相同的虚函数,我们这里说的完全相同是指虚函数的函数名,参数类型,返回值类型三个完全相同。

class Person
{
public://多态实现的本质,调用虚函数//1、基类的指针或者引用调用虚函数//2、被调用的函数一定是虚函数virtual void BuyTicket(){cout << "买票-全价" << endl;}
};
class Student :public Person
{
public://返回类型,函数名,参数列表完全相同构成虚函数的重写//子类的virtual可以去掉virtual void BuyTicket(){cout << "买票-半价" << endl;}
};
//和Person这个类型没有关系
//传派生类会将基类的那一部分切片
void Func(Person* ptr)
{//虽然都是父类的指针在调用该函数,但是具体的实现是由ptr指向的对象实现的。ptr->BuyTicket();
}
void Func(Person& ptr)
{//虽然都是父类的指针在调用该函数,但是具体的实现是由ptr指向的对象实现的。ptr.BuyTicket();
}
int main()
{//满足多态指向谁调用谁,不满足多态就只和ptr的类型有关系.Person ps;Student st;Func(&ps);//子类隐式转换切割父类的那一部分。Func(&st);//传引用Func(ps);Func(st);return 0;
}

🚀 2.1.4 多态场景的选择题

以下程序输出的结果是什么()

A:  A->0  B: B->1  C:  A->1  D: B->0  E: 编译报错
class A
{
public:virtual void func(int val = 1){cout << "A->" << val << endl;}virtual void test(){func();}
};class B : public A
{
public://重写只是重写的函数的实现,使用父类函数的声明部分加上派生类的实现部分//本质上时重写虚函数的实现,所以可以不加virtual;//实际上的重写/*virtual void func(int val = 1){cout << "B->" << val << endl;}*///绝不重新定义继承来的缺省值//virtual void func(int val = 0)virtual void func(int val = 1){cout << "B->" << val << endl;}
};int main()
{B* b = new B;//实际上时 A 中*this 来调用func所以构成多态b->test();//不构成多态//b->func();return 0;
}

正确答案是 B : B->1
分析:
**接下来我们很定会有这样的疑问:**首先我们看到 b 对象是一个指向 B 类型的一个指针,但是 B 是派生类,为什么还会构成多态呢? 构成多态的条件不是需要父类的指针吗?

**接下来我们带着疑问来解决上面的问题,**在 main 函数中 b 对象调用了 test 函数,并且通过 test 函数来调用 func 函数。我们可以发现 test 中并没有传入参数但是我们忽略了作为一个成员函数总是有一个 this 指针,并且是父类的 this 指针,所以我们可以认为是 A 类型的 this 指针来调用的 func 函数这样解决了第一个问题

接下来我们来看 为什么继承的函数还是使用基类的缺省值。其实在继承虚函数时,我们不是将整个虚函数复制过来,而是通过虚函数表来调用它(虚函数表我们后面会详细讲解)。在派生类中不需要在继承的函数上重新加 virtual,这是因为继承时我们继承的是函数的定义,而重写的部分仅是函数的实现(即函数体部分)因此,最终输出的是 B 类型的函数体和 A 类的缺省值

🚀 2.1.5 override和final关键字

C++11提供了override,可以帮助⽤⼾检测是否重写。如果我们不想让派⽣类重写这个虚函数,那么可以⽤final去修饰。

class Car
{
public://使用final不能被重写virtual void Drive() final{}
};class Benz : public Car
{
public://检查是否完成了重写,在编译时进行检查virtual void Drive() override{cout << "Benz" << endl;}
};

在这个代码中Drive函数被final修饰所以不能被重写/覆盖,同时在派生类中邪了override关键字用来检查函数时候被重写/覆盖,这样就会报错。

🚀 2.1.6 重载/重写/隐藏的对比

在这里插入图片描述

📌 3 纯虚函数和抽象类

在C++中,纯虚函数抽象类是面向对象编程中的两个重要概念,主要用于定义接口和实现多态性。以下是对这两个概念的详细介绍:

✨ 3.1 纯虚函数(Pure Virtual Function)

  • 定义:纯虚函数是没有实现的虚函数,只在类中声明但不提供具体实现。其声明格式为:
    virtual void functionName() = 0;
    
    其中= 0表示该函数是纯虚函数。
  • 目的:纯虚函数通常用于定义一个接口或规范,要求派生类必须提供该函数的具体实现。
  • 特点
    • 纯虚函数没有函数体。
    • 任何包含纯虚函数的类都无法直接实例化。
    • 派生类继承该类时必须实现纯虚函数,否则派生类也会成为抽象类。

示例

class Shape {
public:virtual void draw() = 0; // 纯虚函数
};class Circle : public Shape {
public:void draw() override {std::cout << "Drawing Circle" << std::endl;}
};class Square : public Shape {
public:void draw() override {std::cout << "Drawing Square" << std::endl;}
};int main() {Shape* s1 = new Circle();Shape* s2 = new Square();s1->draw(); // 输出: Drawing Circles2->draw(); // 输出: Drawing Squaredelete s1;delete s2;return 0;
}

在上面的例子中,Shape类是一个基类,它定义了一个纯虚函数drawCircleSquare是派生类,它们实现了draw函数,因此可以被实例化。

✨3.2 抽象类(Abstract Class)

  • 定义:抽象类是包含一个或多个纯虚函数的类。由于包含纯虚函数,抽象类无法直接实例化。
  • 目的:抽象类用于作为接口或基类,提供公共的接口规范,而不需要自己实现具体功能。
  • 特点
    • 任何包含纯虚函数的类都是抽象类。
    • 抽象类只能作为基类使用,不能直接创建对象。
    • 派生类可以继承抽象类并实现其纯虚函数,从而可以实例化派生类对象。
    • 抽象类可以包含已实现的普通成员函数,但这不会改变其抽象类的特性。

示例

class Animal {
public:virtual void sound() = 0; // 纯虚函数void sleep() {std::cout << "Animal is sleeping" << std::endl;}
};class Dog : public Animal {
public:void sound() override {std::cout << "Woof!" << std::endl;}
};int main() {// Animal a; // 错误!无法实例化抽象类Dog d;d.sound(); // 输出: Woof!d.sleep(); // 输出: Animal is sleepingreturn 0;
}

在该例子中,Animal是一个抽象类,它定义了一个纯虚函数sound。派生类Dog实现了sound,因此可以实例化。同时,抽象类Animal也可以包含一个普通函数sleep,并且可以在派生类中使用。

✨总结

  • 纯虚函数:只声明而没有定义的虚函数,用于要求派生类实现某些行为。
  • 抽象类:包含纯虚函数的类,不能直接实例化,通常用作接口或基类。

📌 4 多态的原理

✨ 4.1 函数表指针

下面在32为系统下编译的结果为()
A: 编译报错 B:运行报错 C: 8 D:12

class Base{public:virtual void Func1(){cout << "Func1()" << endl;}protected:int _b = 1;char _ch = 'x';}int main(){Base b;cout << sizeof(b) << endl;return o;}

正确答案是12
分析:
在C++中,类对象的内存布局会受到成员变量虚函数表指针的影响:

  1. 成员变量的大小和对齐

    • int类型的成员变量 _b 占用4字节。
    • char类型的成员变量 _ch 占用1字节。
    • 由于内存对齐,_ch后面会填充3字节,使得成员变量部分占用8字节。
  2. 虚函数表指针

    • 含有虚函数的类会在其对象中包含一个虚函数表指针(也称虚表指针),用于在运行时支持多态。
    • 虚表指针占用4字节,指向虚函数表,表中存储了类的虚函数地址。
  3. 总内存占用

    • 成员变量8字节 + 虚表指针4字节 = 总共12字节。

重点

  • 虚函数表指针:每个包含虚函数的类都会有一个虚表指针,占用4字节,用于在对象中指向虚函数表,支持运行时的多态。
  • 内存对齐:成员变量按照对齐要求进行排列,使得结构体或类的内存大小可能大于简单的成员变量之和。

在这里插入图片描述

✨ 4.2 多态的原理

🚀 4.2.1 多态是如何实现的

class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }void func5() { cout << "Base::func5" << endl; }
protected:int a = 1;
};class Derive : public Base
{
public:// 重写基类的func1virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3(){cout << "Derive::func1" << endl;}
};
int main()
{//同类型的虚函数表是一样的,防止数据冗余。Base b1;Base b2;Derive d;return 0;
}

在这里插入图片描述
此时我们进行的是动态绑定,即运行时到指向的对象的虚表中确定对应函数的地址然后再进行调用这样就实现了指针或引⽤指向基类就调⽤基类的虚函数,指向派⽣类就调⽤派⽣类对应的虚函数。

在这里插入图片描述

🚀 4.2.2 虚函数表

虚函数表(Virtual Table)

在C++中,虚函数表(vtable)是编译器用于支持运行时多态的一种机制。当一个类中包含虚函数时,编译器会自动为这个类生成一个虚函数表,用于存储类的虚函数地址。每个对象实例则包含一个指向虚函数表的指针(称为虚表指针,vptr)。

虚函数表的工作原理

  1. 创建虚函数表:在包含虚函数的类中,编译器会为该类生成一个虚函数表。虚函数表包含了所有虚函数的地址。对于基类和派生类,编译器会为每个类单独生成虚函数表。

  2. 虚表指针(vptr):每个类的对象会在内存中包含一个指向其类对应虚函数表的指针,称为虚表指针(vptr)。这个指针在对象创建时自动初始化,以指向该对象所属的类的虚函数表。

  3. 调用过程:当通过基类指针或引用调用虚函数时,编译器会先通过对象的虚表指针(vptr)找到虚函数表(vtable),然后在虚函数表中查找并调用对应的虚函数,实现多态。

结构示例

假设有如下类结构:

class Base {
public:virtual void func1() { std::cout << "Base::func1" << std::endl; }virtual void func2() { std::cout << "Base::func2" << std::endl; }
};class Derived : public Base {
public:void func1() override { std::cout << "Derived::func1" << std::endl; }virtual void func3() { std::cout << "Derived::func3" << std::endl; }
};

在这个例子中,虚函数表的布局如下:

  1. Base类的虚函数表

    • 包含func1func2的地址。
    • func1指向Base::func1
    • func2指向Base::func2
  2. Derived类的虚函数表

    • 继承自Base,会包含func1func2,但func1指向Derived::func1(因为重写了该函数),func2仍指向Base::func2
    • func3Derived独有的虚函数,因此也会被添加到Derived类的虚函数表中。

内存布局示例

假设创建一个Derived类的对象d,则d的内存布局如下:

  • 虚表指针:指向Derived类的虚函数表。
  • 成员变量:包含类定义中的其他成员变量(例如int a;等)。
  • 虚函数表:指向的虚函数表中包含Derived::func1Base::func2Derived::func3的地址。

虚函数表的优点

  • 实现多态:虚函数表是C++实现运行时多态的核心,允许程序在运行时根据对象的实际类型选择函数。
  • 性能较高:通过虚表指针查找虚函数地址,只需一次指针查找和一次跳转,效率相对较高。

重要注意点

  • 虚表是类级别的:同一类的多个对象共享同一个虚函数表。
  • 虚表指针是对象级别的:每个对象实例都有自己的虚表指针,用于指向所属类的虚表。
  • 虚函数表只在有虚函数的类中存在:没有虚函数的类不会生成虚函数表。

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

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

相关文章

【反向迭代器】—— 我与C++的不解之缘(十七)

前言 ​ 在STL中的迭代器部分&#xff0c;之前只关注与正向迭代器&#xff0c;忽视了反向迭代器&#xff1b;现在来看一下反向迭代器到底是个什么东西&#xff0c;以及反向迭代器怎么实现&#xff0c;怎么为之前自己模拟实现的容器增加反向迭代器&#xff1f; 反向迭代器的使用…

无人机与低空经济:开启新质生产力的新时代

无人机技术作为低空经济的核心技术之一&#xff0c;正以其独特的优势在多个行业中发挥着重要作用&#xff0c;成为推动新质生产力革命的重要力量。无人机的应用范围广泛&#xff0c;从农业植保到物流配送&#xff0c;从城市监测到紧急救援&#xff0c;无人机的身影无处不在&…

ElasticSearch7.x入门教程之中文分词器 IK(二)

文章目录 前言一、内置分词器二、中文IK分词器&#xff08;第三方&#xff09;三、本地自定义四、远程词库总结 前言 ElasticSearch 核心功能就是数据检索&#xff0c;首先通过索引将文档写入 es。 查询分析则主要分为两个步骤&#xff1a; 1、词条化&#xff1a;分词器将输入…

宏景HCM uploadLogo.do接口存在任意文件上传漏洞

文章目录 免责声明漏洞描述搜索语法漏洞复现nuclei修复建议 免责声明 本文章仅供学习与交流&#xff0c;请勿用于非法用途&#xff0c;均由使用者本人负责&#xff0c;文章作者不为此承担任何责任 漏洞描述 宏景HCM是一款基于先进的人力资本管理体系和灵活开放的技术架构的企…

Linux:confluence8.5.9的部署(下载+安装+pojie)离线部署全流程 遇到的问题

原文地址Linux&#xff1a;confluence8.5.9的部署&#xff08;下载安装破ji&#xff09;离线部署全流程_atlassian-agent-v1.3.1.zip-CSDN博客 背景&#xff1a;个人使用2核4g 内存扛不住 总是卡住&#xff0c;但是流程通了所以 直接公司开服务器干生产 个人是centos7 公司…

基于web的音乐网站(Java+SpringBoot+Mysql)

目录 1系统概述 1.1 研究背景 1.2研究目的 1.3系统设计思想 2相关技术 2.1 MYSQL数据库 2.2 B/S结构 2.3 Spring Boot框架简介 3系统分析 3.1可行性分析 3.1.1技术可行性 3.1.2经济可行性 3.1.3操作可行性 3.2系统性能分析 3.2.1 系统安全性 3.2.2 数据完整性 …

MATLAB绘图基础11:3D图形绘制

参考书&#xff1a;《 M A T L A B {\rm MATLAB} MATLAB与学术图表绘制》(关东升)。 11.3D图形绘制 11.1 3D图概述 M A T L A B {\rm MATLAB} MATLAB的 3 D {\rm 3D} 3D图主要有&#xff1a; 3 D {\rm 3D} 3D散点图、 3 D {\rm 3D} 3D线图、 3 D {\rm 3D} 3D曲面图、 3 D {\rm…

集合卡尔曼滤波(Ensemble Kalman Filter),用于二维滤波(模拟平面上的目标跟踪),MATLAB代码

集合卡尔曼滤波&#xff08;Ensemble Kalman Filter&#xff09; 文章目录 引言理论基础卡尔曼滤波集合卡尔曼滤波初始化预测步骤更新步骤卡尔曼增益更新集合 MATLAB 实现运行结果3. 应用领域结论 引言 集合卡尔曼滤波&#xff08;Ensemble Kalman Filter, EnKF&#xff09;是…

Bug:gomonkey系列问题(undefined: buildJmpDirective)

Bug&#xff1a;gomonkey系列问题(undefined: buildJmpDirective) 最近拉代码发现其他同事写单测使用的是gomonkey&#xff0c;我本地mac m3芯片执行报错&#xff0c;gomonkey: undefined: buildJmpDirective。 对go convey不熟悉的朋友可以看这篇文章&#xff1a; Go Convey测…

Arcgis 地图制作

地图如下,不同历史时期&#xff1a;

【AI编程实战】安装Cursor并3分钟实现Chrome插件(保姆级)

Cursor介绍 https://www.cursor.com/ 一句话介绍&#xff1a;AI代码编辑器&#xff0c;当前最火的AI编程器 软件下载与安装 下载 打开Cursor官网下载&#xff0c;会根据操作系统的差别进行选择 https://www.cursor.com/ 这里下载的内容很小&#xff0c;是个安装器&#x…

C指针之舞——指针探秘之旅(2)

❤博客主页&#xff1a;折枝寄北-CSDN博客 ❤专栏&#xff1a;C语言学习专栏 在上一篇博客文章&#xff1a;C指针之舞——指针探秘之旅-CSDN博客中&#xff0c;我们学习了字符指针&#xff0c;指针数组&#xff0c;数组指针&#xff0c;数组传参和指针传参等内容&#xff0c;…

大数据新视界 -- Impala 性能优化:量子计算启发下的数据加密与性能平衡(下)(30 / 30)

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

RPC框架负载均衡

什么是负载均衡&#xff1f; 当一个服务节点无法支撑现有的访问量时&#xff0c;会部署多个节点&#xff0c;组成一个集群&#xff0c;然后通过负载均衡&#xff0c;将请求分发给这个集群下的每个服务节点&#xff0c;从而达到多个服务节点共同分担请求压力的目的。 负载均衡主…

JMeter监听器与压测监控之 InfluxDB

1. 简介 在本文中&#xff0c;我们将介绍如何在 Kali Linux 上通过 Docker 安装 InfluxDB&#xff0c;并使用 JMeter 对其进行性能监控。InfluxDB 是一个高性能的时序数据库&#xff0c;而 JMeter 是一个开源的性能测试工具&#xff0c;可以用于对各种服务进行负载测试和性能监…

Banana Pi BPI-CanMV-K230D-Zero 采用嘉楠科技 K230D RISC-V芯片设计

概述 Banana Pi BPI-CanMV-K230D-Zero 采用嘉楠科技 K230D RISC-V芯片设计,探索 RISC-V Vector1.0 的前沿技术&#xff0c;选择嘉楠科技的 Canmv K230D Zero 开发板。这款创新的开发板是由嘉楠科技与香蕉派开源社区联合设计研发&#xff0c;搭载了先进的勘智 K230D 芯片。 K230…

如何判断注入点传参类型--理论

注入点传参类型 在我们找到注入点后&#xff0c;首先要判断传参的类型&#xff0c;才能以正确的形式向数据库查询数据。 注入点传参一般分为数字型和字符型。 数字型&#xff1a;当传入的参数为整形时&#xff0c;存在SQL注入漏洞&#xff0c;就可以认为是数字型注入。 字符…

HarmonyOS(57) UI性能优化

性能优化是APP开发绕不过的话题&#xff0c;那么在HarmonyOS开发过程中怎么进行性能优化呢&#xff1f;今天就来总结下相关知识点。 UI性能优化 1、避免在组件的生命周期内执行高耗时操作2、合理使用ResourceManager3、优先使用Builder方法代替自定义组件4、参考资料 1、避免在…

AI Prompt Engineering

AI Prompt Engineering 简介 Prompt Engineering, 提示工程&#xff0c;是人工智能领域的一项技术&#xff0c;它旨在通过设计高效的提示词&#xff08;prompts&#xff09;来优化生成式 AI&#xff08;如 GPT、DALLE 等&#xff09;的输出。提示词是用户与生成式 AI 交互的核…

NVR接入录像回放平台EasyCVR视频融合平台加油站监控应用场景与实际功能

在现代社会中&#xff0c;加油站作为重要的能源供应点&#xff0c;面临着安全监管与风险管理的双重挑战。为应对这些问题&#xff0c;安防监控平台EasyCVR推出了一套全面的加油站监控方案。该方案结合了智能分析网关V4的先进识别技术和EasyCVR视频监控平台的强大监控功能&#…