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.xs2.ys2.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节中探讨了这种情况 ——默认构造函数与默认参数)。

因此在本教程中,我们不会强制要求结构体和类必须使用值初始化,但强烈建议采用此方式。

posted @ 2025-12-23 09:11  游翔  阅读(8)  评论(0)    收藏  举报