C++:钻石继承与虚继承
QUESTION:什么是钻石继承?
ANSWER:假设我们已经有了两个类Father1和Father2,他们都是类GrandFather的子类。现在又有一个新类Son,这个新类通过多继承机制对类Father1和Father2都进行了继承,此时类GrandFather、Father1、Father2和Son的继承关系是一个菱形,仿佛一个钻石,因此这种继承关系在C++中通常被称为钻石继承(或菱形继承)。
示意图:
示例:
1 #include<iostream> 2 using namespace std; 3 class GrandFather{ //第一层基类GrandFather 4 public: 5 GrandFather()=default; 6 GrandFather(int v):value(v){} 7 int value; 8 }; 9 10 class Father1:public GrandFather{ //第二层基类Father1 11 public: 12 Father1()=default; 13 Father1(int v):GrandFather(v){} 14 void set_value(int value){ //设置value的值 15 this->value=value; 16 } 17 }; 18 19 class Father2:public GrandFather{ //第二层基类Father2 20 public: 21 Father2()=default; 22 Father2(int v):GrandFather(v){} 23 int get_value(){ //获取value的值 24 return this->value; 25 } 26 }; 27 28 class Son:public Father1,public Father2{ //第三次层类Son 29 public: 30 Son()=default; 31 Son(int v):Father1(v),Father2(v){} 32 }; 33 34 int main(){ 35 Son s(10); 36 s.set_value(20); 37 cout<<s.get_value()<<endl; 38 return 0; 39 }
QUESTION:上例中明明将对象s的value值设置成了20,为什么最终value的输出却还是初始化值10?
ANSWER:解决这个问题我们首先需要知道对象s是如何构造的,还是上面的示例:
1 #include<iostream> 2 using namespace std; 3 class GrandFather{ //第一层基类GrandFather 4 public: 5 GrandFather()=default; 6 GrandFather(int v):value(v){ 7 cout<<"调用了GrandFather类的构造函数"<<endl; 8 } 9 int value; 10 }; 11 12 class Father1:public GrandFather{ //第二层基类Father1 13 public: 14 Father1()=default; 15 Father1(int v):GrandFather(v){ 16 cout<<"调用Father1类的构造函数"<<endl; 17 } 18 void set_value(int v){ //设置value的值 19 this->value=v; 20 } 21 }; 22 23 class Father2:public GrandFather{ //第二层基类Father2 24 public: 25 Father2()=default; 26 Father2(int v):GrandFather(v){ 27 cout<<"调用Father2类的构造函数"<<endl; 28 } 29 int get_value(){ //获取value的值 30 return this->value; 31 } 32 }; 33 34 class Son:public Father1,public Father2{ //第三次子类Son 35 public: 36 Son()=default; 37 Son(int v):Father1(v),Father2(v){ 38 cout<<"调用Son类的构造函数"<<endl; 39 } 40 }; 41 42 int main(){ 43 Son s(10); 44 s.set_value(20); 45 cout<<s.get_value()<<endl; 46 return 0; 47 }
我们发现在创建类Son的对象s时,第一层基类GrandFather的构造函数被调用了两次,这说明系统在创建对象s前会先创建两个独立的基类子对象(分别是Father1的对象和Father2的对象),然后再创建包含这两个子对象的对象s,如图:
由此可见,对象s中包含两个分属于不同子对象的成员变量value。而方法set_value()和方法get_value()虽然都是对象s的成员函数,但由于其也分属于对象s中的不同子对象,故其操作所针对的成员变量value不是同一个value,而是方法所在的子对象所包含的value,即上例中方法set_value()的功能是重新设置Father1类所创建的子对象的value值,而方法get_value()是返回Father2类所创建的子对象的value值。
1 int main(){ 2 Son s(10); 3 s.set_value(20); 4 cout<<"Father1类创建的子对象的value值:"<<s.Father1::value<<endl; 5 cout<<"Father2类创建的子对象的value值:"<<s.Father2::value<<endl; 6 return 0; 7 }
QUESTION:如何解决钻石继承中存在的“数据不一致”问题?
ANSWER:在C++中通常利用虚基类和虚继承来解决钻石继承中的“数据不一致”问题
特别注意:
1.什么是虚继承和虚基类
• 虚继承:在继承定义中包含了virtual关键字的继承关系
• 虚基类:在虚继承体系中通过关键字virtual继承而来的基类
2.为什么使用虚基类和虚继承
• 使用虚基类和虚继承可以让一个指定的基类在继承体系中将其成员数据实例共享给从该基类直接或间接派生出的其它类,即使从不同路径继承来的同名数据成员在内存中只有一个拷贝,同一个函数名也只有一个映射
1 #include<iostream> 2 using namespace std; 3 class GrandFather{ //第一层基类GrandFather 4 public: 5 GrandFather()=default; 6 GrandFather(int v):value(v){ 7 cout<<"调用了GrandFather类的构造函数"<<endl; 8 } 9 int value; 10 }; 11 12 class Father1:virtual public GrandFather{ //第二层基类Father1,虚继承基类GrandFather 13 public: 14 Father1()=default; 15 Father1(int v):GrandFather(v){ 16 cout<<"调用Father1类的构造函数"<<endl; 17 } 18 void set_value(int value){ //设置value的值 19 this->value=value; 20 } 21 }; 22 23 class Father2:virtual public GrandFather{ //第二层基类Father2,虚继承基类GrandFather 24 public: 25 Father2()=default; 26 Father2(int v):GrandFather(v){ 27 cout<<"调用Father2类的构造函数"<<endl; 28 } 29 int get_value(){ //获取value的值 30 return this->value; 31 } 32 }; 33 34 class Son:public Father1,public Father2{ //第三次子类Son 35 public: 36 Son()=default; 37 Son(int v):Father1(v),Father2(v),GrandFather(v) { 38 cout<<"调用Son类的构造函数"<<endl; 39 } 40 }; 41 42 int main(){ 43 Son s(10); 44 s.set_value(20); 45 cout<<s.get_value()<<endl; 46 return 0; 47 }
上例中的钻石继承中,由于基类Father1和基类Father2采用虚继承的方式来继承类GrandFather,此时对象s中类Father1和类Father2创建的子对象共享GrandFather类创建的子对象,如图:
此时对象s中成员变量value只有一个,且被Father1类创建的子对象和Father2类创建的子对象所共享,即方法set_value()和方法get_value()操作的value是同一个成员变量。
3.构造函数的调用顺序
• 首先按照虚基类的声明顺序调用虚基类的构造函数
• 然后按照非虚基类的声明顺序调用非虚基类的构造函数
• 之后调用派生类中成员对象的构造函数
• 最后调用派生类自己的构造函数
示例:
1 #include<iostream> 2 using namespace std; 3 class One{ 4 public: 5 int one; 6 One(int o):one(o){ 7 cout<<"调用类One的构造函数"<<endl; 8 } 9 }; 10 11 class Two{ 12 public: 13 int two; 14 Two(int t):two(t){ 15 cout<<"调用类Two的构造函数"<<endl; 16 } 17 }; 18 19 class Three{ 20 public: 21 int three; 22 Three(int t):three(t){ 23 cout<<"调用类Three的构造函数"<<endl; 24 } 25 }; 26 27 class Four{ 28 public: 29 Four(){ 30 cout<<"调用类Four的构造函数"<<endl; 31 } 32 }; 33 34 class Five{ 35 public: 36 int five; 37 Five(int f):five(f){ 38 cout<<"调用类Five的构造函数"<<endl; 39 } 40 }; 41 42 class Six:public One,virtual Two,virtual Three,public Five{ 43 public: 44 Six(int value):One(value),Two(value),Three(value) ,Five(value){ //在派生类的构造函数的成员初始化列表中必须列出对虚基类构造函数的调用 45 cout<<"调用类Six的构造函数"<<endl; 46 } 47 private: 48 Four four; 49 }; 50 51 int main(){ 52 Six six(10); 53 return 0; 54 }
4.使用虚基类和虚继承时的一些注意事项:
• 在派生类对象中,同名的虚基类只产生一个虚基类子对象,而同名的非虚基类则各产生一个非虚基类子对象
• 虚基类的子对象是由最后派生出来的类的构造函数通过调用虚基类的构造函数来初始化的。因此在派生类的构造函数的成员初始化列表中必须列出对虚基类构造函数的调用,如果没有列出,则表示使用该虚基类的默认构造函数。
• 虚基类并不是在声明基类时声明的,而是在声明派生类时通过指定其继承该基类的方式来声明的。