C++多态之带有虚函数的菱形继承与菱形虚拟继承

继承是C++三大特性之一,而继承又分为单继承和多继承,将单继承和多继承组合起来就成了复杂的菱形继承,本文重点讲菱形继承带来的问题和如何解决该问题。

回顾知识:

  • 单继承:一个子类只有一个直接父类时称这个继承关系为单继承。
  • 多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承。

在这里插入图片描述

菱形继承

什么是菱形继承?

菱形继承是多继承的一种特殊情况,由单继承和多继承构成,继承关系看起来像菱形,因此称为菱形继承。

在这里插入图片描述

菱形继承的问题和解决

由上述代码和下面的对象模型可以看出菱形继承有数据冗余(两个_name)和二义性(不知道访问哪个)的问题。
在这里插入图片描述

void Test()
{
	Assistant a;
		// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
		a.Student::_name = "Vision"; a.Teacher::_name = "hou";
}

要解决数据冗余就要放出大招了:虚拟继承!

为了更清晰的演示,下面我采用简单点的代码:

普通菱形继承:

#include<iostream>  
using namespace std;
 
class A
{
public:
	int _a;
};
 
class B : public A
{
public:
	int _b;
};
 
class C : public A
{
public:
	int _c;
};
 
class D : public C, public B
{
public:
	int _d;
};
 
int main()
{
	D dd;
	cout << sizeof(dd) << endl;
 
	dd.B::_a = 1;
	dd._b = 3;
 
	dd.C::_a = 2;
	dd._c = 4;
 
	dd._d = 5;
	B bb;
	C cc;
	cout << sizeof(bb) << endl;
 
	system("pause");
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s48aU7dP-1570376901226)(https://img-blog.csdnimg.c/20191006222449385.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0NDYyNDM2,size_16,color_FFFFFF,t_70)]
B、C中的_a都是来自于A,其实两个_a是同一个,但是因为B、C各自继承,产生了两份,造成了数据冗余。由于D继承于B、C,B、C继承于A,所以D中的_a不知道是来自B还是来自C,于是产生了二义性。

菱形虚拟继承

解决上述问题的方法就是将B和C的继承变为虚继承,在子类继承父类时,在访问限定符前加上virtual就可以虚继承。代码如下:

#include<iostream>  
using namespace std;  
  
class A  
{  
public:  
    int _a;  
};  
  
class B :virtual public A  
{  
public:  
    int _b;  
};  
  
class C :virtual public A  
{  
public:  
    int _c;  
};  
  
class D : public C, public B  
{  
public:  
    int _d;  
};  
  
int main()  
{  
    D dd;  
    cout << sizeof(dd) << endl;  
  
    dd._a = 1;  
    dd._b = 3;  
  
    dd._a = 2;  
    dd._c = 4;  
  
    dd._d = 5;  
    B bb;  
    C cc;  
    cout << sizeof(bb) << endl;  
  
    return 0;  
}  

在这里插入图片描述

菱形虚拟继承的内存对象成员模型:这里可以分析出D对象中将A放到的了对象组成的最下面,这个A 同时属于B和C,那么B和C

如何去找到公共的A呢?这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表

中存的偏移量。通过偏移量可以找到下面的A。

在这里插入图片描述
从上图可以发现对象dd的内存布局与虚继承之前有很大的区别,首先cc对象和bb对象的内存空间中都分别多了一个存储着一个地址的空间,而把它们_a变量放在了成员变量的最底下,使_a成为一个公共的变量。显而易见,加了virtual之后的继承比普通继承的类多了4个字节,而在内存中看到在加了virtual的类中多了一个地址,请看以下分析:

分析一:
在这里插入图片描述
分析二:
在这里插入图片描述
由以上分析,发现虚表里的上面的地址存储的是寻找自己的虚表的偏移量

                                       下面的地址存储的是寻找公共的基类的偏移量

C的虚基表里找公共基类的偏移量是20,B的虚基表里找公共基类的偏移量是12,从各自的虚基表往下数

各自的偏移量就可以找到公共的基类A。用一个公共的位置管理公共继承的基类,这样就解决了二义性。

带有虚函数的菱形继承

在类的成员函数前加上virtual关键字,则这个成员函数称为虚函数。先看看代码:

#include<iostream>  
using namespace std;
 
//定义一个可以指向对象里函数的函数指针  
typedef void(*func)();
 
//打印虚函数表  
void PrintVtable(int* vtable)
{
	printf("vtable:0x%p\n",vtable);
	
	for (size_t i = 0; vtable[i] != 0; ++i)
	{
		printf("第%d个虚函数地址:0x%p,->", i, vtable[i]);
		func f = (func)vtable[i];
		f();
	}
	cout <<"======================================="<< endl;
}
 
class A
{
public:
	int _a;
 
	virtual void func1()
	{
		cout << "A::func1()" << endl;
	}
	virtual void func2()
	{
		cout << "A::func2()" << endl;
	}
};
 
class B :public A
{
public:
	int _b;
	virtual void func1()
	{
		cout << "B::func1()" << endl;
	}
	virtual void func3()
	{
		cout << "B::func3()" << endl;
	}
};
 
class C :public A
{
public:
	int _c;
 
	virtual void func1()
	{
		cout << "C::func1()" << endl;
	}
	virtual void func3()
	{
		cout << "C::func3()" << endl;
	}
};
 
class D : public B,public C
{
public:
	int _d;
 
 
	virtual void func4()
	{
		cout << "D::func4()" << endl;
	}
};
 
 
int main()
{
	D dd;
 
	dd.B::_a = 1;
	dd.C::_a = 2;
	dd._b = 3;
	dd._c = 4;
	dd._d = 5;
 
 
	//D类里继承B类的虚表   
	
	PrintVtable(*(int**)&dd);
 
	system("pause");
	return 0;
}

当发生继承时,如果派生类重写了基类的虚函数,那么派生类的对象中会修改基类的虚表,虚表中的函数指针会指向派生类自己重写的函数,如果派生类没有重写基类的虚函数,那么派生类不会改变那个虚函数的指向只是把它继承下来。
在这里插入图片描述

带有虚函数的菱形虚拟继承

在菱形虚拟继承的基础上加上了虚函数,代码如下:

#include<iostream>  
using namespace std;  
  
//定义一个可以指向对象里函数的函数指针  
typedef void(*func)();  
  
//打印虚函数表  
void printvtable(int* vtable)     
{  
    cout << "虚表地址>" << vtable << endl;  
  
    for (int i = 0; vtable[i] != 0; ++i)  
    {  
        printf("第%d个虚函数地址:0x%x,->", i, vtable[i]);  
        func f = (func)vtable[i];  
        f();  
    }  
    cout << endl;   
}  
  
class A  
{  
public:  
    int _a;  
  
    virtual void func1()  
    {  
        cout << "A::func1()" << endl;  
    }  
};  
  
class B :virtual public A  
{  
public:  
    int _b;  
    virtual void func2()  
    {  
        cout << "B::func2()" << endl;  
    }  
    virtual void func1()  
    {  
        cout << "B::func1()" << endl;  
    }  
};  
  
class C :virtual public A  
{  
public:  
    int _c;  
  
    virtual void func1()  
    {  
        cout << "C::func1()" << endl;  
    }  
};  
  
class D : public C,public B  
{  
public:  
    int _d;  
  
    virtual void func1()  
    {  
        cout << "D::func1()" << endl;  
    }  
    virtual void func3()  
    {  
        cout << "D::func3()" << endl;  
    }  
};  
  
int main()  
{  
    D dd;  
    B bb;  
    C cc;  
    A aa;  
  
    cout << sizeof(dd) << endl;  
  
    dd._a = 1;  
    dd._b = 3;  
  
    dd._a = 2;  
    dd._c = 4;  
  
    dd._d = 5;  
  
    cout << sizeof(bb) << endl;  
  
    //D类里继承B类的虚表  
    cout << "D:bb:0x" <<int* vtabledd = (int*)(*(int*)&dd);   
    printvtable(vtabledd);  
  
    //D类里继承C类的虚表  
    cout << "D:cc:0x" << ((int*)&dd + 3);  
    int* vtablecc = (int*)(*((int*)&dd + 3));  
    printvtable(vtablecc);  
  
  
    //D类里继承A类的虚表  
    cout << "D:aa:0x" << ((int*)&dd + 6);  
    int* vtableaa = (int*)(*((int*)&dd +6));  
    printvtable(vtableaa);  
    return 0;  
}  

虚表:虚函数表,存的是虚函数->多态。虚基表:存的是偏移量,解决二义性和数据冗余性。

在这里插入图片描述

当给bb对象里加了个fun2函数后发现dd的对象模型又多了四个字节,这四个字节是在dd对象中继承的bb的内存空间中多出来的,就是用来存放bb对象自己的虚表地址。

因为aa对象被两个对象所继承,所以aa的虚表被被公用,因此bb和cc对象的虚函数就不能放在他们公共的虚表里,只能创建自己的虚表,把自己的虚函数放在自己的虚表。

然后dd对象里自己的虚函数(非继承来的)存在第一个继承来的对象的虚表里。
下面看看菱形继承dd对象里的虚函数都分别在什么地方存储的:

在这里插入图片描述

在这里插入图片描述
1.dd对象继承重写aa对象的虚函数存储在继承来的aa对象的虚函数表里。

2.dd对象里自己的虚函数(非继承来的)存在第一个继承来的对象的虚表里。

3.因为aa对象被两个对象所继承,所以aa的虚表被被公用,因此bb和cc对象的虚函数就不能放在他们公共的虚表里,只能创建自己的虚表,把自己的虚函数放在自己的虚表。

大致总结如下:

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++中的继承多态虚函数是面向对象编程的重要概念。 继承是指一个类可以从另一个类继承属性和方法。子类可以继承父类的公有成员和保护成员,但不能继承私有成员。通过继承,子类可以重用父类的代码,并且可以添加自己的特定功能。继承可以实现代码的重用和层次化的设计。 多态是指同一个函数可以根据不同的对象调用不同的实现。多态可以通过虚函数来实现。虚函数是在基类中声明为虚拟的函数,它可以在派生类中被重写。当通过基类指针或引用调用虚函数时,实际调用的是派生类中的实现。这样可以实现动态绑定,即在运行时确定调用的函数。 虚函数原理是通过虚函数表来实现的。每个包含虚函数的类都有一个虚函数表,其中存储了虚函数的地址。当调用虚函数时,编译器会根据对象的类型在虚函数表中查找对应的函数地址并调用。 综上所述,C++中的继承多态虚函数是实现面向对象编程的重要机制,它们可以提高代码的灵活性和可扩展性。 #### 引用[.reference_title] - *1* *3* [C++多态虚函数虚函数表](https://blog.csdn.net/weixin_46053588/article/details/121231465)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [c++多态虚函数表内部原理实战详解](https://blog.csdn.net/bitcarmanlee/article/details/124830241)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值