13-9 默认成员初始化
在定义结构体(或类)类型时,我们可以为每个成员提供默认初始化值作为类型定义的一部分。对于未标记为静态的成员,此过程有时称为非静态成员初始化 non-static member initialization。初始化值称为默认成员初始化器default member initializer。
相关内容:
我们在第15.6节——静态成员变量中介绍了静态成员及其初始化。
以下是一个示例:
struct Something
{
int x; // no initialization value (bad)
int y {}; // value-initialized by default
int z { 2 }; // explicit default value
};
int main()
{
Something s1; // s1.x is uninitialized, s1.y is 0, and s1.z is 2
return 0;
}
在上述Something的定义中,x没有默认值,y默认采用值初始化,而z的默认值为2。当用户实例化Something类型对象时若未提供显式初始化值,系统将使用这些默认成员初始化值。
由于对象 s1 未提供初始化器,其成员将初始化为默认值:s1.x 因无默认初始化器而保持未初始化状态;s1.y 默认采用值初始化,因此取值为 0;s1.z 则被初始化为 2。
请注意:尽管我们未为 s1.z 提供显式初始化器,但由于默认成员初始化器的存在,它仍被初始化为非零值。
关键要点:
通过使用默认成员初始化器(或后续将介绍的其他机制),结构体和类即使未提供显式初始化器也能实现自我初始化!
进阶知识:
类模板参数推导(CTAD,详见第13.14节)无法用于非静态成员初始化。
显式初始化值优先于默认值
列表初始化器中的显式值始终优先于默认成员初始化值。
struct Something
{
int x; // no default initialization value (bad)
int y {}; // value-initialized by default
int z { 2 }; // explicit default value
};
int main()
{
Something s2 { 5, 6, 7 }; // use explicit initializers for s2.x, s2.y, and s2.z (no default values are used)
return 0;
}
在上例中,s2 的每个成员都具有显式初始化值,因此完全未使用默认成员初始化值。这意味着 s2.x、s2.y 和 s2.z 分别被初始化为 5、6 和 7。
初始化列表中缺少初始化器(当存在默认值时)
在上一课(13.8节——结构体聚合初始化)中我们提到,若聚合体被初始化但初始化值数量少于成员数量,则所有剩余成员将采用值初始化。然而,若某个成员提供了默认成员初始化器,则将使用该默认初始化器替代。
struct Something
{
int x; // no default initialization value (bad)
int y {}; // value-initialized by default
int z { 2 }; // explicit default value
};
int main()
{
Something s3 {}; // value initialize s3.x, use default values for s3.y and s3.z
return 0;
}
在上例中,s3 是通过空列表初始化的,因此缺少所有初始化器。这意味着若存在默认成员初始化器则使用它,否则将进行值初始化。因此:s3.x(无默认成员初始化器)通过值初始化为 0;s3.y 默认值初始化为 0;s3.z 默认值初始化为 2。
回顾初始化方案
若聚合体通过初始化列表定义:
-
存在显式初始化值时,使用该显式值。
-
若缺少初始化器且存在默认成员初始化器,则使用默认值。
-
若缺少初始化项且不存在默认成员初始化器,则进行值初始化。
若聚合体未定义初始化列表:
-
若存在默认成员初始化器,则使用默认值。
-
若不存在默认成员初始化器,则成员保持未初始化状态。
成员始终按声明顺序进行初始化。
以下示例总结所有情况:
struct Something
{
int x; // no default initialization value (bad)
int y {}; // value-initialized by default
int z { 2 }; // explicit default value
};
int main()
{
Something s1; // No initializer list: s1.x is uninitialized, s1.y and s1.z use defaults
Something s2 { 5, 6, 7 }; // Explicit initializers: s2.x, s2.y, and s2.z use explicit values (no default values are used)
Something s3 {}; // Missing initializers: s3.x is value initialized, s3.y and s3.z use defaults
return 0;
}
我们需要警惕的情况是 s1.x。由于 s1 没有初始化列表,而 x 没有默认成员初始化器,s1.x 将保持未初始化状态(这很糟糕,因为我们应该始终初始化变量)。
始终为成员变量提供默认值
为避免成员变量未初始化的情况,只需确保每个成员变量都具有默认值(无论是显式默认值还是空大括号)。这样无论是否提供初始化列表,成员变量都将被赋予某个值。
请看以下结构体示例,其中所有成员变量均设置了默认值:
struct Fraction
{
int numerator { }; // we should use { 0 } here, but for the sake of example we'll use value initialization instead
int denominator { 1 };
};
int main()
{
Fraction f1; // f1.numerator value initialized to 0, f1.denominator defaulted to 1
Fraction f2 {}; // f2.numerator value initialized to 0, f2.denominator defaulted to 1
Fraction f3 { 6 }; // f3.numerator initialized to 6, f3.denominator defaulted to 1
Fraction f4 { 5, 8 }; // f4.numerator initialized to 5, f4.denominator initialized to 8
return 0;
}
在所有情况下,我们的成员都初始化为特定值。
最佳实践:
为所有成员提供默认值。这确保即使变量定义中未包含初始化列表,成员仍会被初始化。
默认初始化 vs 聚合值初始化
重新审视上述示例中的两行代码:
Fraction f1; // f1.numerator value initialized to 0, f1.denominator defaulted to 1
Fraction f2 {}; // f2.numerator value initialized to 0, f2.denominator defaulted to 1
你会注意到f1是默认初始化,而f2是值初始化,但结果相同(分子初始化为0,分母初始化为1)。那么我们应该选择哪种方式?
值初始化(f2)更安全,因为它能确保所有未提供默认值的成员都经过值初始化(尽管我们应始终为成员提供默认值,但此方式可防范遗漏默认值的情况)。
采用值初始化还有另一优势——它与初始化其他类型对象的方式保持一致。一致性有助于避免错误。
最佳实践:
对于聚合,应优先使用值初始化(使用空大括号初始化器)而非 默认初始化(不使用大括号)。
话虽如此,程序员使用默认初始化而非值初始化来处理类类型的情况并不少见。这部分源于历史原因(值初始化直到C++11才引入),部分是因为存在特殊情况(针对非聚合类型),此时默认初始化可能比值初始化更高效(我们在第14.11节中探讨了这种情况 ——默认构造函数与默认参数)。
因此在本教程中,我们不会强制要求结构体和类必须使用值初始化,但强烈建议采用此方式。

浙公网安备 33010602011771号