章节目录

构造函数成员初始化列表

本节阅读量:

本课继续介绍构造函数。


通过成员初始化列表进行成员初始化

为了让构造函数初始化成员,可以使用成员初始化列表。不要将其与类似名称的“初始值设定项列表”混淆,那是用于值列表初始化聚合。

在下面的示例中,Foo(int,int) 构造函数已更新为使用成员初始化列表来初始化m_x和m_y:

 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
#include <iostream>

class Foo
{
private:
    int m_x {};
    int m_y {};

public:
    Foo(int x, int y)
        : m_x { x }, m_y { y } // 这里是 成员初始化列表
    {
        std::cout << "Foo(" << x << ", " << y << ") constructed\n";
    }

    void print() const
    {
        std::cout << "Foo(" << m_x << ", " << m_y << ")\n";
    }
};

int main()
{
    Foo foo{ 6, 7 };
    foo.print();

    return 0;
}

成员初始化列表在构造函数参数之后定义。它以冒号(:)开头,然后列出要初始化的每个成员以及该变量的初始化值,用逗号分隔。这里必须使用直接形式的初始化(最好使用大括号,但括号也可以)——这里不能使用复制初始化(等号)。还要注意,成员初始化列表不会以分号结尾。

该程序产生以下输出:

1
2
Foo(6, 7) constructed
Foo(6, 7)

实例化foo时,使用指定的值来设置初始化列表中的成员。在这种情况下,将m_x初始化为x的值(6),将m_y初始化为y的值(7)。然后运行构造函数的主体。

当调用print()成员函数时,可以看到m_x仍然具有值6,m_y仍然具有值7。


成员初始化列表的缩进格式

C++提供了很大的自由来根据您的喜好,来格式化成员初始化列表,因为它不关心冒号、逗号或空白的位置。

以下样式都有效(在实践中您可能会看到这三种样式):

1
2
3
    Foo(int x, int y) : m_x { x }, m_y { y }
    {
    }
1
2
3
4
5
    Foo(int x, int y) :
        m_x { x },
        m_y { y }
    {
    }
1
2
3
4
5
    Foo(int x, int y)
        : m_x { x }
        , m_y { y }
    {
    }

我们建议使用上面的第三种样式:

  1. 将冒号放在构造函数名称后面的行上,因为这将成员初始化列表与函数原型清晰地分开。
  2. 缩进成员初始化列表,以便更容易看到函数名。

如果成员初始化列表简短/琐碎,则所有初始值设定项都可以放在一行上:

1
2
3
4
    Foo(int x, int y)
        : m_x { x }, m_y { y }
    {
    }

否则(或者如果愿意),每个成员和初始值设定项对可以放在单独的一行上(以逗号开头以保持对齐):

1
2
3
4
5
    Foo(int x, int y)
        : m_x { x }
        , m_y { y }
    {
    }

成员初始化顺序

C++标准是这样说的,所有成员初始化列表中的成员总是按照它们在类中定义的顺序进行初始化(而不是按照列表中定义的次序)。

在上面的示例中,由于m_x在类定义中定义在m_y之前,因此m_x将首先初始化(即使它没有在列表中首先列出)。

因为我们直观地期望从左到右初始化变量,这可能会导致发生细微的错误。考虑以下示例:

 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
#include <algorithm> // for std::max
#include <iostream>

class Foo
{
private:
    int m_x{};
    int m_y{};

public:
    Foo(int x, int y)
        : m_y{ std::max(x, y) }, m_x{ m_y } // 这一行有问题
    {
    }

    void print() const
    {
        std::cout << "Foo(" << m_x << ", " << m_y << ")\n";
    }
};

int main()
{
    Foo foo{ 6, 7 };
    foo.print();

    return 0;
}

在上面的示例中,意图是计算传入的初始化值中较大的一个(通过std::max(x,y)),然后使用该值初始化m_x和m_y。然而,在作者的计算机上,将打印以下结果:

1
Foo(-858993460, 7)

发生了什么事?尽管m_y在成员初始化列表中列在第一位,但由于m_x是在类中首先定义的,因此m_x首先被初始化。m_x被初始化为m_y的值,该值尚未初始化。最后,m_y被初始化为较大的初始化值。

为了帮助防止这种错误,成员初始化列表中的成员应该按照它们在类中定义的顺序列出。如果成员的初始化顺序不正确,某些编译器将发出警告。

最好避免使用其他成员的值初始化成员变量(如果可能)。这样,即使您确实在初始化顺序中出错,也不重要,因为初始化值之间没有依赖关系。


成员初始化列表与默认成员初始值设置项

可以用几种不同的方法初始化成员:

  1. 如果成员在初始化列表中列出,则使用该初始化值
  2. 否则,如果成员具有默认的初始值设定项,则使用该初始化值
  3. 否则,该成员将默认初始化。

这意味着,如果成员既有默认的初始值设定项,又列在构造函数的成员初始化列表中,则列表中的值优先。

下面是一个显示所有三种初始化方法的示例:

 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
#include <iostream>

class Foo
{
private:
    int m_x{};    // 默认成员初始值 (会被忽略)
    int m_y{ 2 }; // 默认成员初始值 (被使用)
    int m_z;      // 无默认值

public:
    Foo(int x)
        : m_x{ x } // 成员初始化列表
    {
        std::cout << "Foo constructed\n";
    }

    void print() const
    {
        std::cout << "Foo(" << m_x << ", " << m_y << ", " << m_z << ")\n";
    }
};

int main()
{
    Foo foo{ 6 };
    foo.print();

    return 0;
}

在作者的机器上,此输出:

1
2
Foo constructed
Foo(6, 2, -858993460)

下面是正在发生的事情。构造foo时,初始化列表中只有m_x,因此m_x首先初始化为6。m_y不在初始化列表中,但它具有默认值,因此它被初始化为2。m_z既不在成员初始化列表中,也没有默认值,因此它是默认初始化的(对于基本类型,这意味着它未被初始化)。因此,当打印m_z的值时,得到未定义的行为。


构造函数体

构造函数的主体通常为空。这是因为我们主要使用构造函数进行初始化,这是通过成员初始化列表完成的。如果这就是需要做的所有事情,那么不需要在构造函数的主体中使用任何语句。

构造函数体中的语句在成员初始化列表执行后执行,因此可以添加语句来执行所需的任何其他设置任务。在上面的示例中,将一些内容打印到控制台,以显示构造函数已执行,也可以执行其他操作,如打开文件或数据库、分配内存等…

新程序员有时使用构造函数的主体将值分配给成员:

 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
#include <iostream>

class Foo
{
private:
    int m_x{};
    int m_y{};

public:
    Foo(int x, int y)
    {
        m_x = x; // 这是赋值,而不是初始化
        m_y = y; // 这是赋值,而不是初始化
    }

    void print() const
    {
        std::cout << "Foo(" << m_x << ", " << m_y << ")\n";
    }
};

int main()
{
    Foo foo{ 6, 7 };
    foo.print();

    return 0;
}

尽管在这种简单的情况下,这将产生预期的结果,但在需要初始化成员的情况下(例如,属性为常量或引用的数据成员),赋值将不起作用。


14.8 构造函数简介

上一节

14.10 默认构造函数和默认参数

下一节