C++:钻石继承与虚继承
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++:钻石继承与虚继承的更多相关文章
- C++_day8_ 多重继承、钻石继承和虚继承
1.继承的复习 1.1 类型转换 编译器认为访问范围缩小是安全的. 1.2 子类的构造与析构 子类中对基类构造函数初始化只能写在初始化表里,不能写在函数体中. 阻断继承. 1.3 子类的拷贝构造与拷贝 ...
- C++对象模型:单继承,多继承,虚继承
什么是对象模型 有两个概念可以解释C++对象模型: 语言中直接支持面向对象程序设计的部分.对于各种支持的底层实现机制. 类中成员分类 数据成员分为静态和非静态,成员函数有静态非静态以及虚函数 clas ...
- C++ 多继承和虚继承的内存布局(转)
转自:http://www.oschina.net/translate/cpp-virtual-inheritance 警告. 本文有点技术难度,需要读者了解C++和一些汇编语言知识. 在本文中,我们 ...
- C++ 继承之虚继承与普通继承的内存分布
仅供互相学习,请勿喷,有观点欢迎指出~ class A { virtual void aa(){}; }; class B : public virtual A { ]; //加入一个变量是为了看清楚 ...
- C++在单继承、多继承、虚继承时,构造函数、复制构造函数、赋值操作符、析构函数的执行顺序和执行内容
一.本文目的与说明 1. 本文目的:理清在各种继承时,构造函数.复制构造函数.赋值操作符.析构函数的执行顺序和执行内容. 2. 说明:虽然复制构造函数属于构造函数的一种,有共同的地方,但是也具有一定的 ...
- 转载:C++ 多继承和虚继承的内存布局
C++ 多继承和虚继承的内存布局[已翻译100%] 英文原文:Memory Layout for Multiple and Virtual Inheritance 标签: <无> run_ ...
- C++对象模型:单继承,多继承,虚继承,菱形虚继承,及其内存布局图
C++目前使用的对象模型: 此模型下,nonstatic数据成员被置于每一个类的对象中,而static数据成员则被置于类对象之外,static和nonstatic函数也都放在类对象之外(通过函数指针指 ...
- C++ 的多继承与虚继承
C++之多继承与虚继承 1. 多继承 1.1 多继承概念 一个类有多个直接基类的继承关系称为多继承 多继承声明语法 class 派生类名 : 访问控制 基类名1, 访问控制 基类名2, ... { ...
- 2014 0416 word清楚项目黑点 输入矩阵 普通继承和虚继承 函数指针实现多态 强弱类型语言
1.word 如何清除项目黑点 选中文字区域,选择开始->样式->全部清除 2.公式编辑器输入矩阵 先输入方括号,接着选择格式->中间对齐,然后点下面红色框里的东西,组后输入数据 ...
随机推荐
- 自己写的开源MVC-easyMVC分享
简介 基本风格是按照spring mvc做的,在后期会加入一些新的特性,封装成易于自己项目使用的mvc框架. github地址: https://github.com/tangyanbo/easymv ...
- BZOJ3251:树上三角形(乱搞)
Description 给定一大小为n的有点权树,每次询问一对点(u,v),问是否能在u到v的简单路径上取三个点权,以这三个权值为边长构成一个三角形.同时还支持单点修改. Input 第一行两个整数n ...
- Odoo Model内容详解
转载请注明原文地址:https://www.cnblogs.com/cnodoo/p/9390688.html 一:Odoo模型属性 1:_name 模型的唯一标识:如果没有继承其他模型 ...
- linux 的常用命令---------第五阶段
计划任务 计划任务的作用:做一些周期性的任务,在生产中的主要用来定期备份数据. 计划任务分类 :1. 一次性的任务 仅执行一次就完成了: 使用命令 at 2. 周期性的任务 定期自动执行完成 ...
- Jenkins Extended E-mail Notification 2个注意事项:
1.Use SMTP AUTHentication 下 user name 所定义的邮箱,必须和全局配置的管理邮箱一直,否则会报错: Error sending to the following VA ...
- [转]OpenGL通过VBO实现顶点数组绘制顶点
#include "stdlib.h" #include <OpenGL/glext.h> #include <GLUT/GLUT.h> #define B ...
- opencv——对象计数
思路: 1.通过形态学操作.阈值处理.距离变换等方法,使得各个轮廓分开 2.计算轮廓数量 #include <opencv2/opencv.hpp> #include <iostr ...
- win10家庭版安装DockerToolbox-18.03.0-ce
下载DockerToolbox-18.03.0-ce.exe https://mirrors.aliyun.com/docker-toolbox/windows/docker-toolbox/ 点击安 ...
- jqgrid 让隐藏的列在编辑状态时出现且可编辑
有时,我们需要隐藏一个列数据,但在启动编辑时又能够被编辑. 1.设置列为编辑:editable: true 2.设置 editrules属性值为: edithidden: true colModel: ...
- 20155330 《网络对抗》 Exp7 网络欺诈防范
20155330 <网络对抗> Exp7 网络欺诈防范 基础问题回答 通常在什么场景下容易受到DNS spoof攻击 连接局域网的时,特别是在商场或是别的公众较多的场合连接的免费WIFI. ...