1、虚继承
在C++继承中会遇到一种特殊的情况——“钻石继承”。“钻石继承”指的是当一个基类A被两个派生类B和C同时继承,此时另一个派生类D又同时继承B和C,如果此时用图表示出来就像一个菱形一样,如下图。
派生类D通过继承之后拥有基类A和派生类B,C的成员,所以一旦派生类D想要调用基类A的成员(无论是数据成员还是成员函数)时,此时派生类是通过派生类B来访问基类A的成员呢?还是通过派生类C来访问基类A的成员呢?所以这时候编译器就会抱怨,我到底是通过B类访问,还是通过C类访问呢?这就造成了二义性。那么解决这个问题最好的办法就是使用虚继承。
直接看下面的代码来分析。
class Base_A
{
public:
Base_A():m_base_a("Base A") {}
virtual ~Base_A() {}
void printA() { cout << this->m_base_a << endl; }
private:
string m_base_a;
};
class Derived_B :public Base_A
{
public:
Derived_B():m_derived_b("Derived B") {}
virtual ~Derived_B() {}
void printB() { cout << m_derived_b << endl; }
private:
string m_derived_b;
};
class Derived_C :public Base_A
{
public:
Derived_C():m_derived_c("Derived C") {}
virtual~Derived_C() {}
void printC() { cout << m_derived_c << endl; }
private:
string m_derived_c;
};
class Derived_D :public Derived_B, public Derived_C
{
public:
Derived_D():m_derived_d("Derived D") {}
virtual ~Derived_D() {}
void printD() { cout << m_derived_d << endl; }
private:
string m_derived_d;
};
int main()
{
Derived_D D;
D.printA();
return 0;
}
当主函数先声明一个派生类D后,直接调用printA()函数,但是编译器会直接报错,错误显示调用printA()函数不明确。
如果把上面主函数中的代码修改如下,那么编译就可以顺利通过,程序可以正确运行。
D.Base_A::printA();
虽然这样可以解决调用的二义性错误的问题,但是这里还有一个问题:Derived_D类中存在Base_A中两份相同的成员变量和成员函数,会造成一定的空间浪费。而利用虚继承不仅可以解决二义性问题,还可以节省空间。下面我们可以来讨论没有实行虚继承之后和实行虚继承之后的内存空间分布问题。
1.2 虚继承的示例
class Base_A
{
public:
Base_A():m_base_a("Base A") {}
virtual ~Base_A() {}
//~Base_A() {}
void printA() { cout << this->m_base_a << endl; }
private:
string m_base_a;
};
class Derived_B :virtual public Base_A
{
public:
Derived_B():m_derived_b("Derived B") {}
virtual ~Derived_B() {}
void printB() { cout << m_derived_b << endl; }
private:
string m_derived_b;
};
class Derived_C :virtual public Base_A
{
public:
Derived_C():m_derived_c("Derived C") {}
virtual~Derived_C() {}
void printC() { cout << m_derived_c << endl; }
private:
string m_derived_c;
};
class Derived_D :public Derived_B, public Derived_C
{
public:
Derived_D():m_derived_d("Derived D") {}
virtual ~Derived_D() {}
void printD() { cout << m_derived_d << endl; }
private:
string m_derived_d;
};
int main()
{
Derived_D D;
D.printA();
cout << sizeof(Base_A) << endl;
cout << sizeof(Derived_B) << endl;
cout << sizeof(Derived_C) << endl;
cout << sizeof(Derived_D) << endl;
return 0;
}
2、虚继承的内存分布问题
2.1 普通继承的内存分布
承接上面各个类的定义和声明,接下来求取各个类所占的空间大小。如果类中存在虚函数的话,那么会产生4个字节的虚指针,这个虚指针指向的是一个虚函数表。这个虚函数表是用来存放虚函数的,无论类中有多少个虚函数,也只会产生一个虚函数表指针。
cout << sizeof(Base_A) << endl;//虚函数表指针:4个字节;去掉虚析构函数后,Base_A占有28个字节。
cout << sizeof(Derived_B) << endl;//虽然Derived_B的内存空间中有Base_A的拷贝,占有28个字节;本身也占有28个字节,再加上4个字节的虚指针
cout << sizeof(Derived_C) << endl;//Derived_C和Derived_B的内存空间分布相似
cout << sizeof(Derived_D) << endl;//Derived_D中Derived_B和Derived_C各占60字节,再加上自己去除虚函数后占有28个字节
得到的结果如下:
在这里很容易会弄混。虚函数表指针需要特别注意下,无论类里有多少个虚函数,只会有一个虚函数表指针。所以Derived_B、Derived_C和Derived_D都继承了Base_A里的虚析构函数,同时自己有虚析构函数,但是却只有一个虚函数指针表(占4个字节)。
2.2 虚继承的内存分布
cout << sizeof(Base_A) << endl;
cout << sizeof(Derived_B) << endl;
cout << sizeof(Derived_C) << endl;
cout << sizeof(Derived_D) << endl;
对应的结果如下图:
其中Base_A的内存空间的大小依旧是32。而Derived_B的大小由本来的60转换为了64,所增加的4个字节是由于虚继承后,会定义一个指针,指向从由基类赋值过来的内容。同理,Derived_C也会产生4个字节的指针指向赋值过来的内容。而由于Derived_D继承了Derived_B和Derived_C,所以就会把相应的内容会给到Derived_D的内存空间中,这样节省了空间。因为虚继承之后会通过指针共享所复制的内容。所以这就是为什么通过虚继承之后,调用基类Base_A的成员函数时,不会产生二义性的错误。编译器通过指针可以找到所共享的内容,所以不会有这种错误发生。