构造函数成员初始化列表
本节阅读量:
本课继续介绍构造函数。
通过成员初始化列表进行成员初始化
为了让构造函数初始化成员,可以使用成员初始化列表。不要将其与名称相似的“初始值设定项列表”混淆,后者用于通过值列表初始化聚合。
在下面的示例中,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
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。然而,在作者的计算机上,将打印以下结果:
发生了什么事?尽管m_y在成员初始化列表中列在第一位,但由于m_x是在类中首先定义的,因此m_x首先被初始化。m_x被初始化为m_y的值,该值尚未初始化。最后,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
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;
}
|
尽管在这种简单的情况下,这将产生预期的结果,但在需要初始化成员的情况下(例如,属性为常量或引用的数据成员),赋值将不起作用。
最佳实践
与在构造函数主体中赋值相比,优先使用成员初始化列表。
习题
问题 1
编写一个名为Ball的类。Ball应当有两个私有成员变量,一个保存颜色,一个保存半径。另外编写一个函数打印小球的颜色和半径。
下面这个示例程序应当能够通过编译:
1
2
3
4
5
6
7
8
9
10
|
int main()
{
Ball blue { "blue", 10.0 };
print(blue);
Ball red { "red", 12.0 };
print(red);
return 0;
}
|
并产生如下结果:
Ball(blue, 10)
Ball(red, 12)
查看答案
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
|
#include <iostream>
#include <string>
#include <string_view>
class Ball
{
private:
std::string m_color { "none" };
double m_radius { 0.0 };
public:
Ball(std::string_view color, double radius)
: m_color { color }
, m_radius { radius }
{
}
const std::string& getColor() const { return m_color; }
double getRadius() const { return m_radius; }
};
void print(const Ball& ball)
{
std::cout << "Ball(" << ball.getColor() << ", " << ball.getRadius() << ")\n";
}
int main()
{
Ball blue { "blue", 10.0 };
print(blue);
Ball red { "red", 12.0 };
print(red);
return 0;
}
|
问题 2
为什么我们把print()写成非成员函数,而不是成员函数?
查看答案
问题 3
为什么我们把m_color写成std::string,而不是std::string_view?
查看答案
在这个例子中,两者其实没有区别(因为我们的颜色实参是C样式字符串字面值,不会离开作用域)。
但从概念上讲,我们希望Ball类拥有传入的颜色。如果m_color是std::string_view,那么当颜色实参传入的是一个临时对象(例如某个函数返回的std::string)时,一旦这个临时的颜色实参被销毁,m_color就会变成悬空的。
我们会在「13.8 结构体杂项」中更详细地讨论这种情况(并给出示例)。