菱形虚拟继承
文章目录
一:复杂的菱形继承及菱形虚拟继承
1.三种继承方式
单继承:一个子类只有一个直接父类时称这个继承
关系为单继承
多继承:一个子类有两个或以上直接父类时称这个
继承关系为多继承
菱形继承:可以看出其中既有单继承也有多继承
但是菱形继承的问题也很明显:
-
我们可以看到类Assistant继承了两份来自类Person的数据,这就造成了冗余同时也造成访问的二义性。
-
如下代码
#include<iostream>
class Person{
public:
int _id=123;
};
class Student:public Person //单继承
{
public:
const char* name="peter";
};
class Teacher : public Person //单继承
{
public:
const char* sex="男";
};
class Assistant:public Student,public Teacher //多继承
{
public:
int age=20;
};
//以上的所有继承关系构成了菱形继承
/*
Person
/ \
/ \
Student Teacher
\ /
\ /
Assistant
菱形继承的二义性:Assistant类既继承自Student,又继承自Teacher
当访问Student和Teacher共同的基类Person中的成员时产生二义性
菱形继承的数据冗余:Assistant类既继承自Student,又继承自Teacher
则Assistant中就有两份来自Person类中的资源
*/
void test(){
Assistant ast;
std::cout << ast.name << std::endl;
std::cout << ast.sex << std::endl;
std::cout << ast.age << std::endl;
//std::cout << ast._id << std::endl;//提示_id访问不明确
std::cout <<ast.Student::_id << std::endl;
std::cout << ast.Teacher::_id << std::endl;
// 需要显示指定访问哪个父类的成员可以解决二义性问题,
//但是数据冗余问题无法解决
}
int main(){
Assistant ast;
test();
return 0;
}
- 运行结果
这样就造成了继承的二义性和数据冗余的问题
该如何解决这个问题呢?
-
咱们接着来看
-
解决菱形继承的问题就要用到一种新的技术,虚拟继承可以解决菱形继承的二义性和数据冗余的问题。
-
我们可以看到上面的继承关系,在Student和Teacher的继承Person时继承了两份资源,但是同样在继承的时候使用virtual关键字来修饰继承关系,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。
#include<iostream>
class Person{
public:
int _id = 123;
};
class Student :virtual public Person
{
public:
char* name = "peter";
};
class Teacher :virtual public Person
{
public:
char* sex = "男";
};
class Assistant :public Student, public Teacher
{
public:
int age = 20;
};
/*
Person
/ \
/ \
Student Teacher
\ /
\ /
Assistant
*/
void test(){
Assistant ast;
std::cout << ast.name << std::endl;
std::cout << ast.sex << std::endl;
std::cout << ast.age << std::endl;
std::cout << ast._id << std::endl;
//利用虚拟继承解决了访问不明确的问题(菱形继承的二义性和数据冗余问题)
}
int main(){
test();
return 0;
}
2.虚拟继承解决数据冗余和二义性的原理
原理:多个类继承一个对象的时候,这些派生类通过某种方式共享基类中的成员
-
而这个某种方式当然能就是通过虚基表和虚基表指针的方式实现
-
每个派生类的虚基表中存放当前对象相对于基类部分的偏移量,从而通过偏移量准确的找到基类的内容。
虚基表:当前对象相对于基类部分的偏移量
class A {
public:
int _a;
};
// class B : public A
class B : virtual public A {
public:
int _b;
};
// class C : public A
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;
}
- 原理图
这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。
- 下图是上面的Person关系菱形虚拟继承的原理解释
虚拟对象的顶部首先存储的是虚基表指针,接着存储的是关于基类的偏移量
- 虚拟对象的访问步骤
- 1.取对象前4个字节的内容(虚基表的指针)
- 2.通过虚基表指针从偏移量表格中取基类起始位置相对于派生类对象的起始位置的偏移量
- 3.通过偏移量在派生类对象中找到基类成员并访问
- 派生类中内部成员的访问
- 直接访问
二:普通继承与虚拟继承的区别
- 普通继承
- 虚拟继承
#include<iostream>
using namespace std;
class Grandfather{
public:
int _g;
};
class father :virtual public Grandfather{
public:
int _f;
};
class uncle :virtual public Grandfather{
public:
int _u;
};
class child : public father, public uncle{
public:
int _c;
};
void test(){
Grandfather g;
father f;
uncle u;
child c;
cout << "&c.Grandfather::_g" << &c.Grandfather::_g << endl;
c.father::_g=1;
//将_g赋值成了1
cout << "&c.father::_g=" << &c.father::_g << endl;
c.uncle::_g = 2;
//_g的值被修改成了2
cout << "&c.uncle::_g=" << &c.uncle::_g << endl;
cout << "&c._g=" << &c._g << endl;
c._c = 10;
c._f = 5;
c._u = 4;
child* c1 = &c;
}
int main(){
test();
return 0;
}
- 运行结果
-
由运行结果得知在菱形继承的孙子 类中继承的爷爷类中的数据是爷孙三代共享的,共同维护的;
-
再看其结构监视结果
- child类视图
- child类图
-
1.在继承同样内容的时候,虚拟继承比普通继承方式的内存多四个字节
-
2.虚拟继承中基类部分在下,子类部分在上。普通继承与其相反。
-
3.编译器会给派生类生成默认的构造函数—> 要在对象的前四个字节里放一些内容
-
4.虚拟继承对象前4个字节中内容指向一块内存空间
多继承中指针偏移问题
class Base1 {
public:
int _b1;
};
class Base2 {
public:
int _b2;
};
class Derive : public Base1, public Base2 {
public:
int _d;
};
int main()
{
// A. p1 == p2 == p3
// B. p1 < p2 < p3
// C. p1 == p3 != p2
// D. p1 != p2 != p3
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}
-
将派生类 Derive 的对象地址赋给其基类的指针的时候先会发生切片操作 原因是:基类的指针只能访问到自己类型大小的空间
-
因而Base1类型和Base2类型的指针将多余的内容切除了
-
从而p1和p3都指向了p3对象的首地址
-
而p2指向了自己能访问到的地址(发生偏移)
-
如下代码
void Test(){
child c;
child *c1 = &c;
father *f = &c;
uncle *u = &c;
Grandfather *g = &c;
cout << "&c=" << &c << endl;
cout << "c1=" << c1 << endl;
cout << "f=" << f << endl;
cout << "u=" << u << endl;
cout << "g=" << g << endl;
}
- 运行结果
可以看到不同类型的指针指向了相同的对象地址,之后运行结果却有很大差异,那么我们也可以验证基类指针访问派生类中的对象时其只能访问到与其类型匹配的那一块内存(切片操作的结果),同样也验证了多重继承中指针偏移的问题
继承的总结与反思
有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
三:【C++】继承和组合的概念?什么时候用继承?什么时候用组合?
-
继承:通过扩展已有的类来获得新功能的代码重用方法
-
组合:新类由现有类的对象合并而成的类的构造方式
-
何时用继承?何时用组合?
-
1.如果二者间存在一个“是”的关系,并且一个类要对另外一个类公开所有接口,那么继承是更好的选择
// Car和BMW Car和Benz构成is-a的关系
class Car{
protected:
string _colour = "白色"; // 颜色
string _num = "陕ABIT00"; // 车牌号
};
class BMW : public Car{
public:
void Drive() {cout << "好开-操控" << endl;}
};
class Benz : public Car{
public:
void Drive() {cout << "好坐-舒适" << endl;}
};
- 2.如果二者间存在一个“有”的关系,那么首选组合
class Tire{
protected:
string _brand = "Michelin"; // 品牌
size_t _size = 17; // 尺寸
};
class Car{
protected:
string _colour = "白色"; // 颜色
string _num = "陕ABIT00"; // 车牌号
Tire _t; // 轮胎
};
-
ps:
-
没有找到极其强烈无法辩驳的使用继承的利用的时候,一律采用组合
-
组合体现为现实层面,继承主要体现在扩展方面
-
如果并不是需要一个类的所有东西(包括接口和熟悉),那么就不需要使用继承,使用组合更好
-
如果使用继承,那么必须所有的都继承,如果有的东西你不需要继承但是你继承了,那么这就是滥用继承