13-8 结构体聚合初始化

在上一课(13.7——结构体、成员和成员选择简介)中,我们讨论了如何定义结构体、实例化结构体对象以及访问其成员。本节课,我们将讨论如何初始化结构体。

默认情况下,数据成员不会被初始化。

与普通变量类似,数据成员默认情况下不会被初始化。考虑以下结构体:

#include <iostream>

struct Employee
{
    int id; // note: no initializer here
    int age;
    double wage;
};

int main()
{
    Employee joe; // note: no initializer here either
    std::cout << joe.id << '\n';

    return 0;
}

image

由于我们没有提供任何初始化器,因此当joe实例化时 joe.idjoe.agejoe.wage都将是未初始化的。当我们尝试打印joe.id的值时,就会出现未定义行为。

不过,在向您展示如何初始化结构体之前,让我们先稍微绕一下弯。


什么是聚合体aggregate

在一般编程中,聚合数据类型aggregate data type(也称为聚合体aggregate)是指任何可以包含多个数据成员的类型。某些类型的聚合体允许成员具有不同的类型(例如结构体),而另一些类型则要求所有成员必须是同一类型(例如数组)。

在 C++ 中,聚合的定义更狭窄,也复杂得多。

作者注:
在本系列教程中,当我们使用术语“聚合”aggregate(或“非聚合”non-aggregate)时,我们指的是 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; // how do we initialize joe.id, joe.age, and joe.wage?

聚合体使用一种称为聚合初始化aggregate initialization的初始化形式,它允许我们直接初始化聚合体的成员。为此,我们提供一个初始化列表initializer list作为初始化器,该列表仅由用大括号括起的、以逗号分隔的值组成。

聚合初始化主要有两种形式:

struct Employee
{
    int id {};
    int age {};
    double wage {};
};

int main()
{
    Employee frank = { 1, 32, 60000.0 }; // copy-list initialization using braced list
    Employee joe { 2, 28, 45000.0 };     // list initialization using braced list (preferred)

    return 0;
}

每种初始化形式都执行成员逐项初始化memberwise initialization,这意味着结构体中的每个成员都按声明顺序进行初始化。因此,代码 Employee joe { 2, 28, 45000.0 }; 首先将 joe.id 初始化为值 2,接着将 joe.age 初始化为值 28,最后将 joe.wage 初始化为值 45000.0。

最佳实践:
初始化聚合时,优先采用(非复制)大括号列表形式。

在 C++20 中,我们还可以使用括号内的值列表来初始化(某些)聚合体:

Employee robert ( 3, 45, 62500.0 );  // direct initialization using parenthesized list (C++20)

我们建议尽可能避免使用最后一种形式,因为它目前无法与使用花括号省略的聚合类型(特别是std::array)配合使用。


初始化列表中缺少初始化项

若聚合体被初始化,但初始化值的数量少于成员数量,则每个未显式初始化的成员按以下方式初始化:

  • 若成员具有默认成员初始化项,则使用该项。

  • 否则,该成员将通过空初始化列表进行复制初始化。在大多数情况下,这将对这些成员执行值初始化(对于类类型,即使存在列表构造函数,此操作仍会调用默认构造函数)。

struct Employee
{
    int id {};
    int age {};
    double wage { 76000.0 };
    double whatever;
};

int main()
{
    Employee joe { 2, 28 }; // joe.whatever will be value-initialized to 0.0

    return 0;
}

image

提示:
这意味着我们通常可以使用空初始化列表来对结构体的所有成员进行值初始化:

Employee joe {}; // value-initialize all members

重载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 will be value-initialized to 0.0
    std::cout << joe << '\n';

    return 0;
}

这会打印

image

我们可以看到,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 will be value-initialized to 0.0
    std::cout << joe << '\n';

    return 0;
}

现在会输出

image

这样就容易理解一些了。


常量结构体(const struct)

结构体类型的变量可以是常量(或 constexpr),与所有常量变量一样,它们必须被初始化。

struct Rectangle
{
    double length {};
    double width {};
};

int main()
{
    const Rectangle unit { 1.0, 1.0 };
    const Rectangle zero { }; // value-initialize all members

    return 0;
}

image


指定初始化器 (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 {}; // just added
    int c {};
};

int main()
{
    Foo f { 1, 3 }; // now, f.a = 1, f.b = 3, f.c = 0

    return 0;
}

现在所有初始化值都发生了偏移,更糟糕的是编译器可能不会将此识别为错误(毕竟语法仍然有效)。

为避免此类问题,C++20引入了结构体成员的新初始化方式——指定初始化器designated initializers。该机制允许显式定义初始化值与成员的映射关系。成员可通过列表初始化或复制初始化进行初始化,且必须按结构体中声明的顺序初始化,否则将引发警告或错误。未指定初始化器的成员将采用值初始化。

struct Foo
{
    int a{ };
    int b{ };
    int c{ };
};

int main()
{
    Foo f1{ .a{ 1 }, .c{ 3 } }; // ok: f1.a = 1, f1.b = 0 (value initialized), f1.c = 3
    Foo f2{ .a = 1, .c = 3 };   // ok: f2.a = 1, f2.b = 0 (value initialized), f2.c = 3
    Foo f3{ .b{ 2 }, .a{ 1 } }; // error: initialization order does not match order of declaration in struct

    return 0;
}

对 Clang 用户
当使用大括号为单个值指定初始化器时,Clang 会错误地发出警告“标量初始化器周围的大括号”。希望此问题能尽快修复。

image
image

指定初始化器之所以优秀,在于它们能提供某种程度的自文档化功能,并有助于确保初始化值的顺序不会被无意中混淆。然而,指定初始化器也会显著增加初始化列表的冗余度,因此目前我们不建议将其作为最佳实践。

此外,由于无法强制要求在初始化集合时始终使用指定初始化器,建议避免在现有集合定义的中间位置添加新成员,以规避初始化器位移的风险。

最佳实践:
向聚合体添加新成员时,最稳妥的做法是将其添加到定义列表的末尾,这样其他成员的初始化器就不会发生位移。


带初始化列表的赋值

如前一课所示,我们可以单独为结构体的成员赋值:

struct Employee
{
    int id {};
    int age {};
    double wage {};
};

int main()
{
    Employee joe { 1, 32, 60000.0 };

    joe.age  = 33;      // Joe had a birthday
    joe.wage = 66000.0; // and got a raise

    return 0;
}

对于单个成员而言这没问题,但当需要更新多个成员时就不太理想了。类似于使用初始化列表初始化结构体,你也可以通过初始化列表(执行成员赋值)为结构体赋值

struct Employee
{
    int id {};
    int age {};
    double wage {};
};

int main()
{
    Employee joe { 1, 32, 60000.0 };
    joe = { joe.id, 33, 66000.0 }; // Joe had a birthday and got a raise

    return 0;
}

请注意,由于我们不希望修改 joe.id,因此需要在列表中为 joe.id 提供当前值作为占位符,以便成员赋值操作能将 joe.id 赋值给 joe.id。这种做法略显笨拙。


带指定初始化的赋值 C++20

指定初始化器也可用于列表赋值:

struct Employee
{
    int id {};
    int age {};
    double wage {};
};

int main()
{
    Employee joe { 1, 32, 60000.0 };
    joe = { .id = joe.id, .age = 33, .wage = 66000.0 }; // Joe had a birthday and got a raise

    return 0;
}

任何未在赋值操作中指定的成员变量,都将被赋予用于值初始化的默认值。如果我们没有为joe.id指定指定初始化器,那么joe.id将被赋予值0。


使用同类型结构体初始化结构体

结构体也可使用同类型的另一个结构体进行初始化:

#include <iostream>

struct Foo
{
    int a{};
    int b{};
    int c{};
};

std::ostream& operator<<(std::ostream& out, const Foo& f)
{
    out << f.a << ' ' << f.b << ' ' << f.c;
    return out;
}

int main()
{
    Foo foo { 1, 2, 3 };

    Foo x = foo; // copy-initialization
    Foo y(foo);  // direct-initialization
    Foo z {foo}; // direct-list-initialization

    std::cout << x << '\n';
    std::cout << y << '\n';
    std::cout << z << '\n';

    return 0;
}

上面会输出:

image

请注意,这里使用的是我们熟悉的标准初始化形式(复制初始化、直接初始化或直接列表初始化),而非聚合初始化。

这种情况最常见于用函数返回值初始化结构体时,该函数返回的是相同类型的结构体。我们在第13.10节——传递和返回结构体中对此进行了更详细的讲解。

posted @ 2025-12-23 08:29  游翔  阅读(18)  评论(0)    收藏  举报