章节目录

派生类的构造顺序

本节阅读量:

在上一课关于C++基本继承的内容中,我们学习了类可以从其他类继承成员和函数。在本课中,我们将更仔细地看看实例化派生类时发生的构造顺序。

首先,让我们介绍一些新的类,用它们来说明一些重要观点。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Base
{
public:
    int m_id {};

    Base(int id=0)
        : m_id { id }
    {
    }

    int getId() const { return m_id; }
};

class Derived: public Base
{
public:
    double m_cost {};

    Derived(double cost=0.0)
        : m_cost { cost }
    {
    }

    double getCost() const { return m_cost; }
};

在这个例子中,Derived类是从Base类派生的。

Derived继承

由于Derived从Base继承函数和变量,因此您可能会以为Base的成员被复制到了Derived中。然而,事实并非如此。相反,我们可以将“Derived”视为一个由两部分组成的类:一部分“Derived”,一部分“Base”。

Derived结构

之前已经有大量例子说明,当我们实例化一个普通(非派生)类时会发生什么:

1
2
3
4
5
6
int main()
{
    Base base;

    return 0;
}

Base是一个非派生类,因为它不从任何其他类继承成员。C++会为Base分配内存,然后调用Base的默认构造函数进行初始化。

现在让我们看看实例化派生类时会发生什么:

1
2
3
4
5
6
int main()
{
    Derived derived;

    return 0;
}

实际运行时,您不会注意到它与前面实例化非派生类Base的示例有什么不同。但在幕后,情况略有不同。如上所述,Derived实际上包含两个部分:Base部分和Derived部分。当C++构造派生对象时,它会分阶段进行。首先,构造最基本的基类(位于继承树顶部)。然后按顺序构造每个子类,直到最后构造当前子类(位于继承树底部)。

因此,当我们实例化Derived对象时,会先构造Base部分(使用Base默认构造函数)。一旦Base部分完成,就构造Derived部分(使用Derived默认构造函数)。此时,没有更底层的派生类,因此构造工作结束。

这个过程实际上很容易说明:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>

class Base
{
public:
    int m_id {};

    Base(int id=0)
        : m_id { id }
    {
        std::cout << "Base\n";
    }

    int getId() const { return m_id; }
};

class Derived: public Base
{
public:
    double m_cost {};

    Derived(double cost=0.0)
        : m_cost { cost }
    {
        std::cout << "Derived\n";
    }

    double getCost() const { return m_cost; }
};

int main()
{
    std::cout << "Instantiating Base\n";
    Base base;

    std::cout << "Instantiating Derived\n";
    Derived derived;

    return 0;
}

此程序产生以下结果:

1
2
3
4
5
Instantiating Base
Base
Instantiating Derived
Base
Derived

当构造Derived时,会先构造Derived的Base部分。这是有意义的:从逻辑上讲,没有父级,子级就不能存在。这也是更安全的做法:子类通常会使用来自父类的变量和函数,但父类并不知道子类的任何信息。先实例化父类,可以确保在创建派生类并准备使用它们时,这些变量已经完成初始化。


继承链的构造顺序

有时,类会从其他类派生,而这些类本身又是从其他类派生的。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>

class A
{
public:
    A()
    {
        std::cout << "A\n";
    }
};

class B: public A
{
public:
    B()
    {
        std::cout << "B\n";
    }
};

class C: public B
{
public:
    C()
    {
        std::cout << "C\n";
    }
};

class D: public C
{
public:
    D()
    {
        std::cout << "D\n";
    }
};

请记住,C++总是首先构造“第一个”或“最基本”的类。然后,它会按顺序遍历继承树,并依次构造每个派生类。

下面是一个简短的程序,说明整个继承链的创建顺序。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
int main()
{
    std::cout << "Constructing A: \n";
    A a;

    std::cout << "Constructing B: \n";
    B b;

    std::cout << "Constructing C: \n";
    C c;

    std::cout << "Constructing D: \n";
    D d;
}

此代码打印以下内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Constructing A:
A
Constructing B:
A
B
Constructing C:
A
B
C
Constructing D:
A
B
C
D

结论

C++会分阶段构造派生类:从最基本的类开始(位于继承树顶部),最终结束于当前子类(位于继承树底部)。在构造每个类时,会调用该类的适当构造函数来初始化对象的对应部分。您会注意到,本节中的示例类都使用了基类默认构造函数(为了简单起见)。在下一课中,我们将更仔细地研究构造函数在构造派生类过程中的作用(包括如何显式选择派生类要使用的基类构造函数)。


24.1 基本继承

上一节

24.3 派生类的构造函数与初始化

下一节