最近工作中某个软件功能出现了退化,追查下来发现是一个类的成员变量没有被正确的初始化。这个问题与C++存在虚继承的情况下派生类构造函数的写法有关。在此说明一下错误发生的原因,希望对更多的人有帮助。

我们代码中存在虚继承的类的继承结构与下图类似,并不是教科书中经典的菱形结构。从 Intermediate1 和 Intermediate3 到Base2 的继承是虚继承。Base1 和 Base2 包含一些成员变量,并提供了相应的构造函数接受指定的初始化值。Base2 还有一个缺省构造函数,把其成员变量都初始化为0。Intermediate1,2,3 也都提供了一个构造函数接受指定的初始化值,并在在初始化列表里调用Base1和Base2的构造函数完成初始化。

一位同事在做重构时,不小心把Final的代码改成了:

class Final : public Intermediate2, public Intermediate3 {
public:
Final (int a, int b, int c)
: Intermediate2(a, b, c),
Intermediate3(b, c)
{ } };
class Intermediate1 : public Base1, virtual public Base2 {
public:
Intermediate1(int a, int b, int c)
: Base1(a),
Base2(b, c)
{ }
}; class Intermediate2 : public Intermediate1 {
public:
Intermediate2(int a, int b, int c)
: Intermediate1(a, b, c),
Base2(b, c)
{ }
}; class Intermediate3 : virtual public Base2 {
public:
Intermediate3(int b, int c)
: Base2(b, c)
{ }
};

看上去,Final的构造函数将调用Intermediate2 和 Intermediate3的构造函数分别将m_a, m_b 和 m_c初始化成指定的值。可是,运行时发现m_b和m_c的值是0!明显,这是调用了Base2的缺省构造函数。

原来,C++的规则是:如果在继承链上存在虚继承的基类,则最底层的子类要负责完成该虚基类部分成员的构造。我们可以显式调用虚基类的构造函数完成初始化。如果不显式调用虚基类的构造函数,则编译器会调用虚基类的缺省构造函数。如果不显式调用虚基类的构造函数,而虚基类没有定义缺省构造函数,则会出现编译错误。这条规则的原因是:如果不这样做,则虚基类部分会在存在的多个继承链条上被多次初始化。

很多时候,对于继承链上的中间类,我们也会在其构造函数中显式调用虚基类的构造函数,因为一旦有人要创建这些中间类的对象,我们也要保证它们得到正确的初始化。

所以,如果我们要把m_b和m_c初始化成指定的值,Final的构造函数的正确写法应该是这样:

    Final (int a, int b, int c)
: Base2(b, c),
Intermediate2(a, b, c),
Intermediate3(b, c)
{ }

完整的测试程序如下所示,有兴趣的同学可以自行编译运行一下。也可以在调试器中单步运行Final的构造函数,看看前后两种写法分别是调用了Base2的哪个构造函数。

#include "stdafx.h"
#include <iostream> using namespace std; class Base1 {
public:
Base1(int a): m_a(a) {} protected:
int m_a;
}; class Base2 {
public:
Base2(int b, int c): m_b(b), m_c(c) {}
Base2() : m_b(0), m_c(0) {} protected:
int m_b;
int m_c;
}; class Intermediate1 : public Base1, virtual public Base2 {
public:
Intermediate1(int a, int b, int c)
: Base1(a),
Base2(b, c)
{ }
}; class Intermediate2 : public Intermediate1 {
public:
Intermediate2(int a, int b, int c)
: Intermediate1(a, b, c),
Base2(b, c)
{ }
}; class Intermediate3 : virtual public Base2 {
public:
Intermediate3(int b, int c)
: Base2(b, c)
{ }
}; class Final : public Intermediate2, public Intermediate3 {
public:
Final (int a, int b, int c)
: Base2(b, c),
Intermediate2(a, b, c),
Intermediate3(b, c)
{ } void Print() {
cout<<m_a<<", "<<m_b<<", "<<m_c<<endl;
}
}; int _tmain(int argc, _TCHAR* argv[])
{
Final finalObj(1, 2, 3);
finalObj.Print(); return 0;
}

C++中虚继承派生类构造函数的正确写法的更多相关文章

  1. 多重继承,虚继承,MI继承中虚继承中构造函数的调用情况

    先来测试一些普通的多重继承.其实这个是显而易见的. 测试代码: //测试多重继承中派生类的构造函数的调用顺序何时调用 //Fedora20 gcc version=4.8.2 #include < ...

  2. C++继承 派生类中的内存布局(单继承、多继承、虚拟继承)

    今天在网上看到了一篇写得非常好的文章,是有关c++类继承内存布局的.看了之后获益良多,现在转在我自己的博客里面,作为以后复习之用. ——谈VC++对象模型(美)简.格雷程化    译 译者前言 一个C ...

  3. c++ 单继承派生类的构造函数

    1.派生类的构造函数: #include <iostream> #include<string> using namespace std; class Student//声明基 ...

  4. C++中的继承(2)类的默认成员

    在继承关系里面, 在派生类中如果没有显示定义这六个成员函数, 编译系统则会默认合成这六个默认的成员函数. 1.构造与析构函数的调用关系 调用关系先看一段代码: class Base { public ...

  5. C++ 派生类构造函数和析构函数

    几个问题 一个类的各数据成员的构造顺序? 按他们在类定义中出现的先后顺序:先定义者先构造. 类的对象成员的构造函数与类自身的构造函数的执行顺序? 先执行对象成员的构造函数,再执行类自身的构造函数. 构 ...

  6. C++语言笔记系列之十三——派生类构造函数的调用

    1.派生类构造函数的调用 (1)一个基类的全部数据成员均被派生类继承.创建一个派生类对象时.系统在为派生类对象分配单元时一定要为其基类数据成员分配子空间. (2)一个派生类对象在创建时不仅要调用派生类 ...

  7. 《前端之路》- TypeScript (三) ES5 中实现继承、类以及原理

    目录 一.先讲讲 ES5 中构造函数(类)静态方法和多态 1-1 JS 中原型以及原型链 例子一 1-2 JS 中原型以及原型链中,我们常见的 constructor.prototype.**prot ...

  8. android开发中关于继承activity类中方法的调用

    android开发中关于继承activity类中的函数,不能在其他类中调用其方法. MainActivity.java package com.example.testmain; import and ...

  9. C++的派生类构造函数是否要带上基类构造函数

    //public:Student(int s_age):People(s_age) //C++的派生类构造函数后面是否带上基类构造函数,取决于基类构造函数是否需要传入参数,如果要参数,就一定带上:不需 ...

随机推荐

  1. C++学习基础十二——纯虚函数与抽象类

    一.C++中纯虚函数与抽象类: 1.含有一个或多个纯虚函数的类成为抽象类,注意此处是纯虚函数,而不是虚函数. 2.如果一个子类继承抽象类,则必须实现父类中的纯虚函数,否则该类也为抽象类. 3.如果一个 ...

  2. leetcode 202

    202. Happy Number Write an algorithm to determine if a number is "happy". A happy number i ...

  3. KMP算法分析

    KMP是一种复杂度较低的字符串比较算法.基本思路是对欲匹配字符串进行预处理,分析当k位匹配时可以后移的位数,所得的数构成该字符串的特征向量. 求特征向量Next int* Next(string p) ...

  4. Java开发的命名规范

    Java的命名规范 定义规范的目的是为了使项目的代码样式统一,使程序有良好的可读性,便于日后维护. 1.工程的命名(全用小写字母) 工程的命名一般全用小写字母,单词之间用下划线“_”隔开. 2.包的命 ...

  5. 【转】如何在Mac系统中安装R的rattle包

    [转自知乎]:https://www.zhihu.com/question/28944497 1. 安装 xquartz (http://xquartz.macosforge.org)2. 安装 GT ...

  6. Visual Studio 2015 工具箱丢失

    网上主要的解答分为两种:1. 未打开设计界面 2. 重置 实际上,还有一个原因是,没有启动完整版的VS. 安装完后,会有两个VS的程序,一个是Blend For Visual Studio 2015, ...

  7. centos环境自动化批量安装软件脚本

    自动化安装jdk软件部署脚本 准备工作: 1.在执行脚本的服务器上生成免密码公钥: 安装expect命令 yum install -y expect ssh-keygen 三次回车 2.将jdk-7u ...

  8. 机器学习PR:k近邻法分类

    k近邻法是一种基本分类与回归方法.本章只讨论k近邻分类,回归方法将在随后专题中进行. 它可以进行多类分类,分类时根据在样本集合中其k个最近邻点的类别,通过多数表决等方式进行预测,因此不具有显式的学习过 ...

  9. 前端mac下的工具

    1.制作base64图片的 DataURLMaker imageAlpha 减少png图片尺寸 2.共享iphone X-Mirage 3.制作交互图 briefs 4.制作矢量图 sketch

  10. eclipse重定向输入输出到文件

    最近在学习算法第四版,为了要用作者给的测试数据alg4-data,需要将数据直接导入到程序中.在作者的示例代码里用了重定向来做这个事情,但是在eclipse里使用重定向很不方便,查了很多资料,都说是在 ...