C++隐式和显式继承构造函数调用

4

我有一个关于基类构造函数的隐式和显式调用的问题。如果我们有这样一个类层次结构:

class Person{
    protected:
        std::string m_name;
    public:
        Person(std::string& _name) : m_name(_name){std::cout << "A person is being constructed." << std::endl;}
};

class Baby : public Person{
    private:
        int m_no_of_nappies;
    public:
        Baby(std::string& _name, int& _no_of_nappies) : m_no_of_nappies(_no_of_nappies), Person(_name) {std::cout << "A baby is being constructed." << std::endl ;}

};

根据我的讲义,在主函数中调用'Baby',如下所示:
std::string babyname = "Robert";
int nappies = 5;

Baby baby(babyname, nappies);

导致以下情况发生:
  1. 由于在Baby的初始化列表中进行了对Person的显式调用,因此会调用Baby的初始化列表,并初始化no_of_nappies
  2. 接下来,会进行对Person构造函数的调用,并调用Person的初始化列表。将初始化m_name
  3. 然后会调用Person的构造函数体
  4. 最后会调用Baby的构造函数体
这是有道理的,但如果对父类的默认构造函数进行了隐式调用,会怎样呢?
class Vehicle{
    protected:
        int m_no_wheels;
    public:
        Vehicle() : m_no_wheels(0) { std::cout << "A vehicle is being constructed." << std::endl; }
};

class Bicycle : public Vehicle{
    protected:
        bool m_is_locked;
    public:
        Bicycle() : m_is_locked(false) { std::cout << "A bicycle is being constructed." << std::endl; }
};

这部分我不是很确定。我最好的猜测是,在主函数中调用Bicycle bike;具有以下效果:

  1. 从Bike中向Vehicle的默认构造函数进行了隐式调用。在调用Bike的初始化列表之前。
  2. 由于vehicle没有继承任何内容,因此调用Vehicle的初始化列表,它将m_no_wheels初始化为0
  3. 调用Vehicle的构造函数体。
  4. 我们回到Bicycle,现在调用其初始化列表,将m_is_locked初始化为false
  5. 调用Bike的构造函数体。

请问我的隐式调用推理是否正确?

在我看来,主要区别在于显式引用基础构造函数时,子类的初始化列表总是被首先触发以调用该基础构造函数 - 然而,在隐式调用中,始终会首先触发最顶层父类的初始化列表。

谢谢,并感激不尽!

编辑:我特别询问的是,根据对父类的隐式或显式调用,顺序是否会发生变化。

2个回答

2

基类和成员的初始化顺序在[class.base.init]/11中指定,您可以在此处找到摘要:http://en.cppreference.com/w/cpp/language/initializer_list#Initialization_order

列表中成员初始化器的顺序无关紧要:实际初始化顺序如下:

  1. 如果构造函数是最派生类的,则虚基类按照它们在基类声明的深度优先从左到右遍历的顺序进行初始化(从左到右是指出现在基类说明符列表中的顺序)
  2. 然后,直接基类按照它们在该类的基类说明符列表中出现的顺序从左到右进行初始化
  3. 然后,非静态数据成员按照类定义中的声明顺序进行初始化。
  4. 最后,执行构造函数的主体

(注意:如果初始化顺序由不同构造函数的成员初始化程序列表中的出现控制,则析构函数将无法确保销毁顺序与构造顺序相反)

在定义任何构造函数之前,初始化顺序已经确定; 构造函数初始化程序列表仅影响基类和成员的初始化方式,而不是它们初始化的顺序。

因为PersonBaby的基类,所以始终在Baby的成员m_no_of_nappies之前进行初始化。作为Person初始化的一部分,将初始化它自己的成员,然后执行其构造函数主体。在Person构造函数的主体返回之后,然后初始化m_no_of_nappies。(销毁始终按相反顺序进行。)Vehicle同样是Bicycle的基类,并且首先进行初始化; 由于它没有mem-initializer,因此调用默认构造函数。


这非常有意义。进一步解释一下:这是否意味着即使在运行m_no_of_nappies之前调用了Person的初始化程序和构造函数,但仍然会命中Baby的初始值设定列表,以便调用Person的初始化程序。因此,Baby从其初始化列表中调用Person,因此被击中的第一件事是Baby的初始化列表? - user4165455
@MuyiwaOlu 我不是很确定我理解你的问题。我不知道你说的“hit”是什么意思。基类和成员按规定顺序进行初始化,在初始化期间,如果有相应的mem-initializer可用,则用于命名基类或成员的初始化。 - Brian Bi
如果您在派生类数据初始化列表之前未列出基类构造函数,则可能会根据编译器设置而获得警告。@MuyiwaOlu - Steephen
@Brian 我所谓的“击中”,是指Person的初始化列表,以及函数体是否从Baby的初始化列表内部调用? - user4165455
@MuyiwaOlu 初始化列表并不是从另一个初始化列表中调用的。Baby的初始化列表确定调用了Person的哪个构造函数,当调用该构造函数时,相应的初始化列表控制了Person成员的初始化。 - Brian Bi
太好了,那很有道理。谢谢。 - user4165455

1

§12.6.2定义了如何进行初始化:

列表中成员初始化顺序无关紧要:实际初始化顺序如下:

  • 如果构造函数是最派生类的,虚基类按照从左到右深度优先遍历基类声明的顺序进行初始化(从左到右指出现在基类说明符列表中的顺序)
  • 然后,直接基类按照它们在该类的基类说明符列表中出现的顺序从左到右进行初始化
  • 然后,非静态数据成员按照在类定义中的声明顺序进行初始化。
  • 最后,执行构造函数体(注意:如果初始化顺序由不同构造函数中成员初始化列表中的出现控制,则析构函数将无法确保销毁顺序与构造顺序相反)

针对您的情况进行总结(忽略虚函数):

  1. 按照声明继承的基类顺序
  2. 按照声明顺序初始化成员

因此,构造函数初始化列表中的顺序没有影响。

在第一种情况下,您在这一点上是错误的:PersonBaby 的基类,并在 m_no_of_nappies 之前初始化。


编辑:

宝宝在初始化列表中调用Person,因此首先被触发的是宝宝的初始化列表?

[class.base.init]/10可能是您要查找的内容:如果没有委托,您不需要真正“调用”基类构造函数,编译器会在初始化派生对象时为您调用它。

编译器为您设置了一些东西,以帮助保持构造函数和析构函数的顺序正确

忽略初始化程序的顺序的原因是为了保留构造函数和析构函数调用的通常FIFO顺序。允许两个构造函数使用不同的基础和成员初始化顺序将限制实现使用更动态和更昂贵的策略

引用自https://dev59.com/f2Af5IYBdhLWcg3wyVF1#24287946

最后

编译器隐式调用基类(编译器进行的)是在Bicycle的初始化列表之前还是之后进行的?

如§12.6.2所述,在其余成员类初始化之前。


谢谢你为我关注这个问题。那么第二个隐含的问题呢? - user4165455
@MuyiwaOlu [class.base.init]/10 可能是你正在寻找的。如果没有委托,你不需要真正“调用”基类构造函数,当初始化派生对象时,编译器会自动为你调用它。 - Marco A.
我只是想知道编译器对基类的隐式调用是在Bicycle的初始化列表之前还是之后完成的? - user4165455
在成员类的其余初始化之前。 - Marco A.
你能把这个加到帖子里面吗?这样我就可以标记它为答案了。谢谢! - user4165455

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接