【多态】有关多继承和菱形继承的多态

图片名称

博主首页: 有趣的中国人

专栏首页: C++进阶

其它专栏: C++初阶 | 初阶数据结构 | Linux

博主会持续更新

    本篇文章主要讲解 多继承和菱形继承的多态 的相关内容

    文章目录

    • 1. 回顾多态底层
    • 2. 抽象类
      • 2.1 概念
      • 2.2 接口继承和实现继承
    • 3. 虚表所在的内存区域
    • 4. 多继承中的虚函数表
      • 4.1 内存分布
    • 5. 菱形继承和菱形虚拟继承的虚表
      • 5.1 菱形继承
      • 5.2 菱形虚拟继承
    • 6. 关于继承和多态相关题目


      1. 回顾多态底层


      上一篇文章我讲过关于多态底层:

      • 首先编译器会在编译阶段检查语法的时候检查是否满足多态的两个条件:
      •  1. 是否是父类的指针或者引用调用的虚函数;
        
      •  2. 虚函数是否构成重写;
        
      • 如果满足,那就构成多态:如果指针或者引用是指向父类,那就在运行阶段去父类的虚函数表中寻找对应的虚函数;
      • 如果指针或者引用是指向子类中的父类(切片操作),那就在运行阶段去子类的虚函数表中寻找对应的虚函数;
      • 当然如果不满足多态,就会在编译阶段编译器根据调用者的类型决定去调用哪个函数。

      我们可以通过汇编代码查看一下:

      源代码(满足多态):

      class Person {
      public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
      };
      class Student : public Person {
      public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
      };
      // void Func(Person p) 去掉引用不满足多态
      void Func(Person& p)
      {p.BuyTicket();
      }
      int main()
      {Person Mike;Func(Mike);Student Johnson;Func(Johnson);return 0;
      }
      

      汇编代码:

      在这里插入图片描述
      当不满足多态时:
      在这里插入图片描述

      可以反思以下为什么多态一定要满足这两个条件呢?

      • 首先多态是要求类似类型的对象调用相同的函数可能会有不同的不同的结果,那么我们必须要完成函数重写来满足这个条件;
      • 其次为什么必须要是父类的指针或者引用来调用虚函数呢?因为需要完成切片的操作,如果父类的指针指向子类,那么指针就会指向子类中的父类部分然后在运行阶段通过虚函数表找到对应的虚函数并调用。

        2. 抽象类

        2.1 概念

        在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

        class Car
        {
        public:// 纯虚函数virtual void Drive() = 0;
        };
        class Benz :public Car
        {
        public:virtual void Drive(){cout << "Benz-舒适" << endl;}
        };
        class BMW :public Car
        {
        public:virtual void Drive(){cout << "BMW-操控" << endl;}
        };
        void Test()
        {Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();
        }

        2.2 接口继承和实现继承


        普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。


          3. 虚表所在的内存区域


          思考一下虚表在哪片内存区域呢?
          • A. 栈 B. 堆 C. 数据段(静态区) D. 代码段(常量区)

          我们可以写个代码判断一下:

          class Base
          {
          public:virtual void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;}
          private:int _b = 1;
          };
          class Derive : public Base
          {
          public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
          private:int _d = 2;
          };typedef void (*VFPTR)();int main()
          {int a = 10; // 栈static int i = 0; // 静态区int* ptr = new int[10];// 堆const char* str = "hello world";// 常量区cout << "a:栈" << &a << endl;cout << "i:静态区" << &i << endl;cout << "ptr:堆" << ptr << endl;cout << "str:常量区" << &str << endl;Base b;int* p = (int*)(&b);cout << "虚函数表地址:" << p << endl;return 0;
          }
          

          在VS下运行结果:
          在这里插入图片描述

          在g++下运行结果:
          在这里插入图片描述

          很明显虚函数表的地址和常量区的地址相差最小,因此虚函数表也是存在常量区


            4. 多继承中的虚函数表


            首先看一下这段代码,就算一下sizeof(d)

            class Base1 {
            public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
            private:int b1;
            };class Base2 {
            public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
            private:int b2;
            };class Derive : public Base1, public Base2 {
            public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
            private:int d1;
            };int main()
            {Derive d;cout << sizeof(d) << endl;return 0;
            }
            

            这里结果是20

            4.1 内存分布

            调试看一下内存分布:
            在这里插入图片描述

            可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

            我们可以根据调试推断一下d的内存划分:

            在这里插入图片描述
            那怎么证明呢?可以写个代码(注意main函数)看一下:

            class Base1 {
            public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
            private:int b1;
            };class Base2 {
            public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
            private:int b2;
            };class Derive : public Base1, public Base2 {
            public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
            private:int d1;
            };typedef void(*VFPTR) ();
            void PrintVTable(VFPTR vTable[])
            {cout << " 虚表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%p,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
            }
            int main()
            {Derive d;VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);PrintVTable(vTableb1);// 这个代码在此处是可以的,但是如果出现内存对齐就不行了//VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));//PrintVTable(vTableb2);// 这里可以用切片的方法直接找到d中Base2的地址,就不会考虑到内存对齐的复杂问题:Base2* ptr = &d;VFPTR* vTableb3 = (VFPTR*)(*(int*)ptr);PrintVTable(vTableb3);return 0;
            }
            

            运行结果如下:
            在这里插入图片描述
            但是多继承后,虚表中重写的func1的地址不一样,为什么呢?

            其实这里是VS编译器做的一层封装,这不重要。嗯。。。。。实际上调用的是同一个函数。


              5. 菱形继承和菱形虚拟继承的虚表

              5.1 菱形继承


              看一下这段菱形继承的代码:

              class A
              {
              public:virtual void func1() { cout << "A::func1" << endl; }int _a;
              };class B : public A
              //class B : virtual public A
              {
              public:virtual void func2() { cout << "B::func2" << endl; }int _b;
              };class C : public A
              //class C : virtual public A
              {
              public:virtual void func3() { cout << "C::func3" << endl; }int _c;
              };class D : public B, public C
              {
              public:virtual void func4() { cout << "D::func4" << endl; }int _d;
              };int main()
              {D d;cout << sizeof(d) << endl;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
              }
              

              编译内存窗口:
              在这里插入图片描述
              在这里插入图片描述

              可以看出菱形继承和多继承的内存分布几乎差不多,就不解释了。

              5.2 菱形虚拟继承

              看一下这段菱形虚拟继承的代码:

              class A
              {
              public:virtual void func1() { cout << "A::func1" << endl; }int _a;
              };class B : virtual public A
              {
              public:virtual void func2() { cout << "B::func2" << endl; }int _b;
              };class C : virtual public A
              {
              public:virtual void func3() { cout << "C::func3" << endl; }int _c;
              };class D : public B, public C
              {
              public:virtual void func4() { cout << "D::func4" << endl; }int _d;
              };int main()
              {D d;cout << sizeof(d) << endl;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
              }
              

              编译内存窗口:
              在这里插入图片描述
              在这里插入图片描述

              实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看了,一般我们也不需要研究清楚,因为实际中很少用。

              关于菱形继承和菱形虚拟继承更重要的还是如何用菱形虚拟继承解决菱形继承的两个问题:

              1. 数据冗余
              2. 二义性

              我在之前的文章介绍过,这是链接:【继承】复杂的菱形继承

              有兴趣的小伙伴可以看看。


                6. 关于继承和多态相关题目

                1. 下面代码输出结果是:
                #include<iostream>using namespace std;
                class A {
                public:A(const char* s) { cout << s << endl; }~A() {}
                };
                class B :virtual public A
                {
                public:B(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
                };
                class C :virtual public A
                {
                public:C(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
                };
                class D :public B, public C
                {
                public:D(const char* s1, const char* s2, const char* s3, const char* s4) :B(s1, s2),C(s1, s3),A(s1){cout << s4 << endl;}
                };
                int main() {D* p = new D("class A", "class B", "class C", "class D");delete p;return 0;
                }
                

                这里首先调用D的构造函数,先走初始化列表,但是走初始化列表的顺序是按照声明的顺序,而A是最先声明的,所以先走A的构造函数,再走B的构造函数,在走C的构造函数,然而A已经被初始化过了,所以最终的结果是 class A class B class C class D

                1. 多继承指针偏移问题,p1, p2, p3, p4的关系是:
                class Base1 { public: int _b1; };
                class Base2 { public: int _b2; };
                class Derive : public Base1, public Base2 { public: int _d; };
                int main(){
                Derive d;
                Base1* p1 = &d;
                Base2* p2 = &d;
                Derive* p3 = &d;
                return 0;
                }
                

                这太简单了,就是简单的切片,很明显:p1 == p3 != p2。

                1. 以下程序输出结果是什么:
                class A
                {
                public:virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }virtual void test() { func(); }
                };
                class B : public A
                {
                public:void func(int val = 0) { std::cout << "B->" << val << std::endl; }
                };
                int main(int argc, char* argv[])
                {B* p = new B;p->test();return 0;
                }

                这题A类中的虚表中有functest,B类中的虚表指针也是functest,只是func完成了重写(虽然缺省值不同,但是满足参数列表相同、返回值相同、函数名相同,就是重写),所以显然是调用B中的func,但是前面讲过虚函数的继承实际上是接口继承,所及继承了A类的接口,因此val == 1,所以结果是 B->1,这题有点坑人了哈哈哈。

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

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

                相关文章

                Linux——web建立wordpress

                下载 [rootnfs-server ~]# yum install php wget https://wordpress.org/latest.tar.gz解压 /var/www/html [rootnfs-server html]# tar -xzvf latest.tar.gz [rootnfs-server html]# rm latest.tar.gz授权 [rootnfs-server html]# chown -R www:www /var/www/html添加文件备…

                利用kimi等大模型进行运维参数解析和调优

                在运维时&#xff0c;经常遇到很多参数&#xff0c;有些参数不知道意义&#xff0c;知道意义的也有些不知道合理参考值是多少。利用kimi等大模型来当老司机&#xff0c;轻松解决运维难题。 例如在运维hive参数时&#xff0c;有些不知道作用&#xff0c;提示次如下 你的角色是…

                windows ubuntu sed,awk,grep篇:7.sed 多行模式及循环

                目录 46.读取下一行数据并附加到模式空间(命令 N) 47.打印多行模式中的第一行(命令 P) 48. 删除多行模式中的第一行(命令 D) 49.循环和分支(命令 b 和 :label 标签) 50.使用命令 t 进行循环 Sed 默认每次只处理一行数据&#xff0c;除非使用 H,G 或者 N 等命令创建多行模式&…

                python学习笔记B-11:序列结构之列表--二维列表的遍历和生成式

                二维列表的遍历方式&#xff0c;使用双层for循环&#xff0c;遍历索引号。 二维列表的生成式&#xff0c;也是使用类似双层循环的形式生成。 print("##初始化二维列表&#xff0c;每个元素就是1个列表") lst [["东方延续","太空军自然选择号舰长&qu…

                释放Stable Diffusion 无限可能

                最近在整理大语言模型的系列内容&#xff0c;Stable Diffusion 是我下一篇博客的主题。关注 Stable Diffusion&#xff0c;是因为它是目前最受欢迎和影响力最大的多模态生成模型之一。Stable Diffusion 于 2022 年 8 月发布&#xff0c;主要用于根据文本的描述产生详细图像&…

                基于SpringBoot+Vue笔记记录分享网站设计与实现

                项目介绍&#xff1a; 信息数据从传统到当代&#xff0c;是一直在变革当中&#xff0c;突如其来的互联网让传统的信息管理看到了革命性的曙光&#xff0c;因为传统信息管理从时效性&#xff0c;还是安全性&#xff0c;还是可操作性等各个方面来讲&#xff0c;遇到了互联网时代…

                C语言 | Leetcode C语言题解之第50题Pow(x,n)

                题目&#xff1a; 题解&#xff1a; double myPow(double x, int n){if(n 0 || x 1){return 1;}if(n < 0){return 1/(x*myPow(x,-(n1)));}if(n % 2 0){return myPow(x*x,n/2);}else{return x*myPow(x*x,(n - 1)/2);} }

                【Jenkins】持续集成与交付 (三):有关报错解决(Jenkins (2.387.3) or higher required)

                🟣【Jenkins】持续集成与交付 (三):有关报错解决Jenkins (2.387.3) or higher required 一、Jenkins主页报错二、安装Jenkins插件报错三、解决过程(解压替换jenkins.war)四、重新访问登录💖The Begin💖点点关注,收藏不迷路💖 一、Jenkins主页报错 New version …

                吴恩达2022机器学习专项课程(一)7.2 逻辑回归的简化成本函数

                问题预览/关键词 本节课内容逻辑回归的损失函数简化之后的形式是&#xff1f;为什么可以简化&#xff1f;成本函数的通用形式是&#xff1f;逻辑回归成本函数的最终形式是&#xff1f;逻辑回归为什么用对数损失函数计算成本函数&#xff1f;为什么不直接给出逻辑回归损失函数的…

                [详解]Spring AOP

                &#x1f3a5; 个人主页&#xff1a;Dikz12&#x1f525;个人专栏&#xff1a;Spring学习之路&#x1f4d5;格言&#xff1a;吾愚多不敏&#xff0c;而愿加学欢迎大家&#x1f44d;点赞✍评论⭐收藏 目录 什么是AOP? Spring AOP 快速入门 Spring AOP核心概念 切点(Point…

                selenium 4.x入门篇(环境搭建、八大元素定位)

                背景 Web自动化测现状 1. 属于 E2E 测试 2. 过去通过点点点 3. 好的测试&#xff0c;还需要记录、调试网页的细节 一、selenium4.x环境搭建 一键搭建 pip3 install webdriver-helper 安装后自动的完成&#xff1a; 1. 查看浏览器的版本号 2. 查询操作系统的类型…

                【智能优化算法】蚱蜢优化算法(Grasshopper Optimization Algorithm,GOA)

                蚱蜢优化算法(Grasshopper Optimization Algorithm&#xff0c;GOA)是期刊“IEEE Access”&#xff08;IF 3.9&#xff09;的2021年智能优化算法 01.引言 蚱蜢优化算法(Grasshopper optimization algorithm, GOA)&#xff0c;并将其应用于结构优化中的挑战性问题。该算法在数学…

                Microsoft Edge浏览器:高效、简洁、个性化的网页浏览体验

                Microsoft Edge是微软公司推出的一款网络浏览器&#xff0c;它是基于Chromium开源项目开发的&#xff0c;因此与Google Chrome有很多相似之处。以下是一些使用Microsoft Edge的心得体会&#xff1a; 1. 界面简洁&#xff1a;Microsoft Edge的界面设计非常简洁&#xff0c;用户…

                华为od入职第13天!

                今天早上就分配活了&#xff0c;写一个模块的ut&#xff0c;但是今天一句代码没写成&#xff0c;一直在看代码逻辑。下午就是新员工的一个会&#xff0c;部门20来个人做一下自我介绍啥的。晚上我导师给我们几个新员工讲项目框架和一些代码逻辑啥的&#xff0c;讲了一个多小时&a…

                区块链技术与应用学习笔记(5-7节)——北大肖臻课程

                ​ 目录 ​BTC实现 基于交易的账本模式&#xff1a; UTXO集合&#xff1a; 交易费用&#xff1a; BTC网络 1.应用层&#xff1a; 2.网络层&#xff1a; 3传播层&#xff1a; 什么是鲁棒&#xff1f; BTC挖矿&#xff1a; 出块奖励&#xff1a; 挖矿难度调整&#…

                Python | Leetcode Python题解之第51题N皇后

                题目&#xff1a; 题解&#xff1a; class Solution:def solveNQueens(self, n: int) -> List[List[str]]:def generateBoard():board list()for i in range(n):row[queens[i]] "Q"board.append("".join(row))row[queens[i]] "."return b…

                【C语言刷题系列】对数字添加逗号

                目录 一、问题描述 二、解题思路 三、源代码 拓展&#xff1a; 个人主页&#xff1a; 倔强的石头的博客 系列专栏 &#xff1a;C语言指南 C语言刷题系列 一、问题描述 二、解题思路 题目的要求&#xff0c;即对于一个较大的整数&#xff0c;每三位数字之间添加…

                CSS + HTML

                目录 一.CSS&#xff08;层叠样式表&#xff09; 二. CSS 引入方式 三.选择器 3.1 标签选择器 3.2 类选择器 3.3 id选择器 3.4 通配符选择器 3.5 画盒子 四.文字控制属性 4.1字体大小 4.2字体粗细 4.3 字体倾斜 4.4行高 4.5行高--垂直居中 4.6 字体族 4.7 字体复…

                使用mmdetection来训练自己的数据集(visdrone)(四)结果分析

                测试 python tools/test.py <your-config-file> <your-model-weights-file> --out <save-pickle-path>关于test.py 的命令行 parser.add_argument(--out,typestr,helpdump predictions to a pickle file for offline evaluation)计算量、参数量计算脚本 pyth…

                【自然语言处理】Word2VecTranE的实现

                作业一 Word2Vec&TranE的实现 1 任务目标 1.1 案例简介 Word2Vec是词嵌入的经典模型&#xff0c;它通过词之间的上下文信息来建模词的相似度。TransE是知识表示学习领域的经典模型&#xff0c;它借鉴了Word2Vec的思路&#xff0c;用“头实体关系尾实体”这一简单的训练目…