深入理解C++多态-虚函数

引言

C++多态的实现方式可以分为静态多态和动态多态,其中静态多态主要有函数重装和模板两种方式,动态多态就是虚函数。
下面我们将通过解答以下几个问题的方式来深入理解虚函数的原理:

  1. 为什么要引入虚函数?(用来解决什么问题)
  2. 虚函数底层实现原理
  3. 使用虚函数时需要注意什么?

正文

为什么要引入虚函数?

在回答这个问题之前,我们先看一个示例:
假设我们正在开发一个图形编辑器,其中包含各种类型的图形元素,比如圆形、矩形、多边形等。我们要如何管理所有图形对象呢?

  • 甲同学的方案
class Circle {
public:void draw() const {// 实现绘制圆形的代码}
};class Rectangle {
public:void draw() const {// 实现绘制矩形的代码}
};// 管理图形对象:
std::vector<Circle*> circle_shapes;
std::vector<Rectangle*> rectangle_shapes;
circle_shapes.push_back(new Circle());
rectangle_shapes.push_back(new Rectangle());// 刷新绘制图形
for (auto shape : circle_shapes) {shape->draw();
}
for (auto shape : rectangle_shapes) {shape->draw();
}

甲同学实现的方法比较直白简单,有多少种类型的图形就定义多少种类,维护和绘制都需要根据图形类型数量来修改。
当我要新增一种图形类型Polygon时,就需要新增以下代码:

class Polygon {
public:void draw() const {// 实现绘制矩形的代码}
};// 管理图形对象:
std::vector<Polygon*> polygon_shapes;
polygon_shapes.push_back(new Polygon());// 刷新绘制图形
for (auto shape : polygon_shapes) {shape->draw();
}

这种方式的扩展性、可维护性都是最差的。

  • 乙同学的方案
class Shape {
public:virtual void draw() const = 0; // 纯虚函数,使得Shape成为抽象基类
};class Circle : public Shape {
public:void draw() const override {// 实现绘制圆形的代码}
};class Rectangle : public Shape {
public:void draw() const override {// 实现绘制矩形的代码}
};// 管理图形对象:
std::vector<Shape*> shapes;
shapes.push_back(new Circle());
shapes.push_back(new Rectangle());// 刷新绘制图形
// 通过基类指针调用适当的draw方法
for (auto* shape : shapes) {shape->draw(); // 在运行时决定调用哪个类的draw方法
}

乙同学将图形抽象出一个基类Shape,然后继承该类来实现CircleRectangle;同时将通用接口设计成虚函数,派生类重写虚函数,在运行时根据对象来调用哪个类的函数。
这种方式既简化了代码,又提高了可扩展性和可维护性。

具体来说,虚函数解决的主要问题是如何在不完全知道对象类型的情况下,调用正确的函数。在没有虚函数的情况下,函数的调用在编译时就已经确定了(这称为静态绑定)。但是,如果我们想要在运行时根据对象的实际类型来决定调用哪个函数(动态绑定),就需要使用虚函数。

虚函数底层实现原理

我们先介绍一下虚函数实现原理中最重要的两个东西:虚函数表(也称虚表,vtable)和虚指针(也称虚表指针,vptr)。

虚函数表

每个包含虚函数的类或其派生类都会拥有一个虚函数表。这个表是一个编译时生成的静态数组,存储在每个类的定义中。
虚函数表主要包含以下元素:

  • 虚函数指针:表中的每一个条目都是指向类中每个虚函数的指针。这包括从基类继承来的虚函数,如果在派生类中被重写,则指向新的函数地址。
  • 类型信息:在支持运行时类型识别(RTTI)的系统中,虚函数表还可能包含指向类型信息的指针,这有助于typeiddynamic_cast等操作。

虚指针

虚指针是每个对象中的一个隐含成员,如果该对象的类包含虚函数。在对象构造时,编译器设置这个虚指针指向相应类的虚函数表。

每次通过类的实例调用虚函数时,程序会首先通过虚指针访问虚函数表,然后通过虚函数表定位到具体的函数地址并调用。这个过程是在运行时完成的,因此允许函数调用根据对象的实际类型动态绑定,而非编译时决定。

想要了解虚函数的实现原理,就需要先了解类的内存布局,通过内存布局来直观地学习虚函数的原理。

内存布局

普通类的内存布局
class N {
public:void funA() { std::cout << "funA()" << std::endl; }void funB() { std::cout << "funB()" << std::endl; }int a;int b;
};

class N的内存布局如下:

1>class N	size(8):
1>	+---
1> 0	| a
1> 4	| b
1>	+---

想要看一个类的内存布局,只需要通过添加命令行:/d1 reportSingleClassLayoutXXX(其中XXX就是你想要看的类名)即可。

普通的类只会存储数据成员。

  • 普通的类中为什么没有维护成员函数呢?

类的成员函数在编译后存储在程序的代码段中,被程序中所有对象共享。
因为一个类的不同实例对象所执行的成员函数是一样的,没有必要在实例对象中再复制维护了。所有同类的实例对象使用相同的函数代码(通过隐含的this指针来访问对象的成员变量和成员函数),不仅节省内存,也使得程序更加高效。

这里不再详细介绍函数调用的原理了,这是最基础的知识… …

基类的内存布局
class Base {
public:virtual void vFunA() = 0;virtual void vFunB() {}void funA() {}void funB() {}int a;int b;
};

class Base的内存布局如下:

1>class Base	size(12):
1>	+---
1> 0	| {vfptr}
1> 4	| a
1> 8	| b
1>	+---
1>Base::$vftable@:
1>	| &Base_meta
1>	|  0
1> 0	| &Base::vFunA
1> 1	| &Base::vFunB

class Base是一个带虚函数的类,可以看到它的内存布局和普通类有很大的区别。
class Base中的{vfptr}是一个指向虚函数表(vftable)的指针。
Base::$vftable@就是虚函数表,其中&Base_metaclass Base的元数据(该类的类型信息,用于运行时类型识别)。虚函数表内主要是维护该类的虚函数地址。

派生类A的内存布局
class A : public Base {
public:virtual void vFunA() override {}virtual void vFunB() override {}void funA() {}void funB() {}int c;
};

class A的内存布局如下:

1>class A	size(16):
1>	+---
1> 0	| +--- (base class Base)
1> 0	| | {vfptr}
1> 4	| | a
1> 8	| | b
1>	| +---
1>12	| c
1>	+---
1>A::$vftable@:
1>	| &A_meta
1>	|  0
1> 0	| &A::vFunA
1> 1	| &A::vFunB

派生类A的内存布局和基类又不一样了。
因为class A继承class Base,所以内存布局就包含了基类的数据,然后才是自己的成员c
这里需要注意的是虚函数表中,虚函数地址发生了变化,原来虚函数表中的虚函数地址分别是&Base::vFunA&Base::vFunB,现在虚函数地址被更新成class A的虚函数地址了。

派生类B的内存布局
class B : public Base {
public:virtual void vFunA() override {}void funA() {}void funB() {}int d;
};

class B的内存布局如下:

1>class B	size(16):
1>	+---
1> 0	| +--- (base class Base)
1> 0	| | {vfptr}
1> 4	| | a
1> 8	| | b
1>	| +---
1>12	| d
1>	+---
1>B::$vftable@:
1>	| &B_meta
1>	|  0
1> 0	| &B::vFunA
1> 1	| &Base::vFunB

派生类B和A的主要区别就是没有重写虚函数vFunB,所以在虚函数表中可以看到虚函数vFunB的地址没有被更新,还是指向基类的虚函数地址。

所以,从上面四个类的内存布局可以看出:

  1. 只要写了虚函数,就会多生成一个虚函数表,并且还有虚指针指向虚函数表。
  2. 派生类继承基类,并重写虚函数后,虚函数表对应的虚函数地址将被更新。

使用虚函数时需要注意什么?

使用虚函数时需要遵循以下规则:

  1. 虚函数不能是静态的

虚函数的目的是为了实现动态多态,和静态函数在本质上是冲突的。

  1. 要实现运行时多态性,必须使用基类类型的指针或引用来访问虚函数

如果调用是通过对象实例(而非指针或引用),则会发生静态绑定,在编译时,编译器确定了要调用的函数版本,这种确定不会延迟到运行时。

  1. 虚函数的原型在派生类和基类中必须保持一致

虚函数的原型指的是虚函数的名称、返回类型、参数列表、const属性。
这句话的意思就是说派生类重写的虚函数需要和基类的虚函数名称、返回类型、参数列表、const属性都保持一致。

  1. 类可以有虚析构函数,但不能有虚构造函数
  • 首先我们先分析前半句:类可以有虚析构函数

其实在继承关系中,析构函数必须是虚函数。因为当析构函数不是虚函数,那么通过基类指针释放派生类对象时,只能调用基类的析构函数,导致派生类中的部分资源无法释放。

  • 后半句:但不能有虚构造函数

调用虚函数是通过虚指针定位到虚函数表,然后找到对应的虚函数地址。如果构造函数是虚函数,那么调用构造函数是不是需要先通过虚指针来定位虚函数表了,但虚指针的初始化发生在构造函数阶段,所以这里有冲突。

未完待续… …

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

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

相关文章

2024年最新信息安全标准汇总

这些标准是我们在数字化时代保障网络安全、数据安全的重要基石&#xff0c;对于维护国家安全、企业利益和个人信息安全具有至关重要的作用。 随着科技的快速发展&#xff0c;网络空间的安全威胁也在不断演变&#xff0c;从复杂的网络攻击到个人信息泄露&#xff0c;这些安全风…

JAVA面试题大全(十四)

1、Kafka 可以脱离 Zookeeper 单独使用吗&#xff1f;为什么&#xff1f; kafka不能脱离zookper单独使用&#xff0c;因为kafka使用zookper管理和协调kafka的节点服务器。 2、Kafka 有几种数据保留的策略&#xff1f; Kafka提供了多种数据保留策略&#xff0c;这些策略用于定…

哈希双指针

文章目录 一、哈希1.1两数之和1.2字母异位词分组1.3最长子序列 二、双指针2.1[移动零](https://leetcode.cn/problems/move-zeroes/description/?envTypestudy-plan-v2&envIdtop-100-liked)2.2[盛最多水的容器](https://leetcode.cn/problems/container-with-most-water/d…

嵌入式0基础开始学习 ⅠC语言(7)指针

0.问题引入 int a 5; a 1024; //把1024存放到变量a的地址中去 b a; // 取变量a的值&#xff0c;赋值给b >在c语言中&#xff0c;任何一个变量&#xff0c;都有两层含义 (1)代表变量的存储单元的地址&#xff1a;变量的地址…

蓝桥楼赛第30期-Python-第三天赛题 统计学习数据题解

楼赛 第30期 Python 模块大比拼 统计学习数据 介绍 JSON&#xff08;JavaScript Object Notation, /ˈdʒeɪsən/&#xff09;是一种轻量级的数据交换格式&#xff0c;最初是作为 JavaScript 的子集被发明的&#xff0c;但目前已独立于编程语言之外&#xff0c;成为了通用的…

分享10个国内可以使用的GPT中文网站

在今天的人工智能领域&#xff0c;基于对话的语言模型已成为研究的热点&#xff0c;尤其是像 ChatGPT 这样因其出色的语言理解与对话交互能力而广受关注的模型。本文将介绍10个国内可以直接使用GPT的网站&#xff0c;旨在为大家在选择和使用这些优秀的AI工具时提供有价值的参考…

使用pyqt绘制一个爱心!

使用pyqt绘制一个爱心&#xff01; 介绍效果代码 介绍 使用pyqt绘制一个爱心&#xff01; 效果 代码 import sys from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget from PyQt5.QtGui import QPainter, QPen, QBrush, QColor from PyQt5.QtCore import Qt, Q…

[保姆式教程]使用目标检测模型YOLO V8 OBB进行旋转目标的检测:训练自己的数据集(基于卫星和无人机的农业大棚数据集)

最近需要做基于卫星和无人机的农业大棚的旋转目标检测&#xff0c;基于YOLO V8 OBB的原因是因为尝试的第二个模型就是YOLO V8&#xff0c;后面会基于YOLO V9模型做农业大棚的旋转目标检测。YOLO V9目前还不能进行旋转目标的检测&#xff0c;需要修改代码 PS:欢迎大家分享农业大…

【研发日记】Matlab/Simulink技能解锁(九)——基于嵌入式处理器仿真

文章目录 前言 基于嵌入式处理器仿真 使用方式 第一步&#xff0c;硬件连接 第二步&#xff0c;配置硬件资源 第三步&#xff0c;配置XCP协议 第四步&#xff0c;加载Contrl Model 第五步&#xff0c;运行仿真 第六步&#xff0c;仿真报告 分析和应用 总结 参考资料 前言…

无线技术整合到主动噪声控制(ANC)增强噪声降低性能

主动噪声控制&#xff08;ANC&#xff09;已成为一种广泛使用的降噪技术。基本原理是通过产生与外界噪音相等的反向声波&#xff0c;将噪音中和&#xff0c;从而达到降噪的效果。ANC系统通常包括以下几个部分&#xff1a;参考麦克风、处理芯片、扬声器和误差麦克风。参考麦克风…

家政保洁服务小程序怎么做?家政公司快速搭建专属小程序

在数字化时代背景下&#xff0c;家政保洁服务行业也迎来了线上转型的新机遇。家政保洁服务小程序&#xff0c;作为一种新型的线上服务平台&#xff0c;不仅能够提升家政公司的服务效率&#xff0c;还能为顾客提供更加便捷的预约上门服务体验。那么家政保洁服务小程序怎么做呢&a…

AI与量子计算:科技新时代的双重飞跃

在科技的浪潮中,每一次技术革新都如同一次深海潜行,探寻着未知的奥秘。近年来,人工智能(AI)和量子计算两大领域的发展尤为引人注目,它们不仅代表了科技的未来趋势,更是人类社会进步的强大动力。本文将深入探讨这两项技术的最新进展、潜在影响以及它们之间的潜在联系。 …

2024年3月小程序类目调整汇总公告

各位小程序开发者&#xff1a; 为进一步加强平台的规范管理&#xff0c;优化开发者类目选择体验&#xff0c;现对以下类目进行调整&#xff0c;请各位开发者知悉。 类目调整 #【文娱-小说】 现资质要求 &#xff08;3选1&#xff09;&#xff1a; 1、提供《互联网出版许可…

从零开始搭建一个SpringBoot项目

目录 Spring BootSpring Boot 项目开发环境1、快速创建SpringBoot项目2、pom.xml 添加 Meavn 依赖3、配置application.yml4、验证数据库是否连接成功5、配置 Druid 数据源 Spring Boot 整合 MyBatis1、准备依赖2、application-dev.yml 配置3、启动类添加Mapper接口扫描器4、设置…

BWVS 靶场测试

一、PHP弱类型 is_numeric() 输入&#xff1a;127.0.0.1/BWVS/bug/php/code.php # 1、源代码分析 如果num不是数字&#xff0c;那么就输出num&#xff0c;同时如果num1&#xff0c;就输出flag。即num要是字符串又要是数字 # 2、函数分析&#xff1a; is_numeric()函数&…

使用Nginx的Mirror模块的指南

Nginx 是一个广泛使用的 web 服务器和反向代理服务器&#xff0c;性能出色且易于配置。Nginx 提供了各种模块来扩展其功能&#xff0c;其中一个有用的模块是 mirror 模块。本文将详细介绍 Nginx 的 mirror 模块&#xff0c;包括其用途、使用场景、注意事项以及示例代码。 1. m…

《最新出炉》系列入门篇-Python+Playwright自动化测试-40-录制生成脚本

宏哥微信粉丝群&#xff1a;https://bbs.csdn.net/topics/618423372 有兴趣的可以扫码加入 1.简介 各种自动化框架都会有脚本录制功能&#xff0c; playwright这么牛叉当然也不例外。很早之前的selenium、Jmeter工具&#xff0c;发展到每种浏览器都有对应的录制插件。今天我们…

牛客NC392 参加会议的最大数目【中等 贪心+小顶堆 Java/Go/PHP 力扣1353】

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/4d3151698e33454f98bce1284e553651 https://leetcode.cn/problems/maximum-number-of-events-that-can-be-attended/description/ 思路 贪心优先级队列Java代码 import java.util.*;public class Solution {/**…

java面试高级篇(JVM、Mysql、Redis、Kafka)

文章目录 面试专题-java高级篇1. JVM有做过jvm的调优吗?常用的jvm参数调优有哪些?如果jvm持续一段时间频繁的发生Young GC (轻GC) 可能原因有哪些? 2. Mysql2.1. 基本功(见为知笔记)2.2. 什么是索引2.3. 索引的优劣势2.4. MySQL的索引结构2.4.1. B-Tree索引2.4.2. BTree索引…

外卖系统源码开发全攻略:外卖小程序与后台管理系统的设计与实现

今天&#xff0c;小编将详细介绍外卖系统源码的开发全攻略&#xff0c;从需求分析到设计与实现&#xff0c;为开发者提供全面指导。 一、需求分析 1.用户需求 用户是外卖系统的核心&#xff0c;需满足以下基本需求&#xff1a; -浏览菜单并下单 -实时追踪订单 -多种支付方…