QUESTION:什么是钻石继承?

ANSWER:假设我们已经有了两个类Father1和Father2,他们都是类GrandFather的子类。现在又有一个新类Son,这个新类通过多继承机制对类Father1和Father2都进行了继承,此时类GrandFather、Father1、Father2和Son的继承关系是一个菱形,仿佛一个钻石,因此这种继承关系在C++中通常被称为钻石继承(或菱形继承)。

示意图:

示例:

 #include<iostream>
using namespace std;
class GrandFather{ //第一层基类GrandFather
public:
GrandFather()=default;
GrandFather(int v):value(v){}
int value;
}; class Father1:public GrandFather{ //第二层基类Father1
public:
Father1()=default;
Father1(int v):GrandFather(v){}
void set_value(int value){ //设置value的值
this->value=value;
}
}; class Father2:public GrandFather{ //第二层基类Father2
public:
Father2()=default;
Father2(int v):GrandFather(v){}
int get_value(){ //获取value的值
return this->value;
}
}; class Son:public Father1,public Father2{ //第三次层类Son
public:
Son()=default;
Son(int v):Father1(v),Father2(v){}
}; int main(){
Son s();
36 s.set_value(20);
37 cout<<s.get_value()<<endl;
return ;
}

QUESTION:上例中明明将对象s的value值设置成了20,为什么最终value的输出却还是初始化值10?

ANSWER:解决这个问题我们首先需要知道对象s是如何构造的,还是上面的示例:

 #include<iostream>
using namespace std;
class GrandFather{ //第一层基类GrandFather
public:
GrandFather()=default;
GrandFather(int v):value(v){
cout<<"调用了GrandFather类的构造函数"<<endl;
}
int value;
}; class Father1:public GrandFather{ //第二层基类Father1
public:
Father1()=default;
Father1(int v):GrandFather(v){
cout<<"调用Father1类的构造函数"<<endl;
}
void set_value(int v){ //设置value的值
this->value=v;
}
}; class Father2:public GrandFather{ //第二层基类Father2
public:
Father2()=default;
Father2(int v):GrandFather(v){
cout<<"调用Father2类的构造函数"<<endl;
}
int get_value(){ //获取value的值
return this->value;
}
}; class Son:public Father1,public Father2{ //第三次子类Son
public:
Son()=default;
Son(int v):Father1(v),Father2(v){
cout<<"调用Son类的构造函数"<<endl;
}
}; int main(){
Son s();
s.set_value();
cout<<s.get_value()<<endl;
return ;
}

我们发现在创建类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值

 int main(){
Son s();
s.set_value();
4 cout<<"Father1类创建的子对象的value值:"<<s.Father1::value<<endl;
5 cout<<"Father2类创建的子对象的value值:"<<s.Father2::value<<endl;
return ;
}

QUESTION:如何解决钻石继承中存在的“数据不一致”问题?

ANSWER:在C++中通常利用虚基类和虚继承来解决钻石继承中的“数据不一致”问题

特别注意:

1.什么是虚继承和虚基类

• 虚继承:在继承定义中包含了virtual关键字的继承关系

• 虚基类:在虚继承体系中通过关键字virtual继承而来的基类

2.为什么使用虚基类和虚继承

• 使用虚基类和虚继承可以让一个指定的基类在继承体系中将其成员数据实例共享给从该基类直接或间接派生出的其它类,即使从不同路径继承来的同名数据成员在内存中只有一个拷贝,同一个函数名也只有一个映射

 #include<iostream>
using namespace std;
class GrandFather{ //第一层基类GrandFather
public:
GrandFather()=default;
GrandFather(int v):value(v){
cout<<"调用了GrandFather类的构造函数"<<endl;
}
int value;
}; class Father1:virtual public GrandFather{ //第二层基类Father1,虚继承基类GrandFather
public:
Father1()=default;
Father1(int v):GrandFather(v){
cout<<"调用Father1类的构造函数"<<endl;
}
void set_value(int value){ //设置value的值
this->value=value;
}
}; class Father2:virtual public GrandFather{ //第二层基类Father2,虚继承基类GrandFather
public:
Father2()=default;
Father2(int v):GrandFather(v){
cout<<"调用Father2类的构造函数"<<endl;
}
int get_value(){ //获取value的值
return this->value;
}
}; class Son:public Father1,public Father2{ //第三次子类Son
public:
Son()=default;
Son(int v):Father1(v),Father2(v),GrandFather(v) {
cout<<"调用Son类的构造函数"<<endl;
}
}; int main(){
Son s();
44 s.set_value(20);
45 cout<<s.get_value()<<endl;
return ;
}

上例中的钻石继承中,由于基类Father1和基类Father2采用虚继承的方式来继承类GrandFather,此时对象s中类Father1和类Father2创建的子对象共享GrandFather类创建的子对象,如图:

此时对象s中成员变量value只有一个,且被Father1类创建的子对象和Father2类创建的子对象所共享,即方法set_value()和方法get_value()操作的value是同一个成员变量。

3.构造函数的调用顺序

• 首先按照虚基类的声明顺序调用虚基类的构造函数

• 然后按照非虚基类的声明顺序调用非虚基类的构造函数

• 之后调用派生类中成员对象的构造函数

• 最后调用派生类自己的构造函数

示例:

 #include<iostream>
using namespace std;
class One{
public:
int one;
One(int o):one(o){
cout<<"调用类One的构造函数"<<endl;
}
}; class Two{
public:
int two;
Two(int t):two(t){
cout<<"调用类Two的构造函数"<<endl;
}
}; class Three{
public:
int three;
Three(int t):three(t){
cout<<"调用类Three的构造函数"<<endl;
}
}; class Four{
public:
Four(){
cout<<"调用类Four的构造函数"<<endl;
}
}; class Five{
public:
int five;
Five(int f):five(f){
cout<<"调用类Five的构造函数"<<endl;
}
}; class Six:public One,virtual Two,virtual Three,public Five{
public:
Six(int value):One(value),Two(value),Three(value) ,Five(value){ //在派生类的构造函数的成员初始化列表中必须列出对虚基类构造函数的调用
cout<<"调用类Six的构造函数"<<endl;
}
private:
Four four;
}; int main(){
Six six();
return ;
}

4.使用虚基类和虚继承时的一些注意事项:

在派生类对象中,同名的虚基类只产生一个虚基类子对象,而同名的非虚基类则各产生一个非虚基类子对象

• 虚基类的子对象是由最后派生出来的类的构造函数通过调用虚基类的构造函数来初始化的。因此在派生类的构造函数的成员初始化列表中必须列出对虚基类构造函数的调用,如果没有列出,则表示使用该虚基类的默认构造函数。

• 虚基类并不是在声明基类时声明的,而是在声明派生类时通过指定其继承该基类的方式来声明的。

C++:钻石继承与虚继承的更多相关文章

  1. C++_day8_ 多重继承、钻石继承和虚继承

    1.继承的复习 1.1 类型转换 编译器认为访问范围缩小是安全的. 1.2 子类的构造与析构 子类中对基类构造函数初始化只能写在初始化表里,不能写在函数体中. 阻断继承. 1.3 子类的拷贝构造与拷贝 ...

  2. C++对象模型:单继承,多继承,虚继承

    什么是对象模型 有两个概念可以解释C++对象模型: 语言中直接支持面向对象程序设计的部分.对于各种支持的底层实现机制. 类中成员分类 数据成员分为静态和非静态,成员函数有静态非静态以及虚函数 clas ...

  3. C++ 多继承和虚继承的内存布局(转)

    转自:http://www.oschina.net/translate/cpp-virtual-inheritance 警告. 本文有点技术难度,需要读者了解C++和一些汇编语言知识. 在本文中,我们 ...

  4. C++ 继承之虚继承与普通继承的内存分布

    仅供互相学习,请勿喷,有观点欢迎指出~ class A { virtual void aa(){}; }; class B : public virtual A { ]; //加入一个变量是为了看清楚 ...

  5. C++在单继承、多继承、虚继承时,构造函数、复制构造函数、赋值操作符、析构函数的执行顺序和执行内容

    一.本文目的与说明 1. 本文目的:理清在各种继承时,构造函数.复制构造函数.赋值操作符.析构函数的执行顺序和执行内容. 2. 说明:虽然复制构造函数属于构造函数的一种,有共同的地方,但是也具有一定的 ...

  6. 转载:C++ 多继承和虚继承的内存布局

    C++ 多继承和虚继承的内存布局[已翻译100%] 英文原文:Memory Layout for Multiple and Virtual Inheritance 标签: <无> run_ ...

  7. C++对象模型:单继承,多继承,虚继承,菱形虚继承,及其内存布局图

    C++目前使用的对象模型: 此模型下,nonstatic数据成员被置于每一个类的对象中,而static数据成员则被置于类对象之外,static和nonstatic函数也都放在类对象之外(通过函数指针指 ...

  8. C++ 的多继承与虚继承

    C++之多继承与虚继承   1. 多继承 1.1 多继承概念 一个类有多个直接基类的继承关系称为多继承 多继承声明语法 class 派生类名 : 访问控制 基类名1, 访问控制 基类名2, ... { ...

  9. 2014 0416 word清楚项目黑点 输入矩阵 普通继承和虚继承 函数指针实现多态 强弱类型语言

    1.word 如何清除项目黑点 选中文字区域,选择开始->样式->全部清除 2.公式编辑器输入矩阵 先输入方括号,接着选择格式->中间对齐,然后点下面红色框里的东西,组后输入数据   ...

随机推荐

  1. Articulate Presenter文字乱码的排除

    Articulate Presenter乱码的问题如何设置? 字体乱码的设置: 1.首先如果ppt中有中文内容,肯定需要将Articulate Presenter的Character Set设置为No ...

  2. Scala学习之路 (五)Scala的关键字Lazy

    Scala中使用关键字lazy来定义惰性变量,实现延迟加载(懒加载). 惰性变量只能是不可变变量,并且只有在调用惰性变量时,才会去实例化这个变量. 在Java中,要实现延迟加载(懒加载),需要自己手动 ...

  3. 服务发现系统etcd之安装和使用

    一.概述 etcd是一个高可用的键值存储系统,主要用于共享配置和服务发现.etcd是由CoreOS开发并维护的,灵感来自于 ZooKeeper 和 Doozer,它使用Go语言编写,并通过Raft一致 ...

  4. 如何为Windows Forms应用程序添加启动参数(Start-Up Parameters)

    很多场合下,我们需要通过命令行或者快捷方式在Windows Forms程序启动时向其传递参数. 这些参数可能是用来加载某一个文档,或者是应用程序的初始化配置文件. 特别是对那些需要高度自定义配置的大程 ...

  5. Python2.7-csv

    csv模块,用于读写 csv 文件,常用 reader 和 writer 对象进行操作 1.模块的类 1.1 Dialect 对象,设置 csv 文件的各种格式,包括分隔符,引用符,转义符等 1.1. ...

  6. Xcode 备忘

    一. 打印一堆乱七八糟的东西: Edit Scheme... --> Run --> Arguments,在 Environment Variables 里添加 OS_ACTIVITY_M ...

  7. day78

    昨日回顾:  forms组件:   -校验数据(最重要)    -先定义一个类(继承Form)    -写一些要校验的字段(好多类型)    -字段(对象)有一些属性(最长多少,最短多少,是否必填,l ...

  8. MVC 拦截指定的action

    有时,我们需要在特定的一些aciton中做校验.比如:验证是否登录.实现方式有两种: 一.编写一个公共的方法专门用于实现是否登录的验证,然后在每个需要进行验证的aciton的头部去调用该方法,根据方法 ...

  9. C++面向对象模型

    1. 基础知识 C++编译器怎样完毕面向对象理论到计算机程序的转化? 换句话:C++编译器是怎样管理类.对象.类和对象之间的关系 详细的说:详细对象调用类写的方法,那,c++编译器是怎样区分,是那个详 ...

  10. net 表格控件

    一个开源的表格控件,界面像Excel,看来很好,有机会在项目中使用:ReoGrid https://reogrid.net/