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

news2025/2/23 11:27:06
图片名称

博主首页: 有趣的中国人

专栏首页: 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.coloradmin.cn/o/1633290.html

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

                相关文章

                运维的利器–监控–zabbix–第二步:建设–部署zabbix agent--windows server系统--agent客户端安装部署

                第一步&#xff1a;下载windows agent软件 第一点&#xff1a;zabbix官网针对linux和window系统有两种不同的安装方式&#xff0c;其中&#xff1a;windows为tar压缩包&#xff0c;根据你zabbix server安装的版本&#xff0c;在官网下载同样版本的agent软件。 amd64&#xff…

                Eclipse:-Dmaven.multiModuleProjectDirectory system propery is not set.

                eclipse中使用maven插件的时候&#xff0c;运行run as maven build的时候报错 -Dmaven.multiModuleProjectDirectory system propery is not set. Check $M2_HOME environment variable and mvn script match. 可以设一个环境变量M2_HOME指向你的maven安装目录 M2_HOMED:\Apps\…

                vcruntime140_1.dll无法继续执行代码怎么办,分享5种解决方法

                在日常使用电脑进行各项任务操作时&#xff0c;用户可能会遇到一个令人困扰的问题&#xff1a;当尝试运行某款软件以推进工作或享受娱乐时&#xff0c;系统突然弹出错误提示&#xff0c;明确指出“由于找不到vcruntime140_1.dll&#xff0c;无法继续执行代码”。。这个问题可能…

                城市公交查询系统的设计与实现(四)

                目录 4 系统概要设计 4.1 概要设计的概论 4.2 架构设计 4.3 系统功能结构图及分析 4.3.1 系统功能结构图 4.3.2 系统基本功能 1.站点查询 2.公交线路查询 3.站—站的查询 4.在线提问 5.网站公告 6&#xff0e;登录功能 7.用户管理 8.线路维护 9.公告管理 …

                【QT学习】UDP协议,广播,组播

                一。Udp详细解释 UDP&#xff08;User Datagram Protocol&#xff09;是一种无连接的传输层协议&#xff0c;它提供了一种简单的、不可靠的数据传输服务。与TCP相比&#xff0c;UDP不提供可靠性、流量控制、拥塞控制和错误恢复等功能&#xff0c;但由于其简单性和低开销&#x…

                在vue2中,什么是双向绑定,为什么vue3要进行优化?

                一、什么是双向绑定 我们先从单向绑定切入单向绑定非常简单&#xff0c;就是把Model绑定到View&#xff0c;当我们用JavaScript代码更新Model时&#xff0c;View就会自动更新双向绑定就很容易联想到了&#xff0c;在单向绑定的基础上&#xff0c;用户更新了View&#xff0c;Mo…

                # 使用 spring boot 时,@Autowired 注解 自动装配注入时,变量报红解决方法:

                使用 spring boot 时&#xff0c;Autowired 注解 自动装配注入时&#xff0c;变量报红解决方法&#xff1a; 1、使用 Resource 代替 Autowired 注解&#xff0c;根据类型注入改为根据名称注入&#xff08;建议&#xff09;。 2、在 XXXMapper 上添加 Repository 注解&#xff0…

                面向对象编程三大特征:封装、继承、多态

                封装、继承、多态 1. 封装 1.1 介绍 封装(encapsulation)就是把抽象出的数据 [属性] 和对数据的操作 [方法] 封装在一起,数据被保护在内部,程序的其它部分只有通过被授权的操作 [方法] ,才能对数据进行操作。 1.2 封装的理解和好处 1) 隐藏实现细节:方法(连接数据库)<…

                Stable Diffusion 模型分享:Counterfeit-V3.0(动漫)

                本文收录于《AI绘画从入门到精通》专栏&#xff0c;专栏总目录&#xff1a;点这里&#xff0c;订阅后可阅读专栏内所有文章。 文章目录 模型介绍生成案例案例一案例二案例三案例四案例五案例六案例七案例八 下载地址 模型介绍 高质量动漫风格模型。 条目内容类型大模型基础模…

                (十三)Servlet教程——Servlet中Cookie的使用

                1.什么是Cookie Cookie意为甜饼&#xff0c;最早由Netscape社区发展的一种机制。目前Cookie已经成为标准&#xff0c;所有的主流浏览器都支持Cookie。 由于HTTP是一种无状态的协议&#xff0c;服务器仅从网络连接上无法知道客户身份。于是就客户端颁发一个通行证&#xff0c;无…

                SpringBoot框架学习笔记(一):依赖管理和自动配置

                本文为个人笔记&#xff0c;仅供学习参考之用&#xff0c;如有不当之处请指出。 本文基于springboot2.5.3版本&#xff0c;开发环境需要是 jdk 8 或以上&#xff0c;maven 在 3.5 1 SpringBoot 基本介绍 1.1 官方文档 &#xff08;1&#xff09; 官网 : https://spring.io/pr…

                虚函数表与虚函数表指针

                虚函数表与虚函数表是用来实现多态的&#xff0c;每一个类只有一个虚函数表 静态多态&#xff1a;函数重载&#xff08;编译期确定&#xff09; 动态多态&#xff1a;虚函数&#xff08;运行期确定&#xff09; 虚函数表的创建时机&#xff1a; 生成时间&#xff1a; 编译期…

                交换排序-冒泡排序 快速排序

                目录 3.1 冒泡排序 3.2 快速排序 Hoare版本快速排序 挖坑法快速排序 前后指针法快速排序 快速排序优化-三数取中法 快速排序非递归 3.1 冒泡排序 思想&#xff1a;升序情况下&#xff1a;左边大于右边就进行交换&#xff0c;每一次把最大的放在最后一位。 void Swap(int…

                【Unity100个实用小技巧】Unity接入微信SDK

                前言 为了实现Unity接入微信排行榜,记录一下&#xff0c;为了以后用&#xff0c;本篇文章是对于使用中的一些疑惑点记录。完整流程官方和下面链接都有&#xff0c;补充一些&#xff0c;其他文档中未提到的。 步骤 必要步骤 一. 微信转小游戏 勾选 【使用好友关系链】 二. 看下…

                (06)vite与ts的结合

                文章目录 系列全集package.json在根目录创建 tsconfig.json 文件在根目录创建 vite.config.ts 文件index.html额外的类型声明 系列全集 &#xff08;01&#xff09;vite 从启动服务器开始 &#xff08;02&#xff09;vite环境变量配置 &#xff08;03&#xff09;vite 处理 c…

                基于springboot+vue+Mysql的时间管理系统

                开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;…

                基于双层优化的电动汽车优化调度研究(附matlab程序)

                基于双层优化的电动汽车优化调度研究 0.代码链接 基于双层优化的电动汽车优化调度研究(matlab程序)资源-CSDN文库 1.简述 关键词&#xff1a;双层优化 选址定容 输配协同 时空优化 参考文档&#xff1a;《考虑大规模电动汽车接入电网的双层优化调度策略_胡文平》…

                机器学习-11-卷积神经网络-基于paddle实现神经网络

                文章目录 总结参考本门课程的目标机器学习定义第一步&#xff1a;数据准备第二步&#xff1a;定义网络第三步&#xff1a;训练网络第四步&#xff1a;测试训练好的网络 总结 本系列是机器学习课程的系列课程&#xff0c;主要介绍基于paddle实现神经网络。 参考 MNIST 训练_副…

                C# Form1.cs 控件全部丢失的问题解决

                在应用C#开发程序时&#xff0c;代码写了一堆&#xff0c;等调试时&#xff0c;点开 Form1.cs窗体时&#xff0c;出现如下提示。点击忽略并继续是&#xff0c;整个窗体控件全部丢失。 初次遇到这个问题&#xff0c;很容易进入到误区&#xff0c;以为窗体控件真的全部丢失了&am…

                算法入门ABC

                前言 初学算法时真的觉得这东西晦涩难懂&#xff0c;貌似毫无用处&#xff01;后来的后来&#xff0c;终于渐渐明白搞懂算法背后的核心思想&#xff0c;能让你写出更加优雅的代码。就像一首歌唱的那样&#xff1a;后来&#xff0c;我总算学会了如何去爱&#xff0c;可惜你早已远…