2021-09-15 C++ 继承和多态(虚函数,纯虚函数,虚继承)

2023-11-06

C++继承和多态(虚函数、纯虚函数、虚继承)

 

一:继承

继承的概念:为了代码的复用,保留基类的原始结构,并添加派生类的新成员。

继承的本质:代码复用

我们用下图解释下:

 

那么我们这里就可以提出几个问题了:

①:进程的方式有哪些呢?

这里有三种继承方式:

  • public:任意位置可以访问
  • protected:只允许本类类中以及子类类中访问
  • private:只允许本类类中访问

②:派生类继承了基类的什么?

  • 所有成员变量,包括static静态成员变量
  • 成员方法,除构造和析构以外的所有方法
  • 作用于也继承了,但是友元关系没有继承

③:派生类生成的对象的内存布局是什么样的?

派生类对象构造时,基类数据在前,派生类自身数据在后。

如下图所示:(B继承了A)

④:派生类对象的构造析构顺序

(1):派生类对象的构造顺序:

  • 系统调用基类的构造(没有指明构造方式,则按默认的走)
  • 系统调用派生类的构造

(2):派生类对象的析构顺序:

  • 系统调用派生类的析构
  • 系统调用基类的析构

 

那么基类中不同访问限定符下的成员 以不同的继承方式 继承后在派生类中的访问限定是什么样的呢?

核心思想:继承后的权限不会大于继承方式的权限

我们这里有一张图可以概括:

我们可以用以下代码来验证:

测试代码如下:


   
   
  1. #include <iostream>
  2. class Base
  3. {
  4. public:
  5. Base( int a = 10, int b = 20, int c = 30):ma(a), mb(b), mc(c){}
  6. public:
  7. int ma;
  8. protected:
  9. int mb;
  10. private:
  11. int mc;
  12. };
  13. class Derived : private Base
  14. {
  15. public:
  16. void Show()
  17. {
  18. std:: cout << ma << std:: endl;
  19. std:: cout << mb << std:: endl;
  20. //std::cout << mc << std::endl;
  21. }
  22. };
  23. class Derived2 : public Derived
  24. {
  25. public:
  26. void Show()
  27. {
  28. std:: cout << ma << std:: endl;
  29. //std::cout << mb << std::endl;
  30. //std::cout << mc << std::endl;
  31. }
  32. };
  33. int main()
  34. {
  35. Derived d;
  36. Derived2 d2;
  37. d.Show();
  38. d2.Show();
  39. std:: cout << sizeof(Derived) << std:: endl;
  40. return 0;
  41. }

 

 

二:多态

多态的概念:多态可以使我们以相同的方式处理不同类型的对象,其实用一句话来说,就是允许将子类类型的指针赋值给父类类型的指针。

多态的本质:接口复用(一种接口,不同形态)

 

多态性在C++中是通过虚函数实现的。

虚函数:就是父类允许被其子类重新定义的成员函数,而子类重新定义父类函数的做法,称为“覆盖”,或者称为“重写”。

子类重写父类中虚函数时,即使没有virtual声明,该重载函数也是虚函数。

 

我们可以将多态分为3类:

  • 静多态:在编译阶段已经确定函数的入口地址,例如函数重载,模板等
  • 动多态:在运行阶段时才确定函数的入口地址,例如虚函数调用机制
  • 宏多态:例如宏函数,在预编译阶段已经进行了替换

 

动多态:

  1. 在编译期间生成虚函数表,表中保存函数入口地址
  2. 放在只读数据段.rodata
  3. 一个类共用一个虚表,而不是一个对象

而类中是不存在虚函数表的,主要由于数据冗余,太大了,所以都保存一个指针vfptr,让这个指针指向这个虚函数表即可。

那这个虚函数表长什么样子呢?

 

注意一点:

  • 如果基类中有一个成员函数是虚函数,那么派生类中与其同名同参的函数默认会变成虚函数,这是一个覆盖的关系。
  • 编译期间,派生类会生成自己的虚函数表,这时两个虚函数表会进行合并,同名同参的虚函函数覆盖了基类中同名同参的虚函数。

 

下面,我们将介绍一下什么是覆盖:

首先我们需要知道,类与类之间的关系有三种,分别为:

  • 组合 :a part of   是一个 has_a的关系(有一个),例如:A是B的一部分,则不允许B继承A的功能,而是要用A和其他东西组合成B,他们之间就是has_a的关系,现实中则就是眼睛,鼻子和脑袋的关系。
  • 继承 :a kind of 是一个is_a的关系(是一个),例如:若B是A的一种,则允许B继承A的功能,他们之间就是is_a的关系,现实中就是香蕉和水果的关系。
  • 代理 :限制底层的接口,提供新的接口。
  • 这里需要注意的是,用private方式继承,是一个has_a的关系,不是is_a的关系,是有一个的关系,不是是一个的关系。

 

而同名函数之间也有三种关系,分别为:

  • 重载 overload 重载三要素:同名,不同参,同作用域
  • 隐藏 overhide 继承时,派生类中同名的函数隐藏了继承来的同名方法,继承来的函数存在,但是看不到
  • 覆盖 override 继承时,派生类中同名的虚函数覆盖了继承来的同名方法,继承来的虚函数不存在,直接被覆盖掉了。

 

内存分布:

如果基类有虚函数,而派生类中也有虚函数,则如下图:

虚函数表和类是一对一的,这个vfptr指向的是派生类对象的虚表。

 

如果这里有这6个函数,那么哪些可以成为虚函数呢?

  1. 普通函数×(遵守__cdecall调用约定,不依赖对象调用)
  2. 构造函数×(虽然遵守__thiscall调用约定,但是手动调用不了)
  3. 析构函数√(遵守__thiscall调用约定,且可以手动调用)
  4. static修饰的成员方法×(遵守__cdecall调用约定,不依赖对象调用)
  5. inline函数×(inline函数无法取地址,它直接在调用点直接展开)
  6. 普通的成员函数√(遵守__thiscall调用约定,且可以手动调用)

首先我们得知清楚道成为虚函数的条件:

  • 能取地址(排除5)
  • 依赖对象调用(排除1,2,4)

并且如果在构造函数以及析构函数内调用虚函数,那么只会是一个静态绑定,因为这时依赖调用的对象已经不完整了。

注意:虚函数指针的写入时机,是在构造函数第一行代码之前。

 

重点:如果有基类的指针指向了派生类的对象,那么基类就要有虚析构。

首先我们需要了解的是动多态的发生时机:

  • 调用的对象需要完整(这也是为什么在构造函数以及析构函数中调用虚函数,只会触发静多态的原因)
  • 指针调用的是虚函数(要有virtual关键字标识)

这时候我们再来看一看原因:如果派生类申请了内存空间,并在其析构函数中进行了释放,假设由于基类中采用的是非虚析构函数,那么当基类的指针指向了派生类的对象后,当delete释放内存的时候,首先会调用析构函数,但是因为基类的析构函数并不是虚析构,只是普通析构函数,所以只会触发静态绑定(静多态),不会触发动态绑定(动多态),因此调用的是基类的析构函数,而不是派生类的析构函数,那么申请的空间就会得不到释放从而造成内存泄漏,所以,为了防止这种情况的发生,我们需要将基类中的析构函数写成虚析构。

 

注意:如果基类没写虚函数,而派生类写了虚函数,那么当基类指针指向派生类后,delete pb就会崩溃

例如下面代码:


   
   
  1. #include <iostream>
  2. class A
  3. {
  4. public:
  5. A( int a) :ma(a)
  6. {
  7. std:: cout << "A::A(int)" << std:: endl;
  8. }
  9. void Show()
  10. {
  11. std:: cout << "A::ma:" << ma << std:: endl;
  12. }
  13. ~A()
  14. {
  15. std:: cout << "A::~A()" << std:: endl;
  16. }
  17. protected:
  18. int ma;
  19. };
  20. class B : public A
  21. {
  22. public:
  23. B( int b) :A(b),mb(b)
  24. {
  25. std:: cout << "B::B()" << std:: endl;
  26. }
  27. virtual void Show()
  28. {
  29. std:: cout << "B::mb:" << mb << std:: endl;
  30. }
  31. ~B()
  32. {
  33. std:: cout << "B::~B()" << std:: endl;
  34. }
  35. private:
  36. int mb;
  37. };
  38. int main()
  39. {
  40. A* pa = new B( 10);
  41. pa->Show(); //class Base*
  42. //delete (A*)((char*)pa -4);
  43. delete pa;
  44. return 0;
  45. }

 

我们画个图分析一下为什么会崩溃:

原因:当基类指针指向派生类的时候,因为new是从0x100开辟的,但是由于基类指针赋值的是基类构造时的地址0x200,所以当开辟地址与释放地址不一致时,则会造成崩溃。

 

我们可以将基类中析构函数变成虚析构函数,那么基类就会有一个虚函数指针,当两者合并时,就不会产生开辟地址与释放地址不一致的问题了,因为此时内存布局如下:

这时内存开辟地址和释放地址都为0x100,则不会造成崩溃。

 

 

三:纯虚函数

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

纯虚函数的两个特点:

  • 拥有纯虚函数的类叫做抽象类
  • 抽象类不能实例化对象

例如,动物这个类,可以派生出狗和猫这两个类,但是由于它俩都有发出叫声这个方法,那么我们想通过一个函数,通过传入不同的基类指针,实现发出不同的叫声。

代码如下:


   
   
  1. #include <iostream>
  2. #include <string>
  3. class Animal//抽象类
  4. {
  5. public:
  6. Animal( std:: string name) :mname(name)
  7. {
  8. std:: cout << "Animal::Animal()" << std:: endl;
  9. }
  10. virtual void Bark() = 0; //纯虚函数
  11. virtual ~Animal()
  12. {
  13. std:: cout << "Animal::~Animal()" << std:: endl;
  14. }
  15. protected:
  16. std:: string mname;
  17. };
  18. class Dog : public Animal
  19. {
  20. public:
  21. Dog( std:: string name) :Animal(name)
  22. {
  23. std:: cout << "Dog::Dog()" << std:: endl;
  24. }
  25. void Bark()
  26. {
  27. std:: cout << mname << " wang wang wang!" << std:: endl;
  28. }
  29. ~Dog()
  30. {
  31. std:: cout << "Dog::~Dog()" << std:: endl;
  32. }
  33. };
  34. class Cat : public Animal
  35. {
  36. public:
  37. Cat( std:: string name) :Animal(name)
  38. {
  39. std:: cout << "Cat::Cat()" << std:: endl;
  40. }
  41. void Bark()
  42. {
  43. std:: cout << mname << " miao miao miao!" << std:: endl;
  44. }
  45. ~Cat()
  46. {
  47. std:: cout << "Cat::~Cat()" << std:: endl;
  48. }
  49. };
  50. void ShowBark(Animal* pa)
  51. {
  52. pa->Bark();
  53. }
  54. int main()
  55. {
  56. Cat* pc = new Cat( "cat");
  57. Dog* pd = new Dog( "dog");
  58. ShowBark(pc);
  59. ShowBark(pd);
  60. delete pc;
  61. delete pd;
  62. return 0;
  63. }

 

我们运行一下,看一看结果:

我们可以看到,通过一个函数Bark写成纯虚函数virtual void Bark() = 0;,这时派生类对象就可以自行定义这个函数Bark,而我们提供的普通函数ShowBark,通过传入不同的基类指针,调用其纯虚函数Bark,则可以发出不同的叫声。

 

 

四:虚继承

继承可以分为单继承和多继承,那么就会出现这样一种巧妙地结果,菱形继承:

菱形继承:我们很清楚的可以看到,它存在内存重复的问题,所以我们引进了虚继承。

我们可以对内存重复的间接基类做特殊处理,在B和C继承时,加上关键字virtual,这时就形成了虚继承(class B : virtual public A)

 

那么A就叫做虚基类,在内存的最下方开辟一块内存,用来存放A,在其原本位置置放一个虚基类指针vbptr,通过这个指针可以找到这块内存,因为内存在开辟期间不能赋值指向,所以只能通过偏移来找到。

 

如果不加virtual关键字,那么构造顺序则是:ABACD

但是给BC加上virtual,那么构造顺序则是:ABCD

重点:构造时,虚基类的构造顺序最高

重点:而内存分布的时候,是非虚基类顺序>虚基类顺序,但是需要注意的是,虚基类内存向下放的时候,是按照虚继承的顺序,先看见谁,先放谁

 

完整代码如下(下面会根据情况进行简单修改,并进行测试,以验证以上结论):


   
   
  1. #include <iostream>
  2. class A
  3. {
  4. public:
  5. A( int a) :ma(a)
  6. {
  7. std:: cout << "A" << std:: endl;
  8. }
  9. ~A()
  10. {
  11. std:: cout << "~A" << std:: endl;
  12. }
  13. public:
  14. int ma;
  15. };
  16. class B : virtual public A
  17. {
  18. public:
  19. B( int b) :mb(b),A(b)
  20. {
  21. std:: cout << "B" << std:: endl;
  22. }
  23. ~B()
  24. {
  25. std:: cout << "~B" << std:: endl;
  26. }
  27. public:
  28. int mb;
  29. };
  30. class C : virtual public A
  31. {
  32. public:
  33. C( int c) :mc(c),A(c)
  34. {
  35. std:: cout << "C" << std:: endl;
  36. }
  37. ~C()
  38. {
  39. std:: cout << "~C" << std:: endl;
  40. }
  41. public:
  42. int mc;
  43. };
  44. class E
  45. {
  46. public:
  47. E( int e) :me(e)
  48. {
  49. std:: cout << "E" << std:: endl;
  50. }
  51. ~E()
  52. {
  53. std:: cout << "~E" << std:: endl;
  54. }
  55. public:
  56. int me;
  57. };
  58. class D : public B, virtual public E, public C
  59. {
  60. public:
  61. D( int d) :md(d), B(d), C(d), E(d), A(d)
  62. {
  63. std:: cout << "D" << std:: endl;
  64. }
  65. ~D()
  66. {
  67. std:: cout << "~D" << std:: endl;
  68. }
  69. public:
  70. int md;
  71. };
  72. int main()
  73. {
  74. D d(10);
  75. //d.ma = 10;
  76. return 0;
  77. }

 

我们修改类D的继承方式,以验证以上结论:

①:

class B : virtual public A

class C : virtual public A

class D : public B ,virtual public E, public C

运行结果:

内存分布:

可以看到,构造顺序是虚基类的最高。

 

②:

class B : virtual public A

class C : virtual public A

class D :virtual public E, virtual public B, public C

运行结果:

内存分布:

我们可以看到,内存中首先是非虚基类的C,但是按照虚继承顺序,接下来是E,最后才是A以及B。

 

查看内存命令:在开发人员命令提示中输入 cl -d1reportSingleClassLayoutD 测试1.cpp   (D为类名)

 

我们可以得到rfptr与rbptr的区别:

  • rfptr的偏移是总体作用域减当前
  • rbptr的偏移是当前作用域减当前

 

建议:所以说,有虚继承的话,一般不使用动态开辟内存,一般使用栈开辟内存,因为虚继承总会将基类放到最下面,导致内存开辟的地址和释放的地址不一致,导致崩溃。

 

至此,C++继承、多态、虚函数、纯虚函数、虚继承基本了解完毕。

本文转载如下:
————————————————
版权声明:本文为CSDN博主「WuDi_Quan」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/IT_Quanwudi/article/details/88081934

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

2021-09-15 C++ 继承和多态(虚函数,纯虚函数,虚继承) 的相关文章

随机推荐

  • 以太网ARP测试实验

    1 1 ARP测试整体框架 当上位机发送ARP请求时 FPGA返回ARP应答数据 当按下FPGA的触摸按键时 FPGA发送ARP请求 上位机返回ARP应答数据 PLL时钟对eth rxc的输入时钟进行相位调整 GMII TO RGMI 模块
  • uniapp微信小程序订阅消息发送服务通知

    版权声明 本文为博主原创文章 遵循 CC 4 0 BY SA 版权协议 转载请附上原文出处链接和本声明 本文链接 https blog csdn net qq 44718932 article details 126130702 uniap
  • 爬虫发送带headers报错内容为ValueError: too many values to unpack (expected 2)

    这个是我报错的代码 author li lee import requests 变量url接收访问地址 url http www baidu com 加headers 将浏览器的用户代理 写到headers中 用于重置User Agent
  • 《C++ Primer Plus 第六版》编程练习参考答案(第四章)

    第四章 第一题 include
  • NativeWindow 妙用,截取windows消息

    namespace System Windows Forms using System using System ComponentModel using System Drawing using System Drawing Design
  • linux设置VT模式,android – 如何在Linux上的BIOS和KVM模块中启用VT-x?

    KVM Installation I referred the instructions from Ubuntu community documentation page to get KVM installed To see if you
  • C#使用FFmpeg的总结

    上篇文章提到FFmpeg解决项目中视频和语音问题 说道C 和FFmpeg不得不提的2个类库 1 Xabe FFmpeg 简单查看了下源码和demo 发现基于ffmpeg exe的命令行参数进行处理 2 FFmpeg AutoGen 把C语言
  • sqlserver日期格式转换yyyymmdd_8个案例,玩转时间合并转换运算和提取,3个函数就够了...

    大家好 上一篇文章分享了关于日期时间的提取函数和判定 讲解了如何对日期是否是周末进行判定 今天继续分享关于日期时间方面的合并 提取方面的剩余内容 以及如何根据身份证号求算年龄 如何根据入职时间计算员工工龄两个案例 那么就让我们开始吧 操作一
  • jmeter errstr :“unsupported field type for multipart.FileHeader“

    在使用jmeter测试接口的时候 提示errstr unsupported field type for multipart FileHeader 如图所示 这是因为我们 在HTTP信息头管理加content type参数有问题 直接在HT
  • VS社区版许可证过期更新

    VS社区版许可证过期更新 VS社区版是免费使用的 然而会有许可证过期的问题 这时候就需要登录 再进行更新许可证 在登录时卡顿了一天还没有解决问题 我们无法下载许可证 请检查你的网络连接或代理设置 查找方案 有一个方案比较普遍 1 在VS安装
  • brk(), sbrk() 用法详解

    贴上原文地址 好不容易找到了 brk sbrk 改变数据段长度 brk sbrk 的声明如下 include
  • 机器学习实验 - MeanShift聚类

    目录 一 报告摘要 1 1 实验要求 1 2 实验思路 1 3 实验结论 二 实验内容 2 1 方法介绍 2 2 实验细节 2 2 1 实验环境 2 2 2 实验过程 2 2 3 实验与理论内容的不同点 2 3 实验数据介绍 2 4 评价指
  • Spring5 框架 详解 (一) ---- IOC控制反转

    Spring5 框架 spring 框架概述 IOC 容器 1 什么是 IOC 2 IOC底层原理 IOC 过程 IOC 接口 IOC 操作 Bean 管理 IOC 操作 Bean 管理 xml 注入其他类型属性 IOC 操作 Bean 管
  • PHP底层工作原理

    原文地址 http www cnblogs com phphuaibei archive 2011 09 13 2174927 html 最近搭建服务器 突然感觉lamp之间到底是怎么工作的 或者是怎么联系起来 平时只是写程序 重来没有思考
  • Java中数据类型详解

    文章目录 一 数据类型的作用 二 两种数据类型 1 基本数据类型 1 第一类 整数型 2 第二类 浮点型 3 第三类 布尔型 4 第四类 字符型 2 引用数据类型 1 类 2 数组 3 接口 三 基本数据类型之间的转化 一 数据类型的作用
  • Vue 项目如何实现一个全局菜单搜索框

    个人主页 山山而川 xyj 作者简介 前端领域新星创作者 专注于前端各领域技术 共同学习共同进步 一起加油 系列专栏 Vue 系列 学习格言 与其临渊羡鱼 不如退而结网 目录 前言 一 过滤路由 二 搜索框展示路由 三 雏形出现但有缺陷 四
  • 九宫格选择照片

    一 Adapter public class RecyclerImageAdapter extends RecyclerView Adapter
  • 使用IDEA创建JavaWeb项目

    由于看的视频教程比较老 且开发工具为Eclipse 本人已习惯使用IDEA 在此记录一下创建JavaWeb项目过程 创建一个项目 左侧选择 Java Enterprise 右侧选择 Web Application 这里我输入的项目名字为 F
  • 公开课精华

    本文章总结于大疆前技术总监 目前在卡内基梅隆大学读博的杨硕博士在深蓝学院的关于机器人的带约束轨迹规划的公开课演讲内容 全文约5000字 笔者不是机器人领域的 因此特地去了解了一下杨硕博士 深感佩服 不仅是他的履历 更多的是他关于学术上的至臻
  • 2021-09-15 C++ 继承和多态(虚函数,纯虚函数,虚继承)

    C 继承和多态 虚函数 纯虚函数 虚继承 一 继承 继承的概念 为了代码的复用 保留基类的原始结构 并添加派生类的新成员 继承的本质 代码复用 我们用下图解释下 那么我们这里就可以提出几个问题了 进程的方式有哪些呢 这里有三种继承方式 pu