C++语言基础(10)-虚继承

时间:2021-07-09 20:47:24

一.产生背景

先看下列一份代码:

//间接基类A
class A{
protected:
int m_a;
}; //直接基类B
class B: public A{
protected:
int m_b;
}; //直接基类C
class C: public A{
protected:
int m_c;
}; //派生类D
class D: public B, public C{
public:
void seta(int a){ m_a = a; } //命名冲突
void setb(int b){ m_b = b; } //正确
void setc(int c){ m_c = c; } //正确
void setd(int d){ m_d = d; } //正确
private:
int m_d;
}; int main(){
D d;
return ;
}

运行:

C++语言基础(10)-虚继承

编译器报错:“reference to 'm_a' is ambiguous”,说明m_a变量指代不清,编译器不知道该为哪个m_a赋值,事实上,B和C均继承于A,所以B和C中均有m_a这个变量,此时在D中为m_a赋值,就搞不清楚究竟是给B中的m_a赋值还是给C中的m_a赋值了,为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。

二.具体使用

在继承方式前面加上 virtual 关键字就是虚继承,请看下面的例子:

//间接基类A
class A{
protected:
int m_a;
}; //直接基类B
class B: virtual public A{ //虚继承
protected:
int m_b;
}; //直接基类C
class C: virtual public A{ //虚继承
protected:
int m_c;
}; //派生类D
class D: public B, public C{
public:
void seta(int a){ m_a = a; } //正确
void setb(int b){ m_b = b; } //正确
void setc(int c){ m_c = c; } //正确
void setd(int d){ m_d = d; } //正确
private:
int m_d;
}; int main(){
D d;
return ;
}

虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。在本例中,B和C两个类已经作出声明,可以共享它们的基类A,所以A中的成员变量m_a,将永远只有一份,因此再次编译就不会报错了。

三.虚继承时的构造函数

先看下列代码:

#include <iostream>
using namespace std; //虚基类A
class A{
public:
A(int a);
protected:
int m_a;
};
A::A(int a): m_a(a){ } //直接派生类B
class B: virtual public A{
public:
B(int a, int b);
public:
void display();
protected:
int m_b;
};
B::B(int a, int b): A(a), m_b(b){ }
void B::display(){
cout<<"m_a="<<m_a<<", m_b="<<m_b<<endl;
} //直接派生类C
class C: virtual public A{
public:
C(int a, int c);
public:
void display();
protected:
int m_c;
};
C::C(int a, int c): A(a), m_c(c){ }
void C::display(){
cout<<"m_a="<<m_a<<", m_c="<<m_c<<endl;
} //间接派生类D
class D: public B, public C{
public:
D(int a, int b, int c, int d);
public:
void display();
private:
int m_d;
};
D::D(int a, int b, int c, int d): A(a), B(, b), C(, c), m_d(d){ }
void D::display(){
cout<<"m_a="<<m_a<<", m_b="<<m_b<<", m_c="<<m_c<<", m_d="<<m_d<<endl;
} int main(){
B b(, );
b.display(); C c(, );
c.display(); D d(, , , );
d.display();
return ;
}

运行结果:

m_a=10, m_b=20
m_a=30, m_c=40
m_a=50, m_b=60, m_c=70, m_d=80

d.display()输出的结果很奇怪,不应该是100,60,70,80么?其实这里面涉及到了一个问题:

子类D的构造函数中,既调用了B和C的构造函数,又调用了A的构造函数,之前子类的构造函数只负责初始化它的直接父类,再由直接父类的构造函数初始化间接父类,用户尝试调用间接父类的构造函数将导致错误,这怎么又调用A的构造函数?

问题的源头是这样的:

现在采用了虚继承,虚基类 A 在最终派生类 D 中只保留了一份成员变量 m_a,如果由 B 和 C 初始化 m_a,那么 B 和 C 在调用 A 的构造函数时很有可能给出不同的实参,这个时候编译器就会犯迷糊,不知道使用哪个实参初始化 m_a。

那么该如何解决呢?

为了避免出现这种矛盾的情况,C++ 干脆规定必须由最终的派生类 D 来初始化虚基类 A,直接派生类 B 和 C 对 A 的构造函数的调用是无效的。在第 50 行代码中,调用 B 的构造函数时试图将 m_a 初始化为 90,调用 C 的构造函数时试图将 m_a 初始化为 100,但是输出结果有力地证明了这些都是无效的,m_a 最终被初始化为 50,这正是在 D 中直接调用 A 的构造函数的结果。

因此得出虚继承时构造函数的执行顺序是这样的:

虚继承时构造函数的执行顺序与普通继承时不同:在最终派生类的构造函数调用列表中,不管各个构造函数出现的顺序如何,编译器总是先调用虚基类的构造函数,再按照出现的顺序调用其他的构造函数;而对于普通继承,就是按照构造函数出现的顺序依次调用的。

可以将上例中D的构造函数修改成这样:

运行,发现结果仍然是:

m_a=10, m_b=20
m_a=30, m_c=40
m_a=50, m_b=60, m_c=70, m_d=80

所以,虽然我们将 A() 放在了最后,但是编译器仍然会先调用 A(),然后再调用 B()、C(),因为 A() 是虚基类的构造函数,比其他构造函数优先级高。如果没有使用虚继承的话,那么编译器将按照出现的顺序依次调用 B()、C()、A()。

C++语言基础(10)-虚继承的更多相关文章

  1. C&plus;&plus;语言基础&lpar;12&rpar;-虚函数

    一.虚函数使用的注意事项 1.只需要在虚函数的声明处加上 virtual 关键字,函数定义处可以加也可以不加. 2.为了方便,你可以只将基类中的函数声明为虚函数,这样所有子类中具有遮蔽(覆盖)关系的同 ...

  2. C语言基础 &lpar;10&rpar; 变量作用域,生命周期 内存结构

    01 课程回顾 1.指针数组 注意: 对于数组来说,在使用sizeof的时候a和&a[0]是不一样的, 虽然以%x打印出来他们都是地址 2.值传递 int a; fun(a); int *** ...

  3. C语言基础&lpar;10&rpar;-数组

    一.数组的定义 数组就是在内存中连续的相同类型的变量空间. 二.数组在内存中的存储方式 同一个数组所有的成员都是相同的数据类型,同时所有的成员在内存中的地址是连续的,数组名是一个地址的常量,代表数组中 ...

  4. C语言基础10

    栈区间:在函数内部声明的变量都存放在栈区间,比如int char 数组 结构体 指针,只管申请,系统会自动帮我们回收,收回的时间是作用域结束之后,遵循的原则是"先进后出". int ...

  5. Java入门 - 语言基础 - 10&period;条件语句

    原文地址:http://www.work100.net/training/java-if-else.html 更多教程:光束云 - 免费课程 条件语句 序号 文内章节 视频 1 概述 2 if...e ...

  6. Java语言基础&lpar;10&rpar;

    1 方法(三) 案例:Demo1 import java.util.Scanner; public class Demo1 { static int min(int num1,int num2){ i ...

  7. C&plus;&plus;基础 &lpar;6&rpar; 第六天 继承 虚函数 虚继承 多态 虚函数

    继承是一种耦合度很强的关系 和父类代码很多都重复的 2 继承的概念 3 继承的概念和推演 语法: class 派生类:访问修饰符 基类 代码: … … 4 继承方式与访问控制权限 相对的说法: 爹派生 ...

  8. &lbrack;Coursera&rsqb;&lbrack;计算导论与C语言基础&rsqb;&lbrack;Week 10&rsqb;对于&OpenCurlyDoubleQuote;数组应用练习”课后习题的思考题的一些想法

    (首先,关于Honor Code,我咨询过了Help Center,分享课后练习的思考题的想法是可以的(注意不是代码),但要标明引用,引用格式来源于https://guides.lib.monash. ...

  9. C语言的10大基础算法

    C语言的10大基础算法 算法是一个程序和软件的灵魂,作为一名优秀的程序员,只有对一些基础的算法有着全面的掌握,才会在设计程序和编写代码的过程中显得得心应手.本文包括了经典的Fibonacci数列.简易 ...

随机推荐

  1. 高级智能研究计划(IARPA):大脑皮层建模

    哈哈,看到了一篇我最感兴趣的领域的新闻报导,可以深挖里面的各种细节. Quanta Magazine: Illuminating Science - 原文出处 卡内基·梅隆大学 - Tai Sing ...

  2. windows下Redis编译安装

    redis是现在比较流行的noSQL,主流大型网站都用的比较多,很多同学不知道怎么安装,这里介绍在windows下面安装以及扩展,提供学习使用,实际使用环境多在Linux下. 首先到相应网站下载red ...

  3. ssi服务器端指令

    SSI使用详解 你是否曾经或正在为如何能够在最短的时间内完成对一个包含上千个页面的网站的修改而苦恼?那么可以看一下本文的介绍,或许能够对你有所帮助.什么是SSI?SSI是英文Server Side I ...

  4. Heritrix源码分析&lpar;四&rpar; 各个类说明&lpar;转)

    Heritrix的类的确很繁琐,往往继承了一层又一层,最多的继承好像有7层.下面就一个包一个包的说明每个类的作用,由于里面Heritrix组件分明,很多组件没用到的同时该组件的类我也没怎么接触,所以这 ...

  5. 更改linux系统时间

    date  //查看时间 date -s '21:04:20 2014-06-06' //将时间设为21:04:20 2014-06-06

  6. 【LeetCode题意分析&amp&semi;解答】34&period; Search for a Range

    Given a sorted array of integers, find the starting and ending position of a given target value. You ...

  7. python中的魔法属性

    目录 1. __doc__ 2. __module__ 和 __class__ 3. __init__ 4. __del__ 5. __call__ 6. __dict__ 7. __str__ 8. ...

  8. PID控制器开发笔记之四:梯形积分PID控制器的实现

    从微积分的基本原理看,积分的实现是在无限细分的情况下进行的矩形加和计算.但是在离散状态下,时间间隔已经足够大,矩形积分在某些时候显得精度要低了一些,于是梯形积分被提出来以提升积分精度. 1.梯形积分基 ...

  9. python 基本数据类型 之 字符串

    字符串数据出现的意义 掌握字符串的定义和特性 能熟练掌握字符串常用操作,并了解其他工厂方法 字符串的定义和创建 字符串是一个有序的字符集合,用于存储和表示基本的文本信息,  用引号“          ...

  10. Git服务器的搭建和使用

    一.GIT服务器的搭建 1. 安装Git yum -y install git 2. 创建git用户 adduser git 3. 创建证书登陆   收集所有客户端需要登录的用户的公钥,就是他们自己的 ...