• C++中虚继承派生类构造函数的正确写法


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

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

    image

    一位同事在做重构时,不小心把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;
    }
    
    
  • 相关阅读:
    linux命令行下命令参数前的一横(-)和两横(--)的区别
    sql的集合操作
    二叉树的遍历
    linux网络编程中阻塞和非阻塞socket的区别
    Python 信号量
    python中文件的复制
    Linux网络服务器epoll模型的socket通讯的实现(一)
    Linux启动提示“unexpected inconsistency;RUN fsck MANUALLY”
    Linux用户级线程和内核级线程区别
    nodejs的cs模式聊天客户端和服务器实现
  • 原文地址:https://www.cnblogs.com/kaige/p/cplusplus_virtual_inheritance_derived_class_constructor.html
Copyright © 2020-2023  润新知