文章目录

  • 纯虚函数
  • 抽象类
  • 多重继承
    • 二义性问题
    • 菱形继承
  • 虚基类
    • 从内存布局看虚继承的底层实现原理

纯虚函数

纯虚函数是一种特殊的虚函数,在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。这就是纯虚函数的作用。

一般格式如下:

class <类名>
{virtual <类型><函数名>(<参数表>)=0;…
};

例如:

class A
{virtual int funcation(int val) = 0; // 定义纯虚函数
}

纯虚函数可以让类先具有一个操作名称,而没有操作内容,让派生类在继承时再去具体地给出定义

引入原因:

  • 为了方便使用多态特性,我们常常需要在基类中定义虚函数。
  • 在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。

为了解决上述问题,引入了纯虚函数的概念,即 将函数定义为纯虚函数

若要使派生类为非抽象类,则编译器要求在派生类中,必须对纯虚函数予以重写以实现多态性。同时含有纯虚函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。


抽象类

凡是含有纯虚函数的类叫做抽象类。这种类不能定义对象,只是作为基类为派生类服务。但可以定义指针或引用

除非在派生类中完全实现基类中所有的的纯虚函数,否则,派生类也变成了抽象类,不能实例化对象。在派生类实现该纯虚函数后,定义抽象类对象的指针,并指向或引用子类对象。

一般而言纯虚函数的函数体是缺省的,但是也可以给出纯虚函数的函数体(此时纯虚函数变为虚函数),这一点经常被人们忽视。

  • 在定义纯虚函数时,不能定义虚函数的实现部分
  • 在没有重新定义这种纯虚函数之前,是不能调用这种函数的

抽象类的唯一用途是为派生类提供基类,纯虚函数的作用是作为派生类中的成员函数的基础,并实现动态多态性

继承于抽象类的派生类如果不能实现基类中所有的纯虚函数,那么这个派生类也就成了抽象类。因为它继承了基类的抽象函数,只要含有纯虚函数的类就是抽象类。

纯虚函数已经在抽象类中定义了这个方法的声明,其它类中只能按照这个接口去实现。

示例程序如下:

#include <iostream>
using namespace std;/*
** 抽象基类:不能被实例化的基类。 它仅仅只有一个用途,用来派生出其他类。
** 1. 要定义抽象基类,可使用纯虚函数,纯虚函数可当做接口使用
** 2. 基类的纯虚函数,在派生类中必须实现。 虚函数可以不用必须实现
*//*定义抽象基类*/
class Base
{public:/*** 虚函数=0,这个形式为纯虚函数,告诉编译器,必须在派生类中进行实现** 可看成派生类的接口,调用此接口时,调用相应派生类的方法*/virtual void Fun() = 0;
};/*
** 实例化对象时,将创建两个对象,子对象和基类对象,
** 通过从调用的构造函数可以看出
*/
class Derive: public Base
{public:/* 若不实现此函数,编译将会出错 */void Fun() { cout << "Derive::Fun" << endl; } Derive() { cout << "Derive()" << endl; }~Derive() { cout << "~Derive()" << endl; }
};class Derive2: public Base
{public:/* 若不实现此函数,编译将会出错 */void Fun() { cout << "Derive2::Fun" << endl; } Derive2() { cout << "Derive2()" << endl; }~Derive2() { cout << "~Derive2()" << endl; }
};void Show(Base& Base) {Base.Fun();
}int main()
{//Base base;  // error,抽象基类不可实例化对象Derive derive; // 实例化对象Derive2 derive2; // 实例化对象Base* base = &derive; Base* base2 = &derive2;base->Fun();base2->Fun();return 0;
}

我们可以看到,但我们给抽象基类实例化对象时,编译会报错如下:

那么由下图我们可以看出,当我们使用基类指针指向不同的派生类对象时,调用Fun函数将调用基类指针所指对象的Fun函数:


多重继承

之前所介绍的单一继承是指:一个派生类只继承一个基类。

多重继承指的是一个类可以同时继承多个不同基类的行为和特征功能

语法格式如下:

Class <类名> :<继承方式> <基类1>,<继承方式> <基类2>···
{···}

示例程序如下:

class Base1
{public:Base1() { cout << "Base1()" << endl; }~Base1() { cout << "~Base1()" << endl; }
};
class Base2
{public:Base2() { cout << "Base2()" << endl; }~Base2() { cout << "~Base2()" << endl; }
};/*
**  : 之后称为类派生表,表的顺序决定基类构造函数
** 调用的顺序,析构函数的调用顺序正好相反
*/
class Derive : public Base2, public Base1
{};
int main()
{Derive derive;return 0;
}

上述程序算是一段简单的多重继承了,编译运行是没有错误的。平时绝大部分时候,我们都只使用单继承,所为单继承是针对多重继承而言的,即一个类只有一个基类

我们看到运行结果是先是Base2构造,然后Base1构造:

那么多重继承会带来各种各样的问题:

  • 二义性问题
  • 菱形继承导致派生类持有间接基类的多份拷贝

二义性问题

使用多重继承, 一个不小心就可能因为二义性问题而导致编译错误。

最简单的例子,在上面的基类Base1和Base2中若存在相同的方法或成员变量,那么在派生类Derive中或使用Derive的对象时,若使用这个方法或成员变量时,那么编译器不知道需要调用Base1中的方法还是Base2中的方法或成员变量。

当然我们可以给方法添加作用域来解决这个问题,我们也可以通过在派生类Derive中重新定义这个方法来覆盖基类中的同名方法,从而使编译器能够正常工作。

示例如下:

编译后,我们发现错误,即show函数的调用存在二义性问题:

修改如下,我们为show函数加上作用域,即可正常运行:(另外一种解决方法是我们在Derive派生类中重写Show方法,将隐藏基类方法,程序也可正常运行)


菱形继承

我们来看一个示例图:

  • Derive1继承了Base类,它的成员有ma、mb、mc
  • Derive2继承了Base类,它的成员有ma、mb、md
  • Derive3继承了Derive1类和Derive2类,它的成员有ma、mb、mc、ma、mb、md

我们发现了问题:Derive3由于多重继承,拿到了它的间接基类Base的两份数据拷贝,这并不是我们所期望的。

这将会引起问题,例如,通常可以将派生类对象的地址赋给基类指针,但现在将出现二义性:

Derive3 derive3;
Base *base = &derive3; // error

如下图所示:

通常,这种赋值将把基类指针设置为派生类对象中的基类指针的地址。但是现在Derive3中包含两个地址可选择,所以应该使用类型转换来指定对象

这将使得使用基类指针来引用不同的对象(多态性)复杂化。

为了解决上述问题,C++引入了一种新技术——虚基类(virtual base class)。


虚基类

虚继承和虚函数是完全无相关的两个概念。

虚继承是解决C++多重继承问题的一种手段,从不同途径继承来的同一基类,会在派生类中存在多份拷贝。这将存在两个问题:

  • 浪费存储空间
  • 存在二义性问题。

通常可以将派生类对象的地址赋值给基类对象,实现的具体方式是,将基类指针指向继承类(继承类有基类的拷贝)中的基类对象的地址,但是多重继承可能存在一个基类的多份拷贝,这就出现了二义性

虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。例如,通过在类声明中使用关键字 virtual ,可以使这些派生类只保留虚基类的一个副本。

例如上例,如下声明后,Derive1 和 Derive2 虚继承了 Base后, Base 成为了Derive1 和 Derive2 的虚基类,那 Derive3 就可以安全的多继承 Derive1 和 Derive2了。如下图:

从内存布局看虚继承的底层实现原理

了解了虚基类的用法和作用后,我们来看一下虚基类的底层到底是怎样实现的
之前我们介绍虚函数时,介绍了其实现原理即虚函数指针 vfptr 和虚函数表vftable。

那么虚基类的实现是产生虚基类表指针 vbptr 与虚基类表 vbtable。

虚继承底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)。

需要强调的是,虚基类依旧会在派生类里面存在拷贝,只是仅仅只存在一份而已,并不是不在派生类里面了;当虚继承的派生类被当做基类继承时,虚基类指针也会被继承。

实际上,vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚基类表中记录了虚基类与本类的偏移地址;

通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。

在这里我们可以对比虚函数的实现原理:他们有相似之处,都利用了虚指针(均占用对象的存储空间)和虚表(均不占用对象的存储空间)。

  • 虚基类依旧存在继承类中,只占用存储空间;虚函数不占用存储空间。
  • 虚基类表存储的是虚基类相对直接继承类的偏移;而虚函数表存储的是虚函数地址。

接下来我们从内存布局看一下具体过程:
首先看普通继承的内存布局:

/* 普通继承(没有使用虚基类)*/// 基类A
class Base
{public:int ma;
};class Derive1 : public Base
{public:int mb;
};class Derive2 : public Base
{public:int mc;
};class Derive3 : public Derive1, public Derive2
{public:int md;
};

打开VS开发者命令行工具

进入工程目录下,输入下述命令,注意/EHsc不一定要写,根据项目是否开启C++异常检查而定,最后加上类名即可

我们得到Derive3的内存布局如下,我们发现 Derive3 拿到了其间接基类的两份拷贝:

其基类Derive1和Derive2的内存布局如下:

那么接下来我们定义Derive1和Derive2为虚继承,使得Base成为它们的虚基类。

查看Derive1内存布局如下:

我们看到了在Derive1内存的前4个字节中存储了vbptr虚基类指针,而虚基类的数据由之前的首部移至了尾部。

并且我们看到在vbtable虚基类表中,offset偏移量字段显示为8,表示虚基类数据相对Derive1类首部的偏移量(向下偏移量)

同样的,我们查看Derive2内存布局如下:

其发生的变化和Derive1的是相同的。

最后,我们查看一下 Derive3 的内存布局:

同样的,我们发现,虚基类Base的数据移至了Derive3内存布局的末尾,并且只存在一份数据,并不是之前的两份,因此,我们验证了虚基类解决了多重继承中的菱形继承问题。

实际上,vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚表(virtual table),虚表中记录了vbptr与本类的偏移地址;第二项是vbptr到共有基类元素之间的偏移量。

在这个例子中,虚表表明Derive3的间接基类Base的成员变量ma距离类Derive3开始处的位移为20,这样就找到了成员变量ma,而虚继承也不用像普通多继承那样维持着公共基类的两份同样的拷贝,节省了存储空间。


查看内存布局:

cl -d1reportSingleClassLayoutDerive 源.cpp
Derive为类名 后边为源.cpp

C++继承详解(三):抽象类和纯虚函数、多重继承与虚基类的底层实现原理详解相关推荐

  1. 抽象类(纯虚函数、虚函数)和虚基类(虚继承)

    C++多态 C++的多态包括静态多态和动态多态,静态多态包括函数重载和泛型编程,动态多态包括虚函数.静态多态实在编译期间就能确定,动态多态实直在程序运行时才能确定. 抽象类 虚函数 在默认情况下对函数 ...

  2. java源码系列:HashMap底层存储原理详解——4、技术本质-原理过程-算法-取模具体解决什么问题

    目录 简介 取模具体解决什么问题? 通过数组特性,推导ascii码计算出来的下标值,创建数组非常占用空间 取模,可保证下标,在HashMap默认创建下标之内 简介 上一篇文章,我们讲到 哈希算法.哈希 ...

  3. C++中虚继承产生的虚基类指针和虚基类表,虚函数产生的虚函数指针和虚函数表

    本博客主要通过查看类的内容的变化,深入探讨有关虚指针和虚表的问题. 一.虚继承产生的虚基类表指针和虚基类表 如下代码:写一个棱形继承,父类Base,子类Son1和Son2虚继承Base,又来一个类Gr ...

  4. Selenium 底层实现原理详解

    Selenium 简介 Selenium 是目前主流的用于Web应用程序测试的工具,可以直接运行在浏览器中,就像真正的用户在操作一样. Selenium 原理 Selenium工作的过程中有三个角色, ...

  5. C++编程进阶5(内联函数、如何降低编译成本、处理继承体系中同名不同参的成员函数、私有虚函数)

    十七.内联函数 在https://blog.csdn.net/Master_Cui/article/details/106391552中,已经简单的说过内联函数的作用. 函数体较小的内联函数经过编译后 ...

  6. MySQL底层执行原理详解

    一.MySQL的内部组件结构 大体来说,MySQL 可以分为 Server 层和存储引擎层两部分. 1.Server层 ​ 主要包括连接器.查询缓存.分析器.优化器.执行器等,涵盖 MySQL 的大多 ...

  7. SQL底层执行原理详解

    我们平时都是使用sql语句去查询数据,都是很直接的看到结果.那么对于sql底层执行的过程大家有了解吗? 一.MySQL的内部组件结构 大体来说,MySQL 可以分为 Server 层和存储引擎层两部分 ...

  8. python字典实现原理_Python字典底层实现原理详解

    在Python中,字典是通过散列表或说哈希表实现的.字典也被称为关联数组,还称为哈希数组等.也就是说,字典也是一个数组,但数组的索引是键经过哈希函数处理后得到的散列值.哈希函数的目的是使键均匀地分布在 ...

  9. python的底层实现,Python封装底层实现原理详解(通俗易懂)

    事实上,Python 封装特性的实现纯属"投机取巧",之所以类对象无法直接调用以双下划线开头命名的类属性和类方法,是因为其底层实现时,Python 偷偷改变了它们的名称. 前面章节 ...

  10. java源码系列:HashMap底层存储原理详解——5、技术本质-原理过程-算法-取模会带来一个什么问题?什么是哈希冲突?为什么要用链表?

    目录 取模会带来一个什么问题? 演示什么是哈希冲突(哈希碰撞)? 为什么要用链表? 其他--布隆过滤器 取模会带来一个什么问题? 好,那同学们这样他能达到一个目的,但是呢,它也会带来的一个问题,那它会 ...

最新文章

  1. javascript数据结构与算法---检索算法(二分查找法、计算重复次数)
  2. 《偷梁换柱》全世界最最简单对付SMSS。EXE病毒的方法,可能也是对付某类流氓程序的好方法...
  3. 对AI毫无了解?本文带你轻松了解AI
  4. Windows下cmd常用命令【5分钟掌握】
  5. Java中的classpath
  6. Android笔记 Application对象的使用-数据传递以及内存泄漏问题
  7. cudaMemcpy2D介绍
  8. LeetCode-208 Implement Trie (Prefix Tree)
  9. java编程规范每行代码窄字符,wiki/0xFE_编程规范.md at master · islibra/wiki · GitHub
  10. atitit.二维码生成总结java zxing
  11. Palindrome Number之Java实现
  12. 基于unity+vuforia的VR二级齿轮减速器动画分解
  13. MySQL+Navicat安装教程
  14. Access数据库对象包括哪六个?Access与 Excel 最重要的区别是什么?
  15. 服务器raid0系统坏了,服务器磁盘阵列raid1、raid0、raid5故障时的数据恢复思路和方法...
  16. 海马玩关联android,Android ADB连接海马玩模拟器
  17. Coggle专访系统之神与我同在:我的竞赛学习路线
  18. Cisco Packet Tracer 思科模拟器SSH配置
  19. 3D激光开源项目——BLAM安装使用过程的一些问题
  20. np.arange与np.linspace细微区别(数据溢出问题)

热门文章

  1. 概率论在实际生活的例子_生活中有趣的概率论例子
  2. 电脑蓝屏,问题:你的电脑未正确启动,按“重启”以重启你的电脑,有时这样可以解决问题,你还可以按“高级选项”,尝试使用其他选项修复你的电脑
  3. matlab编译后方交会,后方交会MATLAB程序实习报告.docx
  4. 使用 Autel MaxiFlash Elite 进行 GM J2534 编程
  5. 深度相机:结构光、TOF、双目相机
  6. CSS特效二:按钮动画效果
  7. 未曾读过刘备的人,不足以谈人生
  8. stc单片机c语言程序头文件(stc12c5a60s2.h,STC12C5A60S2单片机头文件
  9. android 获取收到短信验证码,Android自动获取短信验证码
  10. c语言互质欧拉函数,互质与欧拉函数