在定义结构体(或类)类型时,我们可以在类型定义中为每个成员提供一个默认初始化值。对于未标记为静态的成员,这一过程有时被称为非静态成员初始化。初始化值被称为默认成员初始化器。
相关内容
我们将在第15.6课——静态成员变量中讨论静态成员和静态成员初始化。
示例:
struct Something
{
int x; // 未提供初始化值(不推荐)
int y {}; // 默认值初始化
int z { 2 }; // 明确的默认值
};
int main()
{
Something s1; // s1.x 未初始化,s1.y 为 0,s1.z 为 2
return 0;
}
在上述 Something
的定义中,x
没有默认值,y
默认进行值初始化,而 z
有一个默认值 2。如果用户在实例化 Something
类型的对象时未提供显式的初始化值,则会使用这些默认成员初始化值。
我们的 s1
对象没有提供初始化器,因此其成员将使用默认值进行初始化。s1.x
没有默认初始化器,因此它保持未初始化状态。s1.y
默认进行值初始化,因此其值为 0。而 s1.z
被初始化为值 2。
请注意,尽管我们没有为 s1.z
提供显式的初始化器,但由于提供了默认成员初始化器,它被初始化为非零值。
关键要点
通过使用默认成员初始化器(或我们稍后将介绍的其他机制),即使没有提供显式的初始化器,结构体和类也可以自我初始化!
对于高级读者
类模板参数推导(CTAD,我们在第13.14课——类模板参数推导(CTAD)和推导指南中有介绍)不能用于非静态成员初始化。
显式初始化值优先于默认值
在列表初始化中,显式的值始终优先于默认成员初始化值。
struct Something
{
int x; // 未提供默认初始化值(不推荐)
int y {}; // 默认值初始化
int z { 2 }; // 明确的默认值
};
int main()
{
Something s2 { 5, 6, 7 }; // 为 s2.x、s2.y 和 s2.z 提供显式初始化值(不使用默认值)
return 0;
}
在上述示例中,s2
为每个成员都提供了显式的初始化值,因此完全不使用默认成员初始化值。这意味着 s2.x
、s2.y
和 s2.z
分别被初始化为值 5、6 和 7。
初始化列表中缺少初始化器且存在默认值
在第13.8课——结构体聚合初始化中,我们提到如果聚合体被初始化,但初始化值的数量少于成员的数量,则所有剩余的成员将进行值初始化。然而,如果为给定成员提供了默认成员初始化器,则将使用该默认成员初始化器,而不是进行值初始化。
struct Something
{
int x; // 未提供默认初始化值(不推荐)
int y {}; // 默认值初始化
int z { 2 }; // 明确的默认值
};
int main()
{
Something s3 {}; // 使用默认值初始化 s3.x,使用默认值初始化 s3.y 和 s3.z
return 0;
}
在上述示例中,s3
使用空列表进行列表初始化,因此所有初始化器都缺失。这意味着如果存在默认成员初始化器,则使用该默认成员初始化器;否则,进行值初始化。因此,s3.x
(没有默认成员初始化器)被值初始化为 0,s3.y
默认进行值初始化为 0,而 s3.z
使用默认值 2。
总结初始化的可能性
如果聚合体使用初始化列表定义:
- 如果存在显式的初始化值,则使用该显式值。
- 如果缺少初始化器且存在默认成员初始化器,则使用默认值。
- 如果缺少初始化器且不存在默认成员初始化器,则进行值初始化。
如果聚合体未使用初始化列表定义:
- 如果存在默认成员初始化器,则使用默认值。
- 如果不存在默认成员初始化器,则成员保持未初始化。
成员始终按照声明顺序进行初始化。
以下示例总结了所有可能性:
struct Something
{
int x; // 未提供默认初始化值(不推荐)
int y {}; // 默认值初始化
int z { 2 }; // 明确的默认值
};
int main()
{
Something s1; // 未提供初始化列表:s1.x 保持未初始化,s1.y 和 s1.z 使用默认值
Something s2 { 5, 6, 7 }; // 显式初始化值:s2.x、s2.y 和 s2.z 使用显式值(不使用默认值)
Something s3 {}; // 缺少初始化器:s3.x 进行值初始化,s3.y 和 s3.z 使用默认值
return 0;
}
我们需要关注的是 s1.x
的情况。因为 s1
没有提供初始化列表,且 x
没有默认成员初始化器,因此 s1.x
保持未初始化(这是不好的,因为我们应该始终初始化变量)。
始终为成员提供默认值
为了避免成员未初始化的可能性,只需确保每个成员都有一个默认值(无论是明确的默认值,还是空的一对大括号)。这样,无论我们是否提供初始化列表,成员都将被初始化为某个值。
考虑以下结构体,它为所有成员都提供了默认值:
struct Fraction
{
int numerator { }; // 我们应该使用 { 0 },但为了示例,这里使用值初始化
int denominator { 1 };
};
int main()
{
Fraction f1; // f1.numerator 进行值初始化为 0,f1.denominator 使用默认值 1
Fraction f2 {}; // f2.numerator 进行值初始化为 0,f2.denominator 使用默认值 1
Fraction f3 { 6 }; // f3.numerator 初始化为 6,f3.denominator 使用默认值 1
Fraction f4 { 5, 8 }; // f4.numerator 初始化为 5,f4.denominator 初始化为 8
return 0;
}
在所有情况下,成员都被初始化为某个值。
最佳实践
为所有成员提供默认值。这可以确保即使变量定义中未包含初始化列表,成员也将被初始化。
默认初始化与值初始化的比较
回顾上述示例中的两行代码:
Fraction f1; // f1.numerator 进行值初始化为 0,f1.denominator 使用默认值 1
Fraction f2 {}; // f2.numerator 进行值初始化为 0,f2.denominator 使用默认值 1
你会注意到,f1
是默认初始化的,而 f2
是值初始化的,但结果相同(numerator
被初始化为 0,denominator
被初始化为 1)。那么,我们应该优先选择哪一种呢?
值初始化的情况(f2
)更安全,因为它会确保任何没有默认值的成员都进行值初始化(尽管我们应该始终为成员提供默认值,但这可以防止遗漏的情况)。
优先使用值初始化还有一个好处——它与其他类型对象的初始化方式一致。保持一致性有助于防止错误。
因此,虽然我们在这些教程中不会强制要求对结构体和类使用值初始化,但我们强烈推荐这样做。