c++中的虚特性(虚基类、虚函数、纯虚函数)

2023-11-10

1. 虚基类

1.1 虚基类作用

为了解决多继承时的命名冲突和冗余数据问题,使得派生类中只保留一份间接基类的成员。

其本质是是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class)

换个角度讲,虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身。

  • 建立对象时所指定的类称为最远派生类。
  • 虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。
  • 在整个继承结构中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化表中为虚基类的构造函数列出参数。如果未列出,则表示调用该虚基类的默认构造函数。
  • 在建立对象时,只有最远派生类的构造函数调用虚基类的构造函数,其他类对虚基类构造函数的调用被忽略。

1.2 虚基类作用机制

1.2.1 不使用虚继承和虚派生的问题

问题显而易见,会造成命名冲突和数据冗余,下面进行问题复现:

多继承很容易产生命名冲突,比如典型的菱形继承,类格如下图所示:

类 A 派生出类 B 和类 C,类 D 继承自类 B 和类 C,这个时候类 A 中的成员变量和成员函数继承到类 D 中变成了两份,一份来自 A-->B-->D 这条路径,另一份来自 A-->C-->D 这条路径。

在一个派生类中保留间接基类的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,但大多数情况下这是多余的:因为保留多份成员变量不仅占用较多的存储空间,还容易产生命名冲突。假如类 A 有一个成员变量 a,那么在类 D 中直接访问 a 就会产生歧义,编译器不知道它究竟来自 A -->B-->D 这条路径,还是来自 A-->C-->D 这条路径。下面是菱形继承的具体实现:

//间接基类A
class A{
protected:
    int m_a;
};

//直接基类B
class B: public A{
protected:
    int m_b;
};

//直接基类C
class C: public A{
protected:
    int m_c;
};

//派生类D
class D: public B, public C{
public:
    void seta(int a){ m_a = a; }  //命名冲突
    void setb(int b){ m_b = b; }  //正确
    void setc(int c){ m_c = c; }  //正确
    void setd(int d){ m_d = d; }  //正确
private:
    int m_d;
};

int main(){
    D d;
    return 0;
}

这段代码实现了上图所示的菱形继承,第 25 行代码试图直接访问成员变量 m_a,结果发生了错误,因为类 B 和类 C 中都有成员变量 m_a(从 A 类继承而来),编译器不知道选用哪一个,所以产生了歧义。

为了消除歧义,我们可以在 m_a 的前面指明它具体来自哪个类:

void seta(int a){ B::m_a = a; }

这样表示使用 B 类的 m_a。当然也可以使用 C 类的:

void seta(int a){ C::m_a = a; }

1.2.2虚继承和虚派生解决变量冗余

为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。

在继承方式前面加上 virtual 关键字就是虚继承,请看下面的例子:

//间接基类A
class A{
protected:
    int m_a;
};

//直接基类B
class B: virtual public A{  //虚继承
protected:
    int m_b;
};

//直接基类C
class C: virtual public A{  //虚继承
protected:
    int m_c;
};

//派生类D
class D: public B, public C{
public:
    void seta(int a){ m_a = a; }  //正确
    void setb(int b){ m_b = b; }  //正确
    void setc(int c){ m_c = c; }  //正确
    void setd(int d){ m_d = d; }  //正确
private:
    int m_d;
};

int main(){
    D d;
    return 0;
}

这段代码使用虚继承重新实现了上图所示的菱形继承,这样在派生类 D 中就只保留了一份成员变量 m_a,直接访问就不会再有歧义了。

虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。

现在让我们重新梳理一下本例的继承关系,如下图所示:

菱形继承和虚继承


观察这个新的继承体系,我们会发现虚继承的一个不太直观的特征:必须在虚派生的真实需求出现前就已经完成虚派生的操作。在上图中,当定义 D 类时才出现了对虚派生的需求,但是如果 B 类和 C 类不是从 A 类虚派生得到的,那么 D 类还是会保留 A 类的两份成员。

换个角度讲,虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身。

1.3 虚基类成员的可见性

因为在虚继承的最终派生类中只保留了一份虚基类的成员,所以该成员可以被直接访问,不会产生二义性。此外,如果虚基类的成员只被一条派生路径覆盖,那么仍然可以直接访问这个被覆盖的成员。但是如果该成员被两条或多条路径覆盖了,那就不能直接访问了,此时必须指明该成员属于哪个类。

以图2中的菱形继承为例,假设 A 定义了一个名为 x 的成员变量,当我们在 D 中直接访问 x 时,会有三种可能性:

  • 如果 B 和 C 中都没有 x 的定义,那么 x 将被解析为 A 的成员,此时不存在二义性。
  • 如果 B 或 C 其中的一个类定义了 x,也不会有二义性,派生类的 x 比虚基类的 x 优先级更高。
  • 如果 B 和 C 中都定义了 x,那么直接访问 x 将产生二义性问题。


可以看到,使用多继承经常会出现二义性问题,必须十分小心。上面的例子是简单的,如果继承的层次再多一些,关系更复杂一些,程序员就很容易陷人迷魂阵,程序的编写、调试和维护工作都会变得更加困难,因此我不提倡在程序中使用多继承,只有在比较简单和不易出现二义性的情况或实在必要时才使用多继承,能用单一继承解决的问题就不要使用多继承。也正是由于这个原因,C++ 之后的很多面向对象的编程语言,例如 JavaC#PHP 等,都不支持多继承。

2. 虚函数

2.1 虚函数作用

虚函数的存在是为了实现动态联编(又称为动态绑定),其作用是:如果使用了virtual关键字,程序将根据引用或指针指向的 对 象 类 型 来选择方法,否则使用引用类型或指针类型来选择方法。

这是一种多态性。为实现多态,可分为静态联编和动态联编。函数重载就是一种静态联编,而虚函数就是动态联编,即函数调用发生在运行阶段,而不是发生在编译阶段,动态联编的效率较低。

2.2 虚函数动态绑定演示

class A{
    private:
        int i;
    public:
        A();
        A(int num) :i(num) {};
        virtual void fun1();
        virtual void fun2();

    };

    class B : public A{
    private:
        int j;
    public:
        B(int num) :j(num){};
        virtual void fun2();// 重写了基类的方法
    };

    // 为方便解释思想,省略很多代码

    A a(1);
    B b(2);
    A *a1_ptr = &a;
    A *a2_ptr = &b;

    // 当派生类“重写”了基类的虚方法,调用该方法时
    // 程序根据 指针或引用 指向的  “对象的类型”来选择使用哪个方法
    a1_ptr->fun2();// call A::fun2();
    a2_ptr->fun2();// call B::fun1();
    // 否则
    // 程序根据“指针或引用的类型”来选择使用哪个方法
    a1_ptr->fun1();// call A::fun1();
    a2_ptr->fun1();// call A::fun1();

2.3 虚函数底层实现机制

实现原理:虚函数表+虚表指针

关键字:虚函数底层实现机制;虚函数表;虚表指针

编译器处理虚函数的方法是:为每个类对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,称为虚表指针(vptr),这种数组成为虚函数表(virtual function table, vtbl),即,每个类使用一个虚函数表,每个类对象用一个虚表指针。

举个例子:基类对象包含一个虚表指针,指向基类中所有虚函数的地址表。派生类对象也将包含一个虚表指针,指向派生类虚函数表。看下面两种情况:

如果派生类重写了基类的虚方法,该派生类虚函数表将保存重写的虚函数的地址,而不是基类的虚函数地址。

如果基类中的虚方法没有在派生类中重写,那么派生类将继承基类中的虚方法,而且派生类中虚函数表将保存基类中未被重写的虚函数的地址。注意,如果派生类中定义了新的虚方法,则该虚函数的地址也将被添加到派生类虚函数表中。

下面的图片体现了上述的底层实现机制:

2.4 虚函数的多继承

各种继承演示:https://blog.csdn.net/xp178171640/article/details/102670181#t3

2.5 虚函数注意事项

(1) 基类方法中声明了方法为虚后,该方法在基类派生类中是虚的。
(2) 若使用指向对象的引用或指针调用虚方法,程序将根据对象类型来调用方法,而不是指针的类型。
(3)如果定义的类被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚。
构造函数不能为虚函数。
基类的析构函数应该为虚函数。
友元函数不能为虚,因为友元函数不是类成员,只有类成员才能是虚函数。
如果派生类没有重定义函数,则会使用基类版本。
重新定义继承的方法若和基类的方法不同(协变除外),会将基类方法隐藏;如果基类声明方法被重载,则派生类也需要对重载的方法重新定义,否则调用的还是基类的方法。

3. 纯虚函数

3.1 纯虚函数作用

设置纯虚函数的目的是:

1,当想在基类中抽象出一个方法,且该基类只做能被继承,而不能被实例化;

2,这个方法必须在派生类(derived class)中被实现;

因为:在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。为了解决这个问题,方便使用类的多态性,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

c++中的虚特性(虚基类、虚函数、纯虚函数) 的相关文章

  • 使用链表进行堆排序

    我想知道是否有人曾经使用链表进行堆排序 如果他们可以提供代码 我已经能够使用数组进行堆排序 但尝试在链表中进行排序似乎不切实际 而且在你知道的地方很痛苦 我必须为我正在做的项目实现链接列表 任何帮助将不胜感激 我也用C 答案是 你不想在链表
  • C# 静态类型不能用作参数

    public static void SendEmail String from String To String Subject String HTML String AttachmentPath null String Attachme
  • 是否可以使用 http url 作为 DirectShow .Net 中源过滤器的源位置?

    我正在使用 DirectShow Net 库创建一个过滤器图 该过滤器图通过使用 http 地址和 WM Asf Writer 来流式传输视频 然后 在网页上 我可以使用对象元素在 Windows Media Player 对象中呈现视频源
  • C# 中的 Stack<> 实现

    我最近一直在实现递归目录搜索实现 并且使用堆栈来跟踪路径元素 当我使用 string Join 连接路径元素时 我发现它们被颠倒了 当我调试该方法时 我查看了堆栈 发现堆栈内部数组中的元素本身是相反的 即最近 Push 的元素位于内部数组的
  • Boost ASIO 串行写入十六进制值

    我正在使用 ubuntu 通过串行端口与设备进行通信 所有消息都必须是十六进制值 我已经在 Windows 环境中使用白蚁测试了通信设置 并得到了我期望的响应 但在使用 Boost asio 时我无法得到任何响应 以下是我设置串口的方法 b
  • Selenium - C# - Webdriver - 无法找到元素

    在 C 中使用 selenium 我试图打开浏览器 导航到 Google 并找到文本搜索字段 我尝试下面的 IWebDriver driver new InternetExplorerDriver C driver Navigate GoT
  • Unity手游触摸动作不扎实

    我的代码中有一种 错误 我只是找不到它发生的原因以及如何修复它 我是统一的初学者 甚至是统一的手机游戏的初学者 我使用触摸让玩家从一侧移动到另一侧 但问题是我希望玩家在手指从一侧滑动到另一侧时能够平滑移动 但我的代码还会将玩家移动到您点击的
  • Linux 上的 RTLD_LOCAL 和dynamic_cast

    我们有一个由应用程序中的一些共享库构成的插件 我们需要在应用程序运行时更新它 出于性能原因 我们在卸载旧插件之前加载并开始使用新插件 并且只有当所有线程都使用旧插件完成后 我们才卸载它 由于新插件和旧插件的库具有相同的符号 我们dlopen
  • 来自嵌入图像的 BitmapSource

    我的目标是在 WPF 窗口上重写 OnRender 方法中绘制图像 someImage png 它是嵌入资源 protected override void OnRender System Windows Media DrawingCont
  • 条件类型定义

    如果我有一小段这样的代码 template
  • 如何防止 Blazor NavLink 组件的默认导航

    从 Blazor 3 1 Preview 2 开始 应该可以防止默认导航行为 https devblogs microsoft com aspnet asp net core updates in net core 3 1 preview
  • 我们可以通过指针来改变const定义的对象的值吗?

    include
  • 对于 C# Express 用户来说,有哪些好的工具可以识别可能重复的代码? [关闭]

    Closed 这个问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 也可以看看 有什么工具可以检查重复的 VB NET 代码吗 https stackoverflow c
  • SQLAPI++ 的免费替代品? [关闭]

    Closed 这个问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 是否有任何免费 也许是开源 的替代品SQLAPI http www sqlapi com 这个库看起来
  • 当Model和ViewModel一模一样的时候怎么办?

    我想知道什么是最佳实践 我被告知要始终创建 ViewModel 并且永远不要使用核心模型类将数据传递到视图 这就说得通了 让我把事情分开 但什么是Model 和ViewModel一模一样 我应该重新创建另一个类还是只是使用它 我觉得我应该重
  • Xamarin Forms Binding - 访问父属性

    我无法访问页面的 ViewModel 属性以便将其绑定到 IsVisible 属性 如果我不设置 BindingContext 我只能绑定它 有没有办法可以在设置 BindingContext 的同时访问页面的 viewmodel root
  • 调用 .ToArray() 时出现 ArgumentException

    我有一个经常被清除的列表 代码完全是这样的 VisitorAgent toPersist List
  • 如何在C#中控制datagridview光标移动

    我希望 datagridview 光标向右移动到下一列 而不是在向单元格输入数据后移动到下一行 我试图通过 dataGridView1 KeyDown 事件捕获键来控制光标 但这并不能阻止光标在将数据输入到单元格后移动到下一行 提前感谢你的
  • 如何组合两个 lambda [重复]

    这个问题在这里已经有答案了 可能的重复 在 C 中组合两个 lambda 表达式 https stackoverflow com questions 1717444 combining two lamba expressions in c
  • 如何在 C# 中获取 CMD/控制台编码

    我需要指定正确的代码页来使用 zip 库打包文件 正如我所见 我需要指定控制台编码 在我的例子中为 866 C Users User gt mode Status for device CON Lines 300 Columns 130 K

随机推荐

  • 【操作系统】王道考研 p14 调度算法的评价指标

    视频 知识总览 最后的思维导图最重要 CPU利用率 系统吞吐量 一个比喻 吞相当于把作业拿过来 吐相当于作业做完了拿走 周转时间 周转时间 作业完成时间 作业提交时间 带权周转时间 作业周转时间 作业工作时间 带权周转时间越大 说明等的越久
  • 解决执行 spark.sql 时版本不兼容的一种方式

    场景描述 hive 数据表的导入导出功能部分代码如下所示 使用 assemble 将 Java 程序和 spark 相关依赖一起打成 jar 包 最后 spark submit 提交 jar 到集群执行 public class Spark
  • CRM--今日简报(接口实现)

    接口定义 按照开始时间和结束时间查询今日基本数据 线索数量 商机数量 合同数量 成交的金额 需求 当前用户在当前的线索数量 商机数量 合同数量 成交的金额 当前用户的线索 商机 合同 成交金额都不需要考虑状态 只要是今日做的 哪怕是回收了删
  • 移动FPGA使用Verilog图像处理verilator模拟和ice40执行

    概述 在verilog中实现简单的图像处理操作 该项目围绕一个中央图像处理模块 image processing v 展开 该模块可以包含在使用 verilator 的模拟环境中 也可以包含在 ice40 Ultraplus fpga 的
  • FLASH和EEPROM的区别和扩展

    EEPROM和FLASH总体差异 部分MCU片内不带程序存储器ROM 可执行代码只能放在外面的EEPROM FLASH 外扩 中 单片机对于数据的处理和系统的大量数据采集 需要考虑数据的长期保存和掉电保存等问题 因此外扩EEPROM就是这系
  • Scala-13:集合-列表List

    Scala 13 集合 列表List 一 不可变 List 1 说明 List 默认为不可变集合 创建一个 List 数据有顺序 可重复 List 默认为不可变集合 val list List Int List 1 2 3 4 3 遍历 L
  • 总结:js中Object.setPrototypeOf和Object.create的区别

    首先知道什么是原型 只要这样才能理解下面的内容 Object setPrototypeOf obj prototype 他是将prototype作为已知对象obj的原型 Object create prototype 是创建一个以proto
  • Java-动态代理原理

    1 什么是代理模式 1 例子 1 例子 在了解Java动态代理技术之前 先了解一下什么是代理模式 其实代理模式在生活中很常见 比如房东与中介其实就是一个代理的过程 房东有自己的房子 在代理模式中扮演角色是被代理对象 中介扮演角色是代理对象
  • java 行转列_Java程序员从阿里面试回来,这些面试题你们会吗?

    序言 简单的介绍一下自己的工作经历与职责 在校或者在工作中主要的工作内容 主要负责的内容 你的信息一清二白的写在简历上 这个主要为了缓解面试者的压力 介绍下自己最满意的 有技术亮点的项目或平台 重点介绍下自己负责那部分的技术细节 主要考察应
  • selenium爬虫与配置谷歌浏览器的driver问题

    用selenium爬虫时 明明已经安装了selenium模块 程序却运行不了 在使用selenium之前必须先配置浏览器对应版本的webdriver 本文主要涉及驱动有问题driver 网上有很多手动的方法 查看谷歌浏览的版本然后在其他博主
  • [ESP32]ESP32 Arduino BLE调试 / 与安卓蓝牙数据交互

    帖子导航 ESP32 ESP32 Arduino开发环境搭建 首先得有支持包吧 ESP32 BLE支持包 进入arduino IDE gt 文件 gt 首选项 gt 项目文件位置 到该路径下 打开git工具 项目地址 https githu
  • Python做的一个猜数字应用

    首先需求一共有五次猜测机会 在五次机会中才对就赢了 结束游戏 五次都猜错就输了 也结束游戏 首先先画个草图 这是我画的草图 再根据草图编写一个窗口 一个Label 一个Entry 一个按钮 然后编写功能 将功能绑定函数 import ran
  • Vue3基础(三)__isRef___isReactve

    isRef isRef方法通过vue解构出来 import isRef from vue 主要是为了判断自己生成的响应数据是否是通过isRef 方法生成的
  • elementui 之input 框总结

    1 elementUI的input框 需要用v model绑定一个变量 这个变量相当于原生input框的value 2 placeholder属性和原生的一样
  • centos7-docker安装redis

    1 docker 拉去redis镜像 docker pull redis 2 创建实例并启动 mkdir p mydata redis conf touch mydata redis conf redis conf docker run p
  • 计算机网络安全防范的论文,计算机网络安全防范策略毕业论文.doc

    计算机网络安全防范策略毕业论文 PAGE PAGE PAGE I 摘 要 信息技术的使用给人们的生活 工作带来了数不尽的便捷和好处 然而计算机信息技术也和其他技术一样是一把双刃剑 当大部分人们使用信息技术提高工作效率 为社会创造更多财富的同
  • oracle数据库下创建用户

    一 使用sqlplus 创建用户 1 登陆到sqlplus 2 键入 create user user name identified by password user name 要用户名 password 对应的密码 3 键入 Grant
  • v-loading

    使用v loading在接口为请求到数据之前 显示加载中 直到请求到数据后消失
  • OpenLayers - Vector绘制地图省市区(十)

    简介 本文讲解经常在开发中出现的功能 绘制地图省市区 主要使用Vector图层通过绘制多边行的方法 绘制出省市区的多边行 把该图层添加到地图图层上 就实现了绘制省市区图形 Vector 矢量图层 在客户端呈现的矢量数据 构成一个矢量图层需要
  • c++中的虚特性(虚基类、虚函数、纯虚函数)

    1 虚基类 1 1 虚基类作用 为了解决多继承时的命名冲突和冗余数据问题 使得派生类中只保留一份间接基类的成员 其本质是是让某个类做出声明 承诺愿意共享它的基类 其中 这个被共享的基类就称为虚基类 Virtual Base Class 换个