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.公式编辑器输入矩阵 先输入方括号,接着选择格式->中间对齐,然后点下面红色框里的东西,组后输入数据 ...
随机推荐
- 2.Redis集群环境搭建
转载请出自出处:http://www.cnblogs.com/hd3013779515/ 一.基本概念 1.redis集群是一个可以在多个节点之间进行数据共享的设施.redis集群提供了以下两个好处1 ...
- 6、JVM--类文件结构(上)
6.1.概述 写的程序需要经编译器翻译成由0和1构成的二进制格式才能由计算机执行 6.2.无关性基石 Java在刚刚诞生之时曾经提出过一个非常著名的宣传口号:“一次编写,到处运行(Write Once ...
- CJOJ 【DP合集】最长上升序列2 — LIS2
题面 已知一个 1 ∼ N 的排列的最长上升子序列长度为 K ,求合法的排列个数. 好题(除了我想不出来我应该找不到缺点), 想一想最长上升子序列的二分做法, 接在序列后面或者替换. 所以对于每一个位 ...
- 2、Pyspider使用入门
1.接上一篇,在webui页面,点击右侧[Create]按钮,创建爬虫任务 2.输入[Project Name],[Start Urls]为爬取的起始地址,可以先不输入,点击[Create]进入: 3 ...
- cloudstack 创建虚拟机失败
Trying to find a potenial host and associated storage pools from the suitable host/pool lists for th ...
- lwip lwiperf 方法进行性能测试 4.5MB/S
硬件配置: STM32F407 + DP83848 + FreeRTOS V10.1.1 + LWIP 2.1.2 2018年12月5日14:31:24 1.先读取 PHY 寄存器 , 查看 自 ...
- 2017-2018-2 20155230《网络对抗技术》实验9:Web安全基础
实践过程记录 下载wegot并配置好java环境后 输入java -jar webgoat-container-7.0-SNAPSHOT-war-exec.jar 在浏览器输入localhost:80 ...
- 20155232《网络对抗》Exp7 网络欺诈防范
20155232<网络对抗>Exp7 网络欺诈防范 一.实践内容 本实践的目标理解常用网络欺诈背后的原理,以提高防范意识,并提出具体防范方法.具体实践有 (1)简单应用SET工具建立冒名网 ...
- Hibernate一对多关联关系保存时的探究
在以前使用hibernate时,经常对保存存在关联关系的对象时,不确定是否能保存成功. 因此,特意对一对多关系的2个对象进行实践. 一.pojo类和配置文件的准备 这里有一点提前 ...
- 连接到win2003的远程桌面,客户端要如何操作
第一步:命令行执行mstsc 第二步:处输入开启了远程桌面功能的计算机IP地址.