谈菱形继承前我们先来讨论多继承
一. 多继承:一个子类同时继承多个父类。
class A
{
public:
A()
{
cout << "A::A" << endl;
}
~A()
{
cout << "A::~A" << endl;
}
private:
int m_a;
};
class B
{
public:
B()
{
cout << "B::B" << endl;
}
~B()
{
cout << "B::~B" << endl;
}
private:
int m_b;
};
class C:public A,public B
{
public:
C()
{
cout << "C::C" << endl;
}
~C()
{
cout << "C::~C" << endl;
}
private:
int m_c;
};
int main()
{
C c;
return 0;
}
我们可以在监视中看到c里面有A,B类的成员。
而且我们还可以看到构造顺序,先构造A,再构造B,最后构造C
问:为什么构造顺序是这样的呢?
原因:是因为在继承中,父类先构造,子类后构造,而A,B都是父类,那么构造的顺序即为声明继承的顺序
即 class C:public A,public B,先构造A类,后构造B类,而class C:public B,public A,是先构造B类,再构造A类。
二.菱形继承
菱形继承:是在多继承的基础上产生的。一个子类同时继承多个父类,而这多个父类又继承于另一个父类。
class A
{
public:
A()
{
cout << "A::A" << endl;
}
~A()
{
cout << "A::~A" << endl;
}
int m_a;
};
class B:public A
{
public:
B()
{
cout << "B::B" << endl;
}
~B()
{
cout << "B::~B" << endl;
}
int m_b;
};
class C:public A
{
public:
C()
{
cout << "C::C" << endl;
}
~C()
{
cout << "C::~C" << endl;
}
int m_c;
};
class D:public B,public C
{
public:
D()
{
cout << "D::D" << endl;
}
~D()
{
cout << "D::~D" << endl;
}
int m_d;
};
int main()
{
D d;
return 0;
}
我们可以看到,在d对象中有两个A类,每个A类里都有一个m_a成员
而且我们可以看到会构造两次A对象,那么d中有两个m_a就很理所当然了。
问:此时如果我们给这个m_a赋值,会发生什么呢?
比如执行代码 d.m_a = 10
;是无法通过编译的。而给出的错误消息是:对m_a的访问不明确。
原因:通过监视我们注意到,d中有两个m_a,一个是B中的,一个是C中的,而此时我们对m_a赋值,但是编译器是不知道我们要给哪个m_a进行赋值,所以出现报错:对m_a的访问不明确。
如果指明对B中的m_a赋值,我们需要加上限定
d.B::m_a = 10;//用类名限制访问
d.C::m_a = 20;
所以这就暴露出菱形继承的弊端:
1.会产生二义性;
2.数据会冗余,在内存中会存储多个变量。
解决菱形继承的办法----------------------------虚继承
三.虚继承
虚继承主要解决继承中访问不明确的问题
虚继承关键字:virtual
1.当虚继承存在的构造顺序
class A
{
public:
A()
{
cout << "A::A" << endl;
}
~A()
{
cout << "A::~A" << endl;
}
private:
int m_a;
};
class B
{
public:
B()
{
cout << "B::B" << endl;
}
~B()
{
cout << "B::~B" << endl;
}
private:
int m_b;
};
class C:public A,virtual public B
{
public:
C()
{
cout << "C::C" << endl;
}
~C()
{
cout << "C::~C" << endl;
}
private:
int m_c;
};
int main()
{
C c;
return 0;
}
可以观察到,当B类是虚拟继承时,虽然继承顺序是A类在前,B类在后,但是真实的构造顺序是先构造B,再构造A,说明虚继承不遵守执行顺序
总结:当有普通继承和虚继承同时存在时,先构造虚继承的类,再构造普通继承的类
2.解决菱形继承暴露出来的问题
class A
{
public:
A()
{
cout << "A::A" << endl;
}
~A()
{
cout << "A::~A" << endl;
}
int m_a;
};
class B:virtual public A
{
public:
B()
{
cout << "B::B" << endl;
}
~B()
{
cout << "B::~B" << endl;
}
int m_b;
};
class C:virtual public A
{
public:
C()
{
cout << "C::C" << endl;
}
~C()
{
cout << "C::~C" << endl;
}
int m_c;
};
class D:public B,public C
{
public:
D()
{
cout << "D::D" << endl;
}
~D()
{
cout << "D::~D" << endl;
}
int m_d;
};
int main()
{
D d;
return 0;
}
当B和C采用了虚继承,观察到
A类只构造了一次,而我们不采用虚继承时,A类构造了两次;
并且这个时候再对m_a赋值,比如d.m_a = 4
,编译器不会报错了。那么是因为什么呢?我们去内存看一看
为了接下来好观察,我们分别对m_a,m_b,m_c,m_d初始化 1,2,3,4.
我们看到在内存中,m_a,m_b,m_c,m_d都可以看到,而我们知道数据是连续存储的。那么m_b左侧的地址和m_c左侧的地址是代表什么呢?
我们输出这两个地址看到在m_b左侧的地址存的值是20(转换为10进制),而我们从58地址向后数20个字节看到,指向的是01,即为m_a;m_c左侧的地址存的值是12(转换为10进制),我们从1c地址向后数12个字节看到,指向的是01,也为m_a。至此我们就可以得到结论了。
总结:虚拟继承了之后,访问m_a不会出错呢,实际上是因为B类和C类里面根本就没有m_a,自始至终m_a只有一个,那就是A中的m_a。B类和C类存储的是从当前地址指向m_a地址的偏移量。B类和C类共享m_a这个成员。
通过B,C这个地址找到m_a,这个地址叫虚基表指针。
虚表指针指向的地址叫做虚基表。
虚基表中存放的是偏移量,通过偏移量可以找到父类的数据
结论:虚继承解决了菱形继承的二义性和数据冗余的问题,但是最好不要设计出菱形继承这样的结构,不然在复杂度和性能上都有问题。
面试题:
1.什么是菱形继承?菱形继承的问题是什么?
答:
菱形继承为一个子类继承于两个父类,而这两个父类又继承于另一个父类;
菱形继承会导致数据冗余和二义性。
2.什么是菱形虚拟继承?是如何解决冗余和二义性的?
答:
菱形虚拟继承是两个父类在继承另一个父类时采用虚拟继承;
是在两个父类中存储虚基表指针,虚基表指针指向的是虚基表,通过虚基表中的偏移量来访问最高父类的成员。