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;
}

在上面的例子中,我们定义了一个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;
}

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;
}
在作者的电脑上,打印出了以下内容:

请注意,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;
}
该程序输出:(修改了一下输出)

请注意,Foo1 和 Foo2 具有相同的成员,唯一的区别在于声明顺序。然而由于添加了填充,Foo1 的大小增加了 50%。
技巧
您可以通过按成员大小递减顺序进行定义来最小化填充。
C++ 编译器不允许重新排序成员,因此必须手动完成此操作。

浙公网安备 33010602011771号