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

图片名称

博主首页: 有趣的中国人

专栏首页: 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.mfbz.cn/a/576313.html

                如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

                相关文章

                文件上传漏洞(upload-labs)

                目录 一、文件上传漏洞 1.什么是文件上传漏洞 常见的WebShell 2.文件上传产生漏洞的原因 二、文件上传绕过 &#xff08;一&#xff09;客服端绕过-JS验证 1.前端验证 upload-labs第一关 &#xff08;二&#xff09;绕过黑名单验证 黑名单验证 1.特殊解析后缀 upl…

                Pandas 2.2 中文官方教程和指南(十一·一)

                原文&#xff1a;pandas.pydata.org/docs/ PyArrow 功能 原文&#xff1a;pandas.pydata.org/docs/user_guide/pyarrow.html pandas 可以利用PyArrow来扩展功能并改善各种 API 的性能。这包括&#xff1a; 与 NumPy 相比&#xff0c;拥有更广泛的数据类型 对所有数据类型支持缺…

                C# 结合JavaScript实现手写板签名并上传到服务器

                应用场景 我们最近开发了一款笔迹测试功能的程序&#xff08;测试版&#xff09;&#xff0c;用户在手写板上手写签名&#xff0c;提交后即可测试出被测试者的心理素质评价分析。类似功能的场景还比如&#xff0c;在银行柜台办理业务&#xff0c;期间可能需要您使用手写设备进…

                linux 编译binutil 遇到问题

                在centos6.10上编译binutil2.27时遇到问题&#xff1a; as.c&#x1f4af;31: error: ‘DEFAULT_GENERATE_ELF_STT_COMMON’ undeclared here (not in a function) 搜到解决方法是这个&#xff1a; 1、https://github.com/riscv-software-src/riscv-tools/issues/66 &#xf…

                十七、Java网络编程(一)

                1、Java网络编程的基本概念 1)网络编程的概念 Java作为一种与平台无关的语言,从一出现就与网络有关及其密切的关系,因为Java写的程序可以在网络上直接运行,使用Java,只需编写简单的代码就能实现强大的网络功能。下面将介绍几个与Java网络编程有关的概念。 2)TCP/IP协议概…

                内置对象部分

                一&#xff0c;内置对象 二&#xff0c;math对象 不是构造函数&#xff0c;不需要new来调用&#xff0c;而是直接使用里面的属性和方法即可 1.随机方法random 返回一个随机的小数 [0,1&#xff09; 2.日起格式化 返回的月份会小一&#xff0c;记得加一 周一返回1&#xff…

                iObit Uninstaller 安装、激活、使用教程

                「软件简介」 IObit Uninstaller 一款专业的卸载工具&#xff0c;旨在彻底移除不需要的软件、插件以及 Windows 应用&#xff0c;同时提供安全、快速和轻量化的 PC 使用体验。 〖下载安装软件〗 版本&#xff1a;V 13.4.0 | 26.9 MB 支持系统&#xff1a;Windows 11/10/8.1/8/…

                传媒论坛编辑部传媒论坛杂志社传媒论坛杂志2024年第7期目录

                专题│场景传播研究 场景传播&#xff1a;一场遮盖自我与寻找自我的博弈 胡沈明; 3 基于CiteSpace的中国场景传播研究热点分析 管倩;粟银慧; 4-610《传媒论坛》投稿&#xff1a;cnqikantg126.com 数字世界的美与危&#xff1a;场景传播的失范与应对之举 王依晗;章洁…

                Centos/linux根目录扩容、分区、挂载。LVM、物理卷、逻辑卷

                前言    &#xff08;空格&#xff09; &#xff1a;分区挂载和扩容是两码事 每个Linux使用者在安装Linux时都会遇到这样的困境&#xff1a;在为系统分区时&#xff0c;如何精确评估和分配各个硬盘分区的容量&#xff0c;因为系统管理员不但要考虑到当前某个分区需要的容量&a…

                在Linux系统内搭建DNS本地服务器

                文章目录 Linux的本地DNS服务一、什么是DNS1.1、域名1.2、DNS服务器、DNS客户端和DNS中继1.3、DNS域名解析 二、搭建DNS服务2.1、正反向解析2.1.1.安装bind软件包2.1.2.修改主配置文件2.1.3.修改区域配置文件2.1.4.配置区域数据文件2.1.5.启动服务、关闭防火墙2.1.6.本地解析测…

                网络安全实训Day24(End)

                写在前面 并没有完整上完四个星期&#xff0c;老师已经趁着清明节假期的东风跑掉了。可以很明显地看出这次持续了“四个星期”实训的知识体系并不完整&#xff0c;内容也只能算是一次基础的“复习”。更多的内容还是靠自己继续自学吧。 网络空间安全实训-渗透测试 文件包含攻击…

                美国FBA头程物流降本增效策略解析

                随着跨境电商的迅速发展&#xff0c;美国FBA头程物流作为连接卖家与消费者的重要环节&#xff0c;其成本控制对于提高卖家盈利能力具有重要意义。本文将从多个方面探讨如何降低美国FBA头程物流成本&#xff0c;帮助卖家在激烈的市场竞争中取得优势。 1.合理安排发货计划是降低成…

                【产品经理修炼之道】- 消金支付体系

                我们常听说“互联网的尽头是放贷”&#xff0c;而当支付与金融结合会衍生出各种场景。本文将给大家拆解下不同消费金融场景下的支付案例&#xff0c;一起来看看吧。 各位小伙伴&#xff0c;大家好&#xff01; 我们常听说“互联网的尽头是放贷”&#xff0c;确实这说其实话糙…

                Docker基础+虚拟化概念

                目录 一、虚拟化简介 1、虚拟化概述 2、cpu的时间分片&#xff08;cpu虚拟化&#xff09; 3、cpu虚拟化性性能瓶颈 4、虚拟化工作 4.1虚拟机工作原理 4.2两大核心组件:QEMU、KVM 4.2.1QEMU&#xff1a; 4.2.2KVM&#xff1a; 5、虚拟化类型 ①全虚拟化&#xff1a; …

                string的OJ题

                1.字符串相加 给定两个字符串形式的非负整数 num1 和num2 &#xff0c;计算它们的和并同样以字符串形式返回。 你不能使用任何內建的用于处理大整数的库&#xff08;比如 BigInteger&#xff09;&#xff0c; 也不能直接将输入的字符串转换为整数形式。 思路&#xff1a;从字…

                【Linux】git和gdb

                下面还有两个Linux中会用到的工具&#xff0c;一个是给git&#xff0c;可以叫版本控制器&#xff0c;就是对我们写的代码进行版本控制 一个是gdb&#xff0c;就是我们C语言/C的代码调试工具 下面我们分别来介绍一下 git git和Linux一样&#xff0c;也是一个开源项目&#xff0c…

                高频 LC、压控及晶体振荡器

                实验名称 高频 LC、压控及晶体振荡器 一、实验目的 1. 正确地使用数字频率计测试频率。 2. 了解电源电压,负载及温度等对振荡须率的影响,从而加深理解为提高频率稳定度应采取的措施。 二、实验原理- 组成一个振荡器能否…

                深度学习知识点:循环神经网络(RNN)、长短期记忆网络(LSTM)、门控循环单元(GRU)

                深度学习知识点&#xff1a;循环神经网络&#xff08;RNN&#xff09;、长短期记忆网络&#xff08;LSTM&#xff09;、门控循环单元&#xff08;GRU&#xff09; 前言循环神经网络&#xff08;RNN&#xff09;RNNs&#xff08;循环神经网络&#xff09;训练和传统ANN&#xff…

                Golang操作Redis

                一. Redis介绍 1.1 简介 Redis是完全开源免费的&#xff0c;遵循BSD协议&#xff0c;是一个高性能的key-value数据库。 Redis与其它的key-value缓存产品有以下三个特点&#xff1a; Redis支持数据持久化&#xff0c;可以见内存中的数据报错在磁盘中&#xff0c;重启的时候可以…

                从Kafka的可靠性设计体验软件设计之美

                目录 1. Kafka可靠性概述 2. 副本剖析 2.1 什么是副本 2.2 副本失效场景 2.3 数据丢失场景 2.4 解决数据丢失方案 3. 日志同步机制 4. 可靠性分析 1. Kafka可靠性概述 Kafka 中采用了多副本的机制&#xff0c;这是大多数分布式系统中惯用的手法&#xff0c;以此来实现水平扩…
                最新文章