默认成员初始化

在定义结构体(或类)类型时,我们可以在类型定义中为每个成员提供一个默认初始化值。对于未标记为静态的成员,这一过程有时被称为非静态成员初始化。初始化值被称为默认成员初始化器。

相关内容

我们将在第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.xs2.ys2.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)更安全,因为它会确保任何没有默认值的成员都进行值初始化(尽管我们应该始终为成员提供默认值,但这可以防止遗漏的情况)。

优先使用值初始化还有一个好处——它与其他类型对象的初始化方式一致。保持一致性有助于防止错误。

因此,虽然我们在这些教程中不会强制要求对结构体和类使用值初始化,但我们强烈推荐这样做。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"