在C++中,类是允许多继承的,多继承大大的提高了代码的复用、减少代码冗余、大大的提高了类的表现力,使得类更贴近现实中的事物,使用起来更为灵活,更面向对象。
但由于这灵活的语法,使得C++使用起来比别的编程语言更为复杂,不过凡事有利必有弊,这里就不去探讨其中的利弊,还是把注意力放到使用继承时候需要注意的地方。
钻石继承
什么是钻石继承?
A
/ \
X Y
\ /
Z
钻石继承是多继承的一种情况如上图中:类A中派生出类X 和类Y ,类X和类Y派生出类Z,那么类A称为公共基类,类Z称为汇合子类。
那么我现在要编写一个类Z,当实例一个Z对象的时候,该对象要包含A X Y的实例各一份。
那么普通的多继承方式能不能达到要求?
#include <stdio.h>
#include <iostream>
using namespace std;
class A
{
public:
A(int data) : m_data(data)
{
cout << "A构造 : " << this << endl;
}
protected:
int m_data;
};
class X : public A
{
public:
X(int data) : A(data)
{
cout << "X构造 : " << this << endl;
}
// 获得继承于类A中m_data的值
int getData(void) const
{
return m_data;
}
};
class Y : public A
{
public:
Y(int data) : A(data)
{
cout << "Y构造 : " << this << endl;
}
// 修改继承于类A中m_data的值
void setData(int data)
{
m_data = data;
}
};
class Z : public X, public Y
{
public:
Z(int data) : X(data), Y(data)
{
cout << "Z构造 : " << this << endl;
}
};
int main(void)
{
Z z(0);
z.setData(100); //使用类Y的函数修改类A的数据
cout << "m_data = " << z.getData() << endl; // 通过类X的函数访问类A的数据
return 0;
}
编译后的运行结果
首先发现了公共基类 A中的m_data并没有成功,而且从打印信息中发现类A实例化了2次,而且打印出来的地址分别和X和Y的地址一样。这说明了Z的实例中存在了2份A的实例,分别存在于实例Y和实例X中。
在z.getData中,是通过实例X提供的函数访问了实例X中的实例A。
在z.setData中,是通过实例Y提供的函数修改了实例Y中的实例A。
这说明了Z的实例通过不同的路径访问实例A,得到了不一样的数据,这样并没有达到设计中的要求,而且使用起来也很不人性化。简直是差评,太糟糕了。
解决方法: 使用虚继承
为了令Z的实例中只拥有一份A的实例,可以采取虚继承来解决钻石继承带来的问题。
/*
通过虚继承解决钻石继承带来的问题
*/
#include <stdio.h>
#include <iostream>
using namespace std;
class A
{
public:
A(int data) : m_data(data)
{
cout << "A构造 : " << this << endl;
}
protected:
int m_data;
};
class X : virtual public A
{
public:
X(int data) : A(data)
{
cout << "X构造 : " << this << endl;
}
int getData(void) const
{
return m_data;
}
};
class Y : virtual public A
{
public:
Y(int data) : A(data)
{
cout << "Y构造 : " << this << endl;
}
void setData(int data)
{
m_data = data;
}
};
class Z : public X, public Y
{
public:
// 注意,采用虚继承的话,必须在汇聚子类的构造函数中显示的初始化公共基类,否则公共基类会调用缺省构造
Z(int data) : X(data), Y(data), A(data)
{
cout << "Z构造 : " << this << endl;
}
};
int main(void)
{
Z z(0);
z.setData(100);
cout << "m_data = " << z.getData() << endl;
return 0;
}
运行结果:
通过虚继承问题就能解决了,类A只构造了一次,所以在类Z的实例中只存在一份实例A。
虚指针 和 虚表
通过虚继承方式被继承的基类称为虚基类,通过虚继承派生的子类,会拥有一个虚指针,该指针指向一个虚表,虚表中记录的该类的各种信息,例如实例中与虚基类实例的偏移量,和虚函数与普通函数的入口地址。虚指针和虚表在C++实现多态的实现中起到重要的作用。
在代码中,A是Z的虚基类子对象,X和Y相对于Z是中间基类子对象。
Z的实例化过程如下(脑补的,如有错误请指出)
编译期间,发现X和Y是采用了虚继承,所以为其增加了一个虚指针和一个虚表,程序运行的时候先创建A,然后创建X,根据X的地址和A的地址,算出偏移量并存放到虚表中,Y与X一样,最后创建Z。
内存模型:虚指针->虚基类表->虚基类子对象相对于中间基类子对象的偏移量。
这样在运行期间就能通过虚指针访问虚表,在从虚表中取得偏移量,就能通过X和Y访问到唯一的A了。
总结:
钻石继承问题:
派生多个中间子类的公共基类子对象,在继承自多个中间子类的汇聚子类对象中,存在多个实例。
在汇聚子类中,或通过汇聚子类对象,访问公共基类的成员,会因继承路径的不同而导致不一致。
通过虚继承,可以保证公共基类子对象在汇聚子类对象中,仅存一份实例,且为多个中间子类子对象所共享。
虚继承:
在继承表中使用virtual关键字。
位于继承链最末端的子类的构造函数负责构造虚基类子对象。
虚基类的所有子类(无论直接的还是间接的)都必须在其构造函数中显式指明该虚基类子对象的构造方式,否则编译器将选择以缺省方式构造该子对象。
虚基类的所有子类(无论直接的还是间接的)都必须在其拷贝构造函数中显式指明以拷贝方式构造该虚基类子对象,否则编译器将选择以缺省方式构造该子对象。