基类构造函数真的在派生类构造函数之前调用吗

Posted

技术标签:

【中文标题】基类构造函数真的在派生类构造函数之前调用吗【英文标题】:Is base class constructor really called before derived class constructor 【发布时间】:2014-11-17 18:59:46 【问题描述】:

我知道这个问题有明确的答案:首先调用基类构造函数,然后调用派生类构造函数。

但我不完全理解“被称为”这个词。是指构造函数的开始使用,还是构造函数的使用结束?换句话说,下面的代码有两种可能的顺序:

    BaseClass 构造函数启动 -> BaseClass 构造函数完成 -> DerivedClass 构造函数启动 -> DerivedClass 构造函数完成。

    DerivedClass 构造函数启动 -> BaseClass 构造函数启动 -> BaseClass 构造函数完成 -> DerivedClass 构造函数完成。

哪一个应该是正确的顺序?如果 1 是正确的,编译器怎么知道在我们初始化 DerivedClass 实例之前调用 BaseClass 构造函数?

似乎情况2是正确的:“调用”应该意味着构造函数的完成。一个后续问题是析构函数怎么样?我知道标准答案是“首先调用派生类的析构函数”。那么正确的顺序是:

DerivedClass 析构函数启动 DerivedClass 析构函数完成 BaseClass 析构函数启动 BaseClass 析构函数完成

谢谢

class BaseClass 
public:
    BaseClass() 
        cout << "BaseClass constructor." << endl;
    
;

class DerivedClass : public BaseClass 
public:
    DerivedClass() : BaseClass() 
        cout << "DerivedClass constructor." << endl;
    
;

int main() 
    DerivedClass dc;

【问题讨论】:

你为什么不运行程序并找出答案? 试试看,一个重要的座右铭;)。 如果 2 是正确的,那么派生类构造函数中的 where 究竟会不会“注入”基类构造函数?编译器将如何做出该决定? 这实际上是一个有趣的问题:由于派生类的构造函数可以在某个翻译单元 A 中定义,而该类型的对象可以在另一个 B 中构造,编译器不会知道 B 中的哪个基如果有几个要调用的类ctor。也就是说,必须在调用基类 ctor 之前使用派生类 ctor 的 一些 部分。 运行代码,查看消息的顺序。 【参考方案1】:

[class.base.init]/10,强调我的:

在非委托构造函数中,初始化在 以下顺序:

首先,并且只针对最 派生类,虚拟基类被初始化[...]

然后,直接基类被初始化 出现在 base-specifier-list 中的声明顺序 (不考虑 mem-initializers 的顺序)。

然后,非静态 数据成员按照它们在 类定义(同样不管 mem-initializers)。

最后,构造函数体的compound-statement被执行

也就是说,派生类的ctor首先被“调用”,但在它的复合语句(它的函数体)之前,基类的ctor必须完成。


我们可以看到这个顺序的一种方法是在派生类的ctor中使用一个function-try-block

#include <iostream>

struct Base 
    Base()  throw "Base throwing\n"; 
;

struct Derived : Base
    Derived()
    try : Base()
    
    catch(char const* p) 
        std::cout << p;
    
;

int main() 
    try  Derived d; catch(...)

Live example

function-try-block 可以捕获在基类和成员初始化期间发生的异常。异常被隐式传播:由于无法构造/初始化基/成员,因此无法构造/初始化(派生)对象。


对于析构函数,[class.dtor]/8

在执行析构函数的主体并销毁主体内分配的所有自动对象后,X 类的析构函数调用 X 的直接非变体非静态的析构函数数据成员,析构函数 对于X 的直接基类,如果X 是最派生类的类型,则其析构函数调用X 的虚拟基类的析构函数。

所有析构函数都被调用,就好像它们被一个合格的引用一样 名称,也就是说,忽略更多派生类中任何可能的虚拟重写析构函数。基类和成员的销毁顺序与其构造函数完成的相反。

如果析构函数是虚拟的,编译器(有时)无法知道必须在析构站点/翻译单元调用哪些析构函数(大多数派生类型及其基类)。因此,析构函数本身必须调用基和成员的析构(至少对于通过动态调度调用的虚拟析构函数)。

【讨论】:

感谢您的 cmets @dyp。您的意思是对于析构函数,构造函数的顺序应该相似吗? DerivedClass 析构函数开始 -> BaseClass 析构函数开始 -> BaseClass 析构函数完成 -> DerivedClass 析构函数完成。 不,正如引用所说:“在执行析构函数的主体 [...] 之后,X 类的析构函数调用 [...] X 的析构函数直接基类 [...]。”也就是说,顺序是:~Derived() 开始 -> ~Derived() 的主体完成 -> ~Base() 开始 -> ~Base() 的主体完成。对于body的执行,构造函数和析构函数的顺序是相反的。【参考方案2】:

正确答案是选项 1,但这可以通过实际运行您已经输入的代码来轻松验证

【讨论】:

您无法仅通过观察来验证保证。

以上是关于基类构造函数真的在派生类构造函数之前调用吗的主要内容,如果未能解决你的问题,请参考以下文章

调用派生类的构造函数在基类的构造函数之前执行

C++中派生类的构造函数怎么显式调用基类构造函数?

派生类的构造函数

绝不在构造函数和析构函数中调用虚函数

绝不在构造函数和析构函数中调用虚函数

基类构造函数的派生类成员初始化