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.公式编辑器输入矩阵 先输入方括号,接着选择格式->中间对齐,然后点下面红色框里的东西,组后输入数据 ...
随机推荐
- leetcode 3. Longest Substring Without Repeating Characters [java]
idea: 设置一个hashset存储非重复元素 j:遍历s i:最近的一个非重复指针 注意点: 1.Set set = new HashSet<>(); add remove publi ...
- 金三银四求职季,前端面试题小梳理(HTML、CSS、JS)
好久没写学习记录,最近太多事,又到一年求职季,都说金三银四求职季,自己也做一下最近学习的一些前端面试题梳理,还是个小白,写的不对请指正,不胜感激. HTML篇 html语义化 用语义化的代码标签书写, ...
- VS2010自行编译OpenCV2.4.4时缺少python27_d.lib的解决方法
错误 24 error LNK1104: 无法打开文件“python27_d.lib” C:\OpenCV\VS2013_64\modules\python\LINK opencv_python 编 ...
- Error at offset之反序列化
关于PHP 序列化(serialize)和反序列化(unserialize)出现错误(Error at offset)的解决办法. 首先我们分析一下为什么会出现这个错误: 编码问题 UTF-8: AN ...
- PAT B1050 螺旋矩阵 (25 分)
本题要求将给定的 N 个正整数按非递增的顺序,填入“螺旋矩阵”.所谓“螺旋矩阵”,是指从左上角第 1 个格子开始,按顺时针螺旋方向填充.要求矩阵的规模为 m 行 n 列,满足条件:m×n 等于 N:m ...
- java环境配置针对win10(电脑重装必备) 最后一步很重要
jdk和jre都默认安装c盘. 系统变量→新建 JAVA_HOME 变量:变量值填写jdk的安装目录(本人是 C:\Program Files\Java\jdk1.8.0_131). 系统变量→新建 ...
- SNAT和DNAT
1.SNAT iptables防火墙 Centos6.6 64位 iptables 内网:eth0 172.16.4.1 外网:eth 112.112.112.112/24 当有用户访问公网时,修改用 ...
- ASP.NET Response.Redirect 丢失 Session的问题(作废,仅供参考)
以前在做ASP.NET开发时一直没注意到一个问题,就是广泛使用的Response.Redirect方法并不会将服务器端在Response中新增或修改的Cookie返回给客户端浏览器,而网站的Sessi ...
- 判断库位是否参与MRP运算
表 T001L 字段DISKZ (库存地点MRP标识)为空,参与MRP运算,为1不参与.
- 20155203 杜可欣《网络对抗技术》Exp1 PC平台逆向破解
1.1 实践目标 本次实践的对象是一个名为pwn1的linux可执行文件. 该程序正常执行流程是:main调用foo函数,foo函数会简单回显任何用户输入的字符串. 该程序同时包含另一个代码片段,ge ...