【C++】继承:菱形继承
希望文章能对你有所帮助,有不足的地方请在评论区留言指正,一起交流学习!
在使用C++编写程序的时候,存在多继承的情况,这是正常的;但是多继承会存在一些问题,那就是菱形继承,虽然C++委员会已经解决了这个问题,但是还是不建议初学者使用,在了解其底层结构之后,它是很难控制的。当然继承的讲解,也有【C++】:继承全面解析-CSDN博客。
1.菱形继承
本小节讲述什么是多继承以及单继承,。
单继承:一个子类只有一个直接父类时称这个继承关系为单继承。

class Person
{
public:string _name = "小五子";; // 姓名int age;};
class Student : public Person
{
protected:int _num; //学号string major;//专业
};
class Teacher : public Person
{
protected:int _id; // 职工编号string subject;//教授科目
};class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修课程
};
int main()
{Assistant a;return 0;
}
创建对象,运行程序查看内存;
上图和本文中的Assistant的结构图是一样的,Student和Teacher都是有自己的Person
注意:虽然在内存中看的时候是有类在外面包着,但是真正的内存存储只有成员变量,因此才会有调用的冲突。
int main()
{Assistant a;a._name = "小六子";return 0;
}
上述程序可定有问题
int main()
{Assistant a;a.Student::_name = "小六子";cout << a.Student::_name << endl;cout << a.Teacher::_name << endl;a.Teacher::_name = "小七子";cout << a.Student::_name << endl;cout << a.Teacher::_name << endl;return 0;
}
这样我们自己解决了二义性的问题。但是数据冗余问题无法解决,怎么办?
上述的程序内存较小;但是我将在Person插入一个256的字符数组呢。
然后我们使用关键字sizeof来计算对象a 的大小
int main()
{Assistant a;cout << sizeof(a) << endl;return 0;
}
我们将Student中的继承去掉再看;抓重放轻。
计算结果
二者相差544字节,也就是说Person单个就是544字节,对于内存小的类,可以忽略,但是就上面那种能忽略?
菱形继承:就是在最后一层的派生类继承的时候将上层的父类重复继承了,就是重复继承。
2.解决方式(虚继承的实现)
虚继承的实现
- 编译器在虚继承的派生类对象中,会加入一个虚基表指针(vbptr)。
- 虚基表(virtual base table, vbtable) 里记录的是从当前对象指针到虚基类子对象的偏移量。
- 访问虚基类的成员时,编译器会通过 vbptr 找到虚基表,再根据偏移量找到虚基类子对象,然后访问成员。
- 这个过程不是通过 vtable(虚函数表),而是通过 vbtable(虚基表)。
虽说二义性可以指定类域区别,但是本意是创建的结构体中Assistant,要有一个名字,并且也解决了数据冗余的问题。所以引入了虚拟继承。
还是上述的例子,我们在菱形继承的腰子处采用虚拟继承。
程序改动
这样最终的 Assistant
里就只会保留一份基类Person的成员,既避免冗余,也消除了二义性。
底层看一下 下述是测试程序,使用对象直接访问成员变量不会再有二义性了。
int main()
{Assistant a;a.Student::_name = "小六子";cout << a.Student::_name << endl;cout << a.Teacher::_name << endl;return 0;
}
看一下结果,
再看一下底层
执行程序
a.Student::_name = "小六子";
说明_Assistant中只有一个_name。使用地址再证明一下。
数据冗余是怎么解决的呢?我们继续看。我们换一个例子,来讲述,更加的明显易懂的。
A/ \B C\ /D
class A
{
public:int _a;
};
class B : virtual public A
{
public:int _b;
};
class C : virtual public A
{
public:int _c;
};
class D : public B, public C
{
public:int _d;
};
int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}
首先我们直接使用sizeof()测试D的大小看看是否解决了数据的冗余。
未采用虚拟继承的;
为什么反而占用的内存多了呢?我们添加一个大一些的数组看看。
可以看出虚拟继承再基类较大的时候才可以明显的区分,但是为什么数据小的时候会增加4个字节的内存呢?我们继续看。
没有采用虚拟继承之前
采用虚拟继承之后
可以看出,菱形继承,将重复的成员变量进行了重新安防。那么下图中的两个地址是干什么的。
调用内存看一下
0088db40指向的位置 以及0088db48
16进制转换到十进制一个是20一个是12。我们地址之间的差距。上面的表叫做虚基表,但是对于对象d自己内存中的成员变量不是随便的调用吗,所以虚基表存在的作用是什么,往下看。
所以20 和 12 分别是 B和C对象距离a的偏移量,所以虚拟继承的底层为了帮助对象找寻a采用了偏移量的方式。但是一个4个字节或者两个字节就可以了为什么还要使用8个字节。还有其他的值要存储 ,为其他的值进行了预留,其他的对象也是用想用的空间的,不同的对象是一样的区间?
偏移量问题还涉及到子类赋值给父类的问题,当子类的引用或者值赋值给了父类,那么,父类将会通过偏移量来找到被重新放置的重复变量。
然后赋值之后的父类虚基表变化
看来赋值不会改变父类偏移量啊,所以偏移量的作用就是帮助对象找到a然后将其赋值给父类。
总结
机制 | 作用 | 表内容 |
---|---|---|
虚函数表(vtable) | 动态绑定虚函数 | 函数地址 |
虚基表(vbtable) | 访问虚基类成员 | 偏移量 |
注意的是:
采用虚继承的方式,派生类本身也会发生变化的,都是将基类的成员变量放在底层,给出虚基表。
总结和反思
- 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱 形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设 计出菱形继承。否则在复杂度及性能上都有问题。
- 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。
- public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
- 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
- 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称 为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的 内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很 大的影响。派生类和基类间的依赖关系很强,耦合度高。
- 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被装。
- 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有 些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用 继承,可以用组合,就用组合。