在上一课(结构体、成员和成员选择入门)中,我们讨论了如何定义结构体、实例化结构体对象以及访问它们的成员。在本课中,我们将探讨结构体是如何被初始化的。
数据成员默认不初始化
与普通变量类似,数据成员默认情况下不会被初始化。考虑以下结构体:
#include <iostream>
struct Employee
{
int id; // 注意:这里没有初始化器
int age;
double wage;
};
int main()
{
Employee joe; // 注意:这里也没有初始化器
std::cout << joe.id << '\n';
return 0;
}
由于我们没有提供任何初始化器,当 joe
被实例化时,joe.id
、joe.age
和 joe.wage
都将是未初始化的。当我们尝试打印 joe.id
的值时,将会出现未定义行为。
然而,在展示如何初始化一个结构体之前,我们先做一个简短的插叙。
什么是聚合?
在一般编程中,聚合数据类型(也称为聚合)是任何可以包含多个数据成员的类型。某些聚合类型允许成员具有不同的类型(例如结构体),而其他类型则要求所有成员必须是单一类型(例如数组)。
在 C++ 中,聚合的定义更为狭窄且相当复杂。
作者注:在本教程系列中,当我们使用“聚合”(或“非聚合”)一词时,我们将指代 C++ 中的聚合定义。
对于高级读者:简化一下,C++ 中的聚合要么是 C 风格数组(17.7 – C 风格数组简介),要么是一个类类型(结构体、类或联合体),该类型具有以下特点:
- 没有用户声明的构造函数(14.9 – 构造函数简介)
- 没有私有或受保护的非静态数据成员(14.5 – 公有和私有成员以及访问限定符)
- 没有虚函数(25.2 – 虚函数和多态)
流行的类型 std::array
(17.1 – std::array
简介)也是一个聚合。
你可以在这里找到 C++ 聚合的精确定义。
目前需要理解的关键点是:只有数据成员的结构体是聚合。
聚合初始化结构体
由于普通变量只能持有一个值,因此我们只需要提供一个初始化器:
int x { 5 };
然而,结构体可以有多个成员:
struct Employee
{
int id {};
int age {};
double wage {};
};
当我们定义一个结构体类型的对象时,我们需要一种在初始化时初始化多个成员的方法:
Employee joe; // 我们如何初始化 joe.id、joe.age 和 joe.wage?
聚合使用一种称为聚合初始化的初始化形式,它允许我们直接初始化聚合的成员。为此,我们提供一个初始化器列表作为初始化器,它只是一个用大括号括起来的逗号分隔的值列表。
聚合初始化主要有两种形式:
struct Employee
{
int id {};
int age {};
double wage {};
};
int main()
{
Employee frank = { 1, 32, 60000.0 }; // 使用大括号列表的复制列表初始化
Employee joe { 2, 28, 45000.0 }; // 使用大括号列表的列表初始化(推荐)
return 0;
}
每种初始化形式都执行逐成员初始化,这意味着结构体中的每个成员将按照声明顺序依次初始化。因此,Employee joe { 2, 28, 45000.0 };
首先用值 2 初始化 joe.id
,然后用值 28 初始化 joe.age
,最后用值 45000.0 初始化 joe.wage
。
最佳实践
推荐使用非复制的大括号列表形式初始化聚合。
从 C++20 开始,我们还可以使用带括号的值列表初始化某些聚合:
Employee robert ( 3, 45, 62500.0 ); // 使用带括号的列表直接初始化(C++20)
我们建议尽可能避免使用这种形式,因为它目前不适用于使用大括号省略的聚合(特别是 std::array
)。
初始化器列表中缺少初始化器
如果聚合被初始化,但初始化值的数量少于成员的数量,则每个没有显式初始化器的成员将按以下方式初始化:
- 如果成员有默认成员初始化器,则使用该初始化器。
- 否则,成员将从空初始化器列表进行复制初始化。在大多数情况下,这将对成员执行值初始化(对于类类型,即使存在列表构造函数,也会调用默认构造函数)。
struct Employee
{
int id {};
int age {};
double wage { 76000.0 };
double whatever;
};
int main()
{
Employee joe { 2, 28 }; // joe.whatever 将被值初始化为 0.0
return 0;
}
在上述示例中,joe.id
将被初始化为值 2,joe.age
将被初始化为值 28。由于 joe.wage
没有显式初始化器,但有默认成员初始化器,因此 joe.wage
将被初始化为 76000.0。最后,由于 joe.whatever
没有显式初始化器,它将被值初始化为 0.0。
提示
这意味着我们通常可以使用空初始化列表对结构体的所有成员进行值初始化:
Employee joe {}; // 对所有成员进行值初始化
重载 operator<<
以打印结构体
在第 13.5 课 – 输入输出运算符的重载入门中,我们展示了如何重载 operator<<
以打印枚举类型。为结构体重载 operator<<
也很有用。
以下是上一节中的相同示例,但现在带有重载的 operator<<
:
#include <iostream>
struct Employee
{
int id {};
int age {};
double wage {};
};
std::ostream& operator<<(std::ostream& out, const Employee& e)
{
out << e.id << ' ' << e.age << ' ' << e.wage;
return out;
}
int main()
{
Employee joe { 2, 28 }; // joe.wage 将被值初始化为 0.0
std::cout << joe << '\n';
return 0;
}
该程序的输出为:
2 28 0
我们可以看到 joe.wage
确实被值初始化为 0.0(打印为 0)。
与枚举类型不同,结构体可以持有多个值。如何格式化输出(例如如何分隔值)完全取决于你。
我们重载的 operator<<
输出的三个值并不直观,因为没有表明这些值的含义。让我们用同一个示例,但更新输出函数使其更具描述性:
#include <iostream>
struct Employee
{
int id {};
int age {};
double wage {};
};
std::ostream& operator<<(std::ostream& out, const Employee& e)
{
out << "id: " << e.id << " age: " << e.age << " wage: " << e.wage;
return out;
}
int main()
{
Employee joe { 2, 28 }; // joe.wage 将被值初始化为 0.0
std::cout << joe << '\n';
return 0;
}
现在输出为:
id: 2 age: 28 wage: 0
这样更容易理解。
常量结构体
结构体类型的变量可以是 const
(或 constexpr
),就像所有常量变量一样,它们必须被初始化。
struct Rectangle
{
double length {};
double width {};
};
int main()
{
const Rectangle unit { 1.0, 1.0 };
const Rectangle zero { }; // 对所有成员进行值初始化
return 0;
}
C++20 的指定初始化器
当从值列表初始化结构体时,初始化器将按声明顺序应用于成员。
struct Foo
{
int a {};
int c {};
};
int main()
{
Foo f { 1, 3 }; // f.a = 1, f.c = 3
return 0;
}
现在考虑如果你更新这个结构体定义,添加一个不是最后一个成员的新成员会发生什么:
struct Foo
{
int a {};
int b {}; // 刚添加的
int c {};
};
int main