虚拟继承是C++语言中一个非常重要但是又比较生僻的存在,它的定义非常简单,但是对于理解C++的继承机制却是非常有用的。笔者最近学习过程中发现对C++的虚拟继承不是很明朗,故在这里对虚继承做个小结。
首先说下遇到的问题吧。代码如下(代码来自于何海涛《程序员面试精选100题第32题)。意图是要设计一个不能被继承的类,类似java中的final。但是又可以生成栈对象,可以像一般的C++类一样使用。
#include<iostream>
using std::endl;
using std::cout;
template <class T> class MakeFinal
{
friend T;
private:
MakeFinal()
{
cout<<"in MakeFinal"<<endl;
}
~MakeFinal(){}
};
class FinalClass: virtual public MakeFinal<FinalClass>
{
public:
FinalClass()
{
cout<<"in FinalClass"<<endl;
}
~FinalClass(){}
};
class Try: public FinalClass
{
public:
Try()
{
cout<<"in Try"<<endl;
}
};
这样的确使得FinalClass不能被继承了,原因在于类FinalClass是从类MakeFinal<Final>虚继承过来的,在调用Try的构造函数的时候,会直接跳过FinalClass而直接调用MakeFinal<FinalClass>的构造函数。而Try不是MakeFinal<Final>的友元,所以这里就会出现编译错误。但是如果把虚继承改成一般的继承,这里就没什么问题了。笔者对这里的调用顺序不是很明朗,为了对虚继承有彻底的了解,故做个小结。将从下面几个方向进行总结:1、为何要有虚继承;2、虚继承对于类的对象布局的影响;3、虚基类对构造函数的影响;
1、为什么需要虚继承
由于C++支持多重继承,那么在这种情况下会出现重复的基类这种情况,也就是说可能出现将一个类两次作为基类的可能性。比如像下面的情况
#include<iostream>
using std::cout;
using std::endl;
class Base
{
protected:
int value;
public:
Base()
{
cout<<"in Base"<<endl;
}
};
class DerivedA:protected Base
{
public:
DerivedA()
{
cout<<"in DerivedA"<<endl;
}
};
class DerivedB: protected Base
{
public:
DerivedB()
{
cout<<"in DerivedB"<<endl;
}
};
class MyClass:DerivedA,DerivedB
{
public:
MyClass()
{
cout<<"in MyClass"<<value<<endl;
}
};
编译时的错误如下
这中情况下会造成在MyClass中访问value时出现路径不明确的编译错误,要访问数据,就需要显示地加以限定。变成DerivedA::value或者DerivedB::value,以消除歧义性。并且,通常情况下,像Base这样的公共基类不应该表示为两个分离的对象,而要解决这种问题就可以用虚基类加以处理。如果使用虚继承,编译便正常了,类的结构示意图便如下。
虚继承的特点是,在任何派生类中的virtual基类总用同一个(共享)对象表示,正是如上图所示。
2、虚继承对类的对象布局的影响
要理解多重继承情况中重复基类时为什么会出现访问路径不明确的编译错误,需要了解继承中类对象在内存中的布局。在C++继承中,子类会继承父类的成员变量,因此在子类对象在内存中会包括来自父类的成员变量。实例代码如下,输出结果表明了每个对象在内存中所占的大小。
#include<iostream>
using std::cout;
using std::endl;
class Base
{
protected:
int value;
public:
Base()
{
//cout<<"in Base"<<endl;
}
};
class DerivedA:protected Base
{
protected:
int valueA;
public:
DerivedA()
{
//cout<<"in DerivedA"<<endl;
}
};
class DerivedB: protected Base
{
protected:
int valueB;
public:
DerivedB()
{
//cout<<"in DerivedB"<<endl;
}
};
class MyClass:DerivedA
{
private:
int my_value;
public:
MyClass()
{
//cout<<"in MyClass"<<value<<endl;
}
};
int main()
{
Base base_obj;
DerivedA derA_obj;
MyClass my_obj;
cout<<"size of Base object "<<sizeof(base_obj)<<endl;
cout<<"size of DerivedA object "<<sizeof(derA_obj)<<endl;
cout<<"size of MyClass object "<<sizeof(my_obj)<<endl;
}
输出结果如下
从类的定义结合这里的输出便不难明白,在子类对象中是包含了父类数据的,即在C++继承中,一个子类的object所表现出来的东西,是其自己的members加上其基类的member的总和。示意图如下(这里只讨论非静态变量)
在单继承的时候,访问相关的数据成员时,只需要使用名字即可。但是,在多重继承时,情况会变得复杂。因为重复基类中,在子类中变量名是相同的。这时,如果直接使用名字去访问,便会出现歧义性。看下面的代码以及对应的输出
#include<iostream>
using std::cout;
using std::endl;
class Base
{
protected:
int value;
public:
Base()
{
//cout<<"in Base"<<endl;
}
};
class DerivedA:protected Base
{
protected:
int valueA;
public:
DerivedA()
{
//cout<<"in DerivedA"<<endl;
}
};
class DerivedB: protected Base
{
protected:
int valueB;
public:
DerivedB()
{
//cout<<"in DerivedB"<<endl;
}
};
class MyClass:DerivedA,DerivedB
{
private:
int my_value;
public:
MyClass()
{
//cout<<"in MyClass"<<value<<endl;
}
};
int main()
{
Base base_obj;
DerivedA derA_obj;
MyClass my_obj;
cout<<"size of Base object "<<sizeof(base_obj)<<endl;
cout<<"size of DerivedA object "<<sizeof(derA_obj)<<endl;
cout<<"size of MyClass object "<<sizeof(my_obj)<<endl;
}
输出如下
代码的变化之处在于MyClass同时继承了DerivedA和DerivedB。而my_obj在内存中的大小变成了20,比之前大了8.正好是增加了继承至DerivedB中的数据部分的大小。上面情况中,my_obj在内存中的布局示意图如下
从图中可以看到,来自Base基类的数据成员value重复出现了两次。这也正是为什么在MyClass中直接访问value时会出现访问不明确的问题了。
那么使用虚继承后,对象的数据在内存中的布局又是什么样子呢?按照预测,既然在my_obj中只有一份来自Base的value,那么大小是否就是16呢?
代码及输出如下
#include<iostream>
using std::cout;
using std::endl;
class Base
{
protected:
int value;
public:
Base()
{
//cout<<"in Base"<<endl;
}
};
class DerivedA:protected virtual Base
{
protected:
int valueA;
public:
DerivedA()
{
//cout<<"in DerivedA"<<endl;
}
};
class DerivedB: protected virtual Base
{
protected:
int valueB;
public:
DerivedB()
{
//cout<<"in DerivedB"<<endl;
}
};
class MyClass:DerivedA,DerivedB
{
private:
int my_value;
public:
MyClass()
{
//cout<<"in MyClass"<<value<<endl;
}
};
int main()
{
Base base_obj;
DerivedA derA_obj;
DerivedB derB_obj;
MyClass my_obj;
cout<<"size of Base object "<<sizeof(base_obj)<<endl;
cout<<"size of DerivedA object "<<sizeof(derA_obj)<<endl;
cout<<"size of DerivedB object "<<sizeof(derB_obj)<<endl;
cout<<"size of MyClass object "<<sizeof(my_obj)<<endl;
};
输出结果如下
可以看到,DerivedA和DerivedB对象的大小变成了12,而MyClass对象的大小则变成了24.似乎大大超出了我们的预料。这其实是由于编译器在其中插入了一些东西用来寻找这个共享的基类数据所用而造成的。(来自《深度探索C++对象模型》第3章 侯捷译)这样理解,Class如果内含一个或多个虚基类子对象,那么将被分割为两部分:一个不变部分和一个共享部分。不变局部中的数据,不管后继如何衍化,总是拥有固定的offset,所以这一部分数据可以直接存取。至于共享局部,所表现的就是虚基类子对象。根据编译其的不同,会有不同的方式去得到这部分的数据,但总体来说都是需要有一个指向这部分共享数据的指针。
示意图如下
当然实际编译器使用的技术比这个要复杂,这里就不做详细讨论了。感兴趣的朋友可以参见《深入探索C++对象模型》
3、虚继承对构造函数的影响
对于构造函数的影响,借助于下面的原则可以理解(来自《深入理解C++对象模型》)
构造函数的调用可能内带大量的隐藏码,因为编译器会对构造函数进行扩充,一般而言编译器所作的扩充操作大约如下:
1、记录在成员初始化列表中的数据成员的初始化操作会被放到构造函数本身中,按照数据成员声明的顺序
2、如果有一个数据成员没有出现在初始化列表中,但是它有一个默认构造函数,那么这个默认构造函数会被调用
3、在那之前,如果有虚函数表,会调整虚函数表指针
4、在那之前,会对上一层基类的构造函数进行调用
5、在那之前,所有虚基类的构造函数必须被调用,按照声明的继承顺序从左往右,从最深到最浅的顺序
从上面的规则可以看出,对于虚基类的构造函数的调用是放在最前面的,并且需要子类对于虚基类的构造函数拥有访问权限
从下面的示例代码可以看出
#include<iostream>
using std::cout;
using std::endl;
class Base
{
protected:
int value;
public:
Base()
{
cout<<"in Base"<<endl;
}
};
class DerivedA:protected Base
{
protected:
int valueA;
public:
DerivedA()
{
cout<<"in DerivedA"<<endl;
}
};
class DerivedB
{
protected:
int valueB;
public:
DerivedB()
{
cout<<"in DerivedB"<<endl;
}
};
class TestClass
{
public:
TestClass()
{
cout<<"in TestClass"<<endl;
}
};
class MyClass:DerivedA,virtual DerivedB
{
private:
int my_value;
TestClass testData;
public:
MyClass()
{
//cout<<"in MyClass"<<value<<endl;
}
};
int main()
{
/*
Base base_obj;
DerivedA derA_obj;
DerivedB derB_obj;
MyClass my_obj;
cout<<"size of Base object "<<sizeof(base_obj)<<endl;
cout<<"size of DerivedA object "<<sizeof(derA_obj)<<endl;
cout<<"size of DerivedB object "<<sizeof(derB_obj)<<endl;
cout<<"size of MyClass object "<<sizeof(my_obj)<<endl;*/
MyClass my_obj;
}
代码运行后的效果如下所示
虽然在声明继承顺序的时候DerivedA的顺序是在DerivedB的前面的,但是由于DerivedB是虚拟继承,所以对DerivedB的调用会在最前。但是如果将DerivedA继承也改成虚继承,那么调用顺序就会发生变化。并且具体DerivedA的构造函数的调用与DerivedB的构造函数的调用顺序还与是MyClass虚继承DerivedA还是DerivedA虚继承Base有关。还是用代码来说明
MyClass虚继承DerivedA的情况,由于DerivedA声明在DerivedB前面,并且都是虚继承,所以先调用DerivedA的构造函数。但DerivedA的父类是Base,所以具体的构造函数的调用顺序是Base、DerivedA、DerivedB、TestClass。这种情况代码如下
#include<iostream>
using std::cout;
using std::endl;
class Base
{
protected:
int value;
public:
Base()
{
cout<<"in Base"<<endl;
}
};
class DerivedA:protected Base
{
protected:
int valueA;
public:
DerivedA()
{
cout<<"in DerivedA"<<endl;
}
};
class DerivedB
{
protected:
int valueB;
public:
DerivedB()
{
cout<<"in DerivedB"<<endl;
}
};
class TestClass
{
public:
TestClass()
{
cout<<"in TestClass"<<endl;
}
};
class MyClass:virtual DerivedA,virtual DerivedB
{
private:
int my_value;
TestClass testData;
public:
MyClass()
{
//cout<<"in MyClass"<<value<<endl;
}
};
int main()
{
/*
Base base_obj;
DerivedA derA_obj;
DerivedB derB_obj;
MyClass my_obj;
cout<<"size of Base object "<<sizeof(base_obj)<<endl;
cout<<"size of DerivedA object "<<sizeof(derA_obj)<<endl;
cout<<"size of DerivedB object "<<sizeof(derB_obj)<<endl;
cout<<"size of MyClass object "<<sizeof(my_obj)<<endl;*/
MyClass my_obj;
}
输出如下
但如果将虚继承放在由DerivedA虚继承Base,而MyClass非虚继承DerivedA。那么按照从左往右,从深到浅的顺序,调用顺序应该是Base、DerivedB、DerivedA、TestClass
,代码示例如下
#include<iostream>
using std::cout;
using std::endl;
class Base
{
protected:
int value;
public:
Base()
{
cout<<"in Base"<<endl;
}
};
class DerivedA:protected virtual Base
{
protected:
int valueA;
public:
DerivedA()
{
cout<<"in DerivedA"<<endl;
}
};
class DerivedB
{
protected:
int valueB;
public:
DerivedB()
{
cout<<"in DerivedB"<<endl;
}
};
class TestClass
{
public:
TestClass()
{
cout<<"in TestClass"<<endl;
}
};
class MyClass: DerivedA,virtual DerivedB
{
private:
int my_value;
TestClass testData;
public:
MyClass()
{
//cout<<"in MyClass"<<value<<endl;
}
};
int main()
{
/*
Base base_obj;
DerivedA derA_obj;
DerivedB derB_obj;
MyClass my_obj;
cout<<"size of Base object "<<sizeof(base_obj)<<endl;
cout<<"size of DerivedA object "<<sizeof(derA_obj)<<endl;
cout<<"size of DerivedB object "<<sizeof(derB_obj)<<endl;
cout<<"size of MyClass object "<<sizeof(my_obj)<<endl;*/
MyClass my_obj;
}
输出如下
这里还有一个问题是,左右顺序和深浅顺序该如何抉择呢?答案是先左右,再深浅。比如将上面的代码改为class MyClas: virutal DerivedB, DerivedA{...}
那么最后的调用顺序就是DerivedB,Base, DerivedA,TestClass。读者可以自己验证。