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

本文详细解析了C++中多态的底层原理,包括多继承中的虚函数表分配、抽象类和接口继承,以及菱形继承和菱形虚拟继承的特点。通过实例和代码展示了这些概念在实际编程中的应用和注意事项。
摘要由CSDN通过智能技术生成
图片名称

博主首页: 有趣的中国人

专栏首页: C++进阶

博主会持续更新

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


      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,这题有点坑人了哈哈哈。

                • 52
                  点赞
                • 33
                  收藏
                  觉得还不错? 一键收藏
                • 24
                  评论
                评论 24
                添加红包

                请填写红包祝福语或标题

                红包个数最小为10个

                红包金额最低5元

                当前余额3.43前往充值 >
                需支付:10.00
                成就一亿技术人!
                领取后你会自动成为博主和红包主的粉丝 规则
                hope_wisdom
                发出的红包
                实付
                使用余额支付
                点击重新获取
                扫码支付
                钱包余额 0

                抵扣说明:

                1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
                2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

                余额充值