13-11 结构杂项

具有程序定义成员的结构

在 C++ 中,结构体(和类)可以​​拥有其他程序自定义类型的成员。有两种方法可以实现这一点。

首先,我们可以定义一个程序定义类型(在全局作用域中),然后将其用作另一个程序定义类型的成员:

#include <iostream>

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

struct Company
{
    int numberOfEmployees {};
    Employee CEO {}; // Employee is a struct within the Company struct
};

int main()
{
    Company myCompany{ 7, { 1, 32, 55000.0 } }; // Nested initialization list to initialize Employee
    std::cout << myCompany.CEO.wage << '\n'; // print the CEO's wage

    return 0;
}

image

在上面的例子中,我们定义了一个Employee结构体,然后将其用作另一个Company结构体的成员。当我们初始化我们的结构体时Company,我们也可以Employee使用嵌套的初始化列表来初始化我们的成员。如果我们想知道CEO的薪水是多少,我们只需使用两次成员选择运算符即可:myCompany.CEO.wage;

其次,类型还可以嵌套在其他类型中,因此如果员工仅作为公司的一部分存在,则员工类型可以嵌套在公司结构中:

#include <iostream>

struct Company
{
    struct Employee // accessed via Company::Employee
    {
        int id{};
        int age{};
        double wage{};
    };

    int numberOfEmployees{};
    Employee CEO{}; // Employee is a struct within the Company struct
};

int main()
{
    Company myCompany{ 7, { 1, 32, 55000.0 } }; // Nested initialization list to initialize Employee
    std::cout << myCompany.CEO.wage << '\n'; // print the CEO's wage

    return 0;
}

这通常用于类,所以我们将在以后的课程中更多地讨论这一点(15.3 -- 嵌套类型(成员类型))。


作为所有者的结构体应该拥有作为所有者的数据成员。

在 5.9 课——std::string_view(第二部分)中,我们介绍了所有者和查看者的双重概念。所有者管理自己的数据,并控制何时销毁数据。查看者查看其他人的数据,但无法控制何时修改或销毁数据。

大多数情况下,我们希望结构体(和类)拥有其所包含数据的所有权。这样做有几个好处:

  • 只要结构体(或类)存在,数据成员就一直有效。
  • 这些数据成员的值不会发生意外变化。

使结构体(或类)成为所有者的最简单方法是,为每个数据成员指定一个所有者类型(例如,不是查看器、指针或引用)。如果结构体或类的所有数据成员都是所有者,那么该结构体或类本身就自动成为所有者。

如果一个结构体(或类)包含一个作为查看器的数据成员,那么被查看的对象有可能在查看该对象的数据成员之前被销毁。如果发生这种情况,结构体中就会留下一个悬空成员,访问该成员会导致未定义行为。

最佳实践
大多数情况下,我们希望结构体(和类)拥有所有权。最简单的实现方法是确保每个数据成员都有一个所有权类型(例如,不是查看器、指针或引用)。

作者注
遵循安全结构体原则。不要让成员悬空。

这就是为什么字符串数据成员几乎总是所有者类型std::string(即所有者),而不是查看者类型std::string_view(即查看者)。以下示例说明了这一点的重要性:

#include <iostream>
#include <string>
#include <string_view>

struct Owner
{
    std::string name{}; // std::string is an owner
};

struct Viewer
{
    std::string_view name {}; // std::string_view is a viewer
};

// getName() returns the user-entered string as a temporary std::string
// This temporary std::string will be destroyed at the end of the full expression
// containing the function call.
std::string getName()
{
    std::cout << "Enter a name: ";
    std::string name{};
    std::cin >> name;
    return name;
}

int main()
{
    Owner o { getName() };  // The return value of getName() is destroyed just after initialization
    std::cout << "The owners name is " << o.name << '\n';  // ok

    Viewer v { getName() }; // The return value of getName() is destroyed just after initialization
    std::cout << "The viewers name is " << v.name << '\n'; // undefined behavior

    return 0;
}

image

getName() 函数返回用户输入的名称作为临时 std::string。该临时返回值在调用该函数的完整表达式结束时被销毁。

对于 o,这个临时 std::string 用于初始化 o.name。由于 o.name 是 std::string,因此 o.name 会复制临时 std::string。然后临时 std::string 就消失了,但 o.name 不受影响,因为它是一个副本。当我们在后续语句中打印 o.name 时,它​​会按我们的预期工作。

对于 v,这个临时 std::string 用于初始化 v.name。由于 v.name 是一个 std::string_view,因此 v.name 只是临时 std::string 的视图,而不是副本。然后临时的 std::string 消失,留下 v.name 悬空。当我们在后续语句中打印 v.name 时,我们会得到未定义的行为。


结构体大小与数据结构对齐

通常情况下,结构体的大小是其所有成员大小的总和,但并非总是如此!

请考虑以下程序:

#include <iostream>

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

int main()
{
    std::cout << "The size of short is " << sizeof(short) << " bytes\n";
    std::cout << "The size of int is " << sizeof(int) << " bytes\n";
    std::cout << "The size of double is " << sizeof(double) << " bytes\n";

    std::cout << "The size of Foo is " << sizeof(Foo) << " bytes\n";

    return 0;
}

在作者的电脑上,打印出了以下内容:

image

请注意,short + int + double 的大小为 14 字节,但 Foo 的大小为 16 字节!

实际上,我们只能说结构体的大小至少等于其包含的所有变量的总和。但实际大小可能更大!出于性能考虑,编译器有时会在结构体中添加间隔(称为填充padding)。

在上面的 Foo 结构体中,编译器在成员 a 之后隐式添加了 2 字节的填充,使得结构体大小变为 16 字节而非 14 字节。

对于高级读者
编译器添加填充的原因超出了本教程的范围,但希望深入了解的读者可查阅维基百科上的数据结构对齐相关条目。此内容属于可选阅读材料,理解结构体或C++语言并不需要这些知识!

这实际上会对结构体的大小产生相当显著的影响,如下面的程序所示

#include <iostream>

struct Foo1
{
    short a{}; // will have 2 bytes of padding after a
    int b{};
    short c{}; // will have 2 bytes of padding after c
};

struct Foo2
{
    int b{};
    short a{};
    short c{};
};

int main()
{
    std::cout << sizeof(Foo1) << '\n'; // prints 12
    std::cout << sizeof(Foo2) << '\n'; // prints 8

    return 0;
}

该程序输出:(修改了一下输出)

image

请注意,Foo1 和 Foo2 具有相同的成员,唯一的区别在于声明顺序。然而由于添加了填充,Foo1 的大小增加了 50%。

技巧
您可以通过按成员大小递减顺序进行定义来最小化填充。

C++ 编译器不允许重新排序成员,因此必须手动完成此操作。

posted @ 2025-12-24 10:11  游翔  阅读(19)  评论(0)    收藏  举报