C++中的各种"虚"

  • 1. 菱形继承
    • 1.1 虚继承 && 虚基类
    • 1.2 虚基类指针(vbptr)&& 虚基类表(vbtable)
  • 2. 多态
    • 2.1 函数地址绑定时机(早/晚绑定)
    • 2.2 虚函数
    • 2.3 虚函数指针(vfptr)与虚函数表(vftable)
      • 2.3.1 多态的优点
    • 2.4 纯虚函数 && 抽象类
    • 2.5 虚析构 && 纯虚析构

C++中的一些重要概念都与“”相关,比如:

① 虚基类、虚继承、虚基类指针(vbptr)、虚基类表(vbtable);

② 虚函数、纯虚函数、虚函数指针(vfptr)、虚函数表(vftable)、抽象类、虚析构、纯虚析构。

这里对上面罗列出的概念做一个总结,争取把这些都一次讲清楚:

1. 菱形继承

由于C++支持多继承,即一个类可以继承自多个类,故有时候会存在菱形继承(又叫钻石继承)的情景,即两个子类继承同一个父类而又有子类同时继承这两个子类

菱形继承示意伪代码:

class CA{public:int m_A;
};class CB :public CA{};
class CC :public CA{};class CD :public CB,public CC{};

即CB和CC继承自CA类,而CD由继承自CB类和CC类。

菱形继承会产生一些问题:

  1. 当CD对象想要调用成员变量m_A的时候,如果不使用作用域加以区分调用哪个父类中的m_A时,会产生错误,即产生二义性的问题:

    CD *obj = new CD();obj->CB::m_A = 10;obj->CC::m_A = 20;//cout << obj->m_A << endl;       // 报错cout << obj->CB::m_A << endl;   // 成功执行,输出10cout << obj->CC::m_A << endl;    // 成功执行,输出20delete obj;
    
  2. 菱形继承导致同个成员变量的多次继承,造成CD对象中m_A变量的冗余(空间浪费)。

而虚继承技术是用于解决菱形继承上述问题的方法。

1.1 虚继承 && 虚基类

利用虚继承可以解决菱形继承的问题,在继承之前,加上关键字virtual

class CA{int m_A;
};class CB :virtual public CA{};
class CC :virtual public CA{};class CD :public CB,public CC{};

CB和CC虚继承自CA类,此时CA类就是虚基类

此时不论加不加作用域,CD访问m_A都是在访问同一片地址:

 CD *obj = new CD();obj->CB::m_A = 10;obj->CC::m_A = 20;cout << obj->m_A << endl;        // 成功执行,输出20cout << obj->CB::m_A << endl;    // 成功执行,输出20cout << obj->CC::m_A << endl;    // 成功执行,输出20delete obj;

这是因为CD类的对象从CB和CC中继承下来的对象不再是他们各自的m_A,而是一个指针 —— vbptr (virtual base pointer,虚基类指针)

1.2 虚基类指针(vbptr)&& 虚基类表(vbtable)

CD实例化对象中的 vbptr 会指向其 vbtable ( virtual base table,虚基类表 ), 而虚基类表中记录着vbptr 指向实际变量的偏移量(offset),通过 vbptr + offset 的方式可以访问到唯一的成员变量,从而不再产生歧义和空间浪费的问题。

【注意】C++ 创建一个子类对象时会调用父类的构造函数,那么会创建父类对象吗?

答曰:不会创建另外一个父类对象,只是初始化子类中属于父类的成员,父子类上同名的成员变量和函数可以通过作用域来指定。

创建一个对象的时候,发生了两件事情,一是分配对象所需的内存,二是调用构造函数进行初始化。子类对象包含从父类对象继承过来的成员,实现上来说,一般也是子类的内存区域中有一部分就是父类的内存区域。调用父类构造函数的时候,这块父类对象的内存区域就被初始化了。为了避免未初始化的问题,语法强制子类调用父类构造函数。

2. 多态

多态是C++面向对象的三大特性之一。

  • 多态可以分为两类:

    1. 静态多态: 函数重载 和 运算符重载属于静态多态,即复用函数名;
    2. 动态多态: 基于 派生类虚函数 实现运行时多态。
  • 静态多态和动态多态的区别:

    1. 静态多态:函数地址早绑定 —— 编译阶段就已经确定函数地址;
    2. 动态多态:函数地址晚绑定 —— 运行阶段才能确定函数的地址。

2.1 函数地址绑定时机(早/晚绑定)

通过下面的C++伪代码来理解什么是函数地址早/晚绑定

/* 动物类 */
class Animal {public:void speak(){ cout << "动物在说话" << endl; }
};/* 猫类 */
class Cat :public Animal{void speak(){ cout << "迪奥纳特调~" << endl; }
};/* 测试API */
void doSpeak(Animal &animal){   // 父类引用指向子类对象,animal.speak();
}/* 测试案例 */
void test01(){Cat cat;doSpeak(cat); //?问题:该行输出什么?
}

C++中允许父子之间的类型转换(不需要强制转换),在doSpeak()函数中参数是父类的引用,test01()函数中传入的是子类对象,这在语法上是没毛病的。可能会有的同学认为我们传入的参数是Cat类,理应调用Cat类的speak()函数,但实际上 test01()函数中,输出的结果是 "动物在说话",即调用的是父类Animal类的speak()函数

为什么会产生这样的现象?

原因就在于void doSpeak(Animal &animal)函数是地址早绑定的,即在编译时就已经确定doSpeak()内部speak()函数的调用地址是Animal类中的speak()函数,故此不论传入test01()函数的对象参数是继承自Animal类的猫类狗类还是别的什么类,最终的结果都将是调用父类Animal类的speak()函数。

如果想让猫说话,这个函数的地址就不能是早绑定的,需要在运行阶段进行绑定(晚绑定),通过派生类和虚函数实现,即运行时多态。

2.2 虚函数

在基类Animal类的void speak()函数前加上virtual关键字,使其成为虚函数:

virtual void speak(){ cout << "动物在说话" << endl; }

继承自含有虚函数的基类后,子类重写父类中的虚函数,就可以实现地址晚绑定。

此时再次运行test01()函数后,输出的结果是 "迪奥纳特调~"。特点就是会根据传入的对象不同,执行相应类的函数,总结如下:

  • 动态多态满足条件

    1. 有继承关系
    2. 子类重写父类中的虚函数
  • 动态多态的使用

    1. 父类的引用指向子类传入对象

      /* 假设Cat类继承自Animal类且重写了Animal类的虚函数 */
      // 参数为父类引用
      void doSpeak(Animal &animal){ animal.speak();
      }int main(){Cat cat;// 传入子类对象doSpeak(cat);  // 执行Cat类中的speak()函数            return 0;
      }
      
    2. 父类的指针指向子类传入对象

      /* 假设Cat类继承自Animal类且重写了Animal类的虚函数 */
      Animal *obj = new Cat();
      obj.speak();    // 执行Cat类中的speak()函数
      

2.3 虚函数指针(vfptr)与虚函数表(vftable)

当我们在给Animaal类的speak()函数加上virtual关键字之前,实例化一个Animal对象obj并用sizeof(obj),可以看到,大小为1字节。

这是因为C++类中只有非静态成员变量是存储在对象中的,其他的静态成员变量、静态成员函数、成员函数都由所有对象共享类中的一份实例,而为了区分空对象和NULL,C++中规定空对象的大小为1个字节。

但在给Animaal类的speak()函数加上virtual关键字之后,再使用sizeof()函数查看该对象大小,可以看到结果是4字节(32位OS)或8字节(64位OS),具体视操作系统位数而定。

这是为什么呢?

因为使用虚函数后,在对象的地址空间中存储了一个指针,即 vfptr(virtual function pointer,虚函数指针);

vfptr 指针会指向一张表,即 vftable(virtual function table,虚函数表),该表内部会记录虚函数的地址。

当子类即Cat类没有重写父类即Animal类中的虚函数时,子类会继承父类中的vfptr和vftable,如下示意图:

当子类即Cat类重写父类即Animal类中的虚函数之后,子类中 vftable 内部会替换成子类虚函数的地址(父类中的vftable没有改变),如下示意图:

在满足继承与虚函数的重写后,当父类的指针或者引用指向子类对象时,就会发生多态,具体执行子类还是父类中的函数由子类中 vfptr 查 vftable 决定。

2.3.1 多态的优点

使用多态有如下优点:

  1. 代码组织结构清晰,可读性强
  2. 利于项目的前期开发和后期的拓展及维护

使用多态符合大型软件工程开发设计原则中的开闭原则,即对修改(源码)关闭,对添加(插件/功能/模块)开放。

举一个例子,比如我们要实现一个二元运算计算器,在没有掌握多态之前,通常会使用流程控制语句如if…else或goto、switch等 来对参数中的操作符做判断再执行相应运算;

这样写虽然简洁快速,但是对于大型的项目来说,如果需要给该计算器添加新的运算方式如求n次幂时,我们需要去源码的流程控制语句中添加一个判断和执行,这样就违背了开闭原则,不利于项目后期的维护与拓展;

如果使用多态,那么可以设计一个基类,该基类中包含两个操作数做成员变量,以及一个虚函数;

这样在需要后续扩展每种运算功能时,只需一个继承自该基类的子类,并重写基类中的虚函数为具体的计算函数即可(不需要修改源码,而是添加子类),即一个子类对应于一种运算。在需要进行运算时只需要将基类的指针或引用指向子类的对象,并调用该指针或引用的相应函数即可实现多态。

2.4 纯虚函数 && 抽象类

在多态中,通常父类中的虚函数的实现是没有意义的,主要都是调用子类重写父类的虚函数,因此,可以将虚函数改为纯虚函数

纯虚函数语法

virtual 返回值类型 函数名 (参数列表) = 0;
  • 类中有了纯虚函数,这个类也成为抽象类

  • 抽象类特点:

    1. 无法实例化对象
    2. 子类必须重写抽象类中的纯虚函数,否则也属于抽象类

2.5 虚析构 && 纯虚析构

使用多态时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码

解决方式:将父类中的膝盖函数改为虚析构纯虚析构

C++中构造函数的调用顺序由父类到子类依次构造,析构函数相反。

  • 虚析构和纯虚析构共性:

    1. 可以解决父类指针释放子类对象
    2. 都需要有具体的函数实现
  • 虚析构和纯虚析构区别:

    • 如果是纯虚析构,该类属于抽象类,无法实例化对象
  • 虚析构语法:

    virtual ~类名(){}
    
  • 纯虚析构语法:

    /* 类内声明 */
    virtual ~类名() = 0;/* 类外实现 */
    类名::类名(){}
    

【注意】纯虚析构和纯虚函数不同,纯虚函数不需要实现,但纯虚析构仍需要实现。

C++中的各种“虚“-- 虚函数、纯虚函数、虚继承、虚基类、虚析构、纯虚析构、抽象类讲解相关推荐

  1. 转载:(C++)浅谈多态基类析构函数声明为虚函数

    原博文:https://www.cnblogs.com/AndyJee/p/4575810.html 主要内容: 1.C++类继承中的构造函数和析构函数 2.C++多态性中的静态绑定和动态绑定 3.C ...

  2. (C++)浅谈多态基类析构函数声明为虚函数

    主要内容: 1.C++类继承中的构造函数和析构函数 2.C++多态性中的静态绑定和动态绑定 3.C++多态性中析构函数声明为虚函数 1.C++类继承中的构造函数和析构函数 在C++的类继承中, 建立对 ...

  3. 为什么基类的析构函数是虚函数?

    1.第一段代码 #include<iostream> using namespace std; class ClxBase{ public:ClxBase() {};~ClxBase() ...

  4. C++ day22 继承(二)基类指针数组通过虚方法实现智能的多态

    继承一共有三种: 公有继承 私有继承 保护继承 文章目录 公有继承 基类和派生类的关系 is-a(用公有继承表示"是一种"的关系) has-a uses-a is-like-a i ...

  5. 有虚继承的类其有几个虚继承的基类则该类就有几个虚表

    对于下面的测试代码(64位系统):C类继承了A  B 均为虚类,所以在结果中sizeof(C) 结果为32 #include<iostream> using namespace std; ...

  6. C++虚继承(七) --- 虚继承对基类构造函数调用顺序的影响

    继承作为面向对象编程的一种基本特征,其使用频率非常高.而继承包含了虚拟继承和普通继承,在可见性上分为public.protected.private.可见性继承比较简单,而虚拟继承对学习c++的难度较 ...

  7. c++中继承 掩藏基类成员,访问父类对比c#

    这方面看一个代码就可以 代码 //testclass.h #include<stdio.h> classbaseclass{ int a; public: voidcprint(); }; ...

  8. C++之继承探究(十):抽象基类与纯虚函数

    前文:C++之继承探究(九):多态的代价 抽象基类与纯虚函数   ♠\spadesuit♠ 纯虚函数:虚函数只有声明,函数体=0,就是一个纯虚函数,纯虚函数没有函数体,不需要实现.在子类里实现纯虚函数 ...

  9. 38.【C++ 虚函数 纯虚函数 虚基类 (最全详解)】

    虚函数.虚基类.纯虚函数 (一).虚函数 1.什么是虚函数: 2.虚函数的格式: 3.关于虚函数的注意事项: 4.虚函数的作用: 5.虚函数访问格式 6.虚函数的各种疑难杂症 [当指针是基类.但虚函数 ...

  10. C++ 虚拟继承与虚基类

    1.多重继承带来的问题 C++ 虚拟继承一般发生在多重继承的情况下.C++ 允许一个类有多个父类,这样就形成多重继承.多重继承使得派生类与基类的关系变得更为复杂,其中一个容易出现问题是某个基类沿着不同 ...

最新文章

  1. DevExpress学习之DevExpress.XtraGrid.Columns.GridColumn
  2. HBase 6、用Phoenix Java api操作HBase
  3. KubeVela v1.2 发布:你要的图形化操作控制台 VelaUX 终于来了
  4. RocketMQ的架构设计详解
  5. 二进制数表示形式:原码、反码与补码
  6. 计算工资底薪加提成的java_月工资如何计算我是做服务行业,底薪2200全勤100加提成5 - 找法网(findlaw.cn)...
  7. 一文快速掌握 Git 用法(Git介绍及使用流程)
  8. Volley源码解析(三)
  9. RocketMQ消费端消息回退(消费重试)机制源码解析
  10. android 项目交接文档,产品交付规范文档
  11. SVN 忽略不需要提交的文件
  12. RSA/数字证书/签名原理详解
  13. 纯Vue实现网页日常任务清单小功能(数据存储在浏览器)
  14. 如何把电脑视频转换成MP4格式?视频转换用嗨格式
  15. 你未看此花时,此花与汝同归于寂;你来看此花时,则此花颜色一时明白起来。
  16. 【干货】Dialog的高冷用法
  17. iOS8扩展插件开发配置
  18. MySQL Cluster导入数据表时报错:Got error 708 'No more attribute metadata records (increas
  19. vue h5点击跳转主流手机应用商店app下载页
  20. ESP8266人体感应项目

热门文章

  1. [附源码]计算机毕业设计springboot在线票务系统
  2. cmd命令大全,cmd打开计算器,cmd打开记事本...
  3. 渲染用计算机功耗,用了功率计,才发现原来电脑功耗其实并不大,买大功率电源是否浪费?...
  4. Colosal-AI复现流程
  5. cocos2dx基础篇(19)——音乐音效SimpleAudioEngine
  6. 关于 Unity 音乐音效管理
  7. pdf怎么转换成jpg或png格式的图片?
  8. bat脚本一键批量修改文件名
  9. stm32之配置串口中断服务函数
  10. 小程序访问,提示网络不给力,请稍后再试