第10章 结构体与联合体
第10章 结构体与联合体
- 如何用结构体将不同类型的数据打包成一个整体
- 联合体的内存共享特性及其典型应用场景
- 结构体与指针、数组的配合使用技巧
想象一下你的学生证,上面有你的学号、姓名、年龄、班级等信息。在C++里,如果你用单独的变量来存储这些信息,管理起来会很零散。结构体(struct)就像一张定制的信息卡片,让你能把所有相关数据打包成一个整体。
10.1 结构体:自定义的数据类型
结构体是一种用户自定义的数据类型,它允许你将多个不同类型的变量组合在一起,形成一个新的复合类型。定义结构体,就是为这种新类型设计一个“蓝图”。
结构体的定义与使用
定义一个结构体,需要使用关键字 struct,后面跟上结构体名称和一对花括号。花括号内是它的“成员变量”。
// 定义一个名为Student的结构体
struct Student {
int id; // 学号
char name[20]; // 姓名
int age; // 年龄
float score; // 成绩
}; // 注意这里的分号不能少!
定义好蓝图后,就可以用它来创建具体的变量了,这称为“结构体变量”。访问其内部的成员,需要使用点运算符(.)。
// 创建结构体变量并初始化
Student stu1 = {1001, "张三", 18, 95.5};
// 访问和修改成员
stu1.age = 19;
cout << "姓名:" << stu1.name << endl;
常见误区
- 忘记结尾分号:在结构体定义的大括号后必须加上分号,这是新手最常见的编译错误之一。
- 混淆定义与声明:
struct Student { ... };是类型定义,不分配内存。只有像Student stu1;这样声明变量时,内存才被分配。 - 错误初始化:初始化列表中的值必须与成员变量的声明顺序和类型严格匹配,否则会导致数据错乱或编译错误。
结构体与数组、指针
结构体可以像基本类型一样,用于创建数组或与指针结合。这在处理批量数据时非常有用。
使用结构体指针时,访问成员需要使用箭头运算符(->),它是解引用和点运算符的组合简写。
Student stuArray[50]; // 结构体数组,存储50个学生信息
stuArray[0].id = 1001;
Student *pStu = &stuArray[0];
// 以下两种访问方式等价
(*pStu).score = 90.0;
pStu->score = 90.0; // 更常用、更清晰
我在一个嵌入式传感器数据采集项目中踩过坑:定义了一个包含温度、湿度、时间戳的结构体,然后用指针数组管理多个传感器。一开始错误地用点运算符访问指针成员,编译器报了一堆错,排查了半天才发现是运算符用错了。
关键点:结构体的大小与内存对齐
结构体的大小并不简单地等于所有成员大小之和。编译器为了提升内存访问效率,会进行“内存对齐”。例如,一个包含 char (1字节) 和 int (4字节) 的结构体,在32位系统上大小可能是8字节而不是5字节。使用 sizeof(Student) 可以获取其实际大小。在涉及网络传输或二进制文件读写时,必须考虑这一点。
10.2 联合体:共享内存的“变体”
如果说结构体是多个房间的套房,那么联合体(union)就是同一个房间,在不同时间可以布置成卧室或书房。它允许你在同一块内存位置存储不同的数据类型,但任何时候只有一个成员是有效的。
联合体的定义与特性
联合体使用关键字 union 定义,语法与结构体类似,但其所有成员共享同一段内存空间。
union DataPacket {
int intValue;
float floatValue;
char str[4];
};
DataPacket packet;
packet.intValue = 0x41424344; // 赋值给int成员
// 此时访问 packet.floatValue 或 packet.str 将解释同一段内存数据
联合体的大小至少能容纳其最大的成员。上面例子中,int 和 float 通常是4字节,char str[4] 也是4字节,所以 sizeof(DataPacket) 通常是4。
| 特性 | 结构体 (struct) | 联合体 (union) |
|---|---|---|
| 内存分配 | 每个成员拥有独立的内存空间 | 所有成员共享同一块内存空间 |
| 总大小 | ≥ 所有成员大小之和(考虑对齐) | = 最大成员的大小(考虑对齐) |
| 数据存储 | 可同时存储所有成员的值 | 同一时间只能存储一个成员的值 |
| 主要用途 | 聚合相关数据(如学生信息) | 节省内存、类型转换、协议解析 |
| 访问方式 | 使用 . 或 -> 运算符 | 使用 . 或 -> 运算符 |
场景1:协议解析
- 网络数据包头部可能有不同类型
- 用联合体根据“类型”字段解释后续数据
- 节省定义多个结构体的开销
场景2:寄存器映射
- 嵌入式开发中,一个32位寄存器
- 可整体访问,也可按位段访问
- 联合体实现硬件寄存器的不同视图
场景3:变体数据
- 数据可能是整数、浮点数或短字符串
- 配合一个“类型标签”成员使用
- 根据标签决定如何解释联合体数据
编者提示:联合体与指针结合时需格外小心,因为对其中一个成员的赋值会覆盖其他成员的值。这与第7章学习的指针解引用操作原理紧密相关,不当使用可能导致难以察觉的内存错误。
10.3 结构体与联合体的高级用法
结构体和联合体可以嵌套使用,以构建更复杂的数据模型。一个常见的模式是:结构体内包含一个联合体,并附带一个标签来指示当前联合体中哪个成员有效。
struct SensorData {
enum DataType { INT, FLOAT, ERROR } type; // 类型标签
union {
int iVal;
float fVal;
char errCode;
} value; // 匿名联合体,可直接访问其成员
};
SensorData data;
data.type = SensorData::FLOAT;
data.value.fVal = 36.5; // 直接访问联合体成员
// 根据类型标签安全地读取数据
if (data.type == SensorData::FLOAT) {
cout << "温度:" << data.value.fVal << endl;
}
实际开发中,大部分团队会使用这种“标签联合”模式来处理变体数据,因为它比纯粹的联合体更安全,能避免误用无效成员。
工程实践:位域
在结构体中,可以对整型成员使用“位域”来指定其占用内存的位数。这在处理硬件寄存器或需要极致压缩数据的场景(如网络协议头)中非常有用。
struct StatusRegister {
unsigned int errorFlag : 1; // 1位
unsigned int mode : 3; // 3位
unsigned int reserved : 28; // 28位
};
注意:位域的行为依赖于编译器和硬件平台,可移植性较差,仅在必要时使用。
动手试一试
1. 定义一个 Book 结构体,包含书名(字符数组)、ISBN号(长整型)、价格(浮点数)和库存量(整型)。创建一个包含3本书的数组,并编写一个函数 printBookInfo 来打印任意一本书的信息。
2. 创建一个联合体 Converter,包含一个 float 成员 f 和一个 unsigned char 数组成员 bytes[4]。给 f 赋值 3.14,然后循环打印 bytes 数组的每个元素(以十六进制格式),观察浮点数在内存中的字节表示。
3. (挑战)定义一个结构体 NetworkPacket,它使用匿名联合体。联合体包含两个结构体成员:一个 Ipv4Header(包含源IP、目的IP等)和一个 ArpHeader(包含硬件类型、协议类型等)。在 NetworkPacket 中添加一个 packetType 字段,并模拟根据类型解析不同协议头的过程。
检验你的理解
- 以下代码的输出是什么?为什么?
struct Test { char a; int b; char c; }; cout << sizeof(Test) << endl; // 假设在32位系统上 - 下面的联合体变量
u在赋值后,成员ch的值是什么?union U { int num; char ch; }; U u; u.num = 65; - 判断对错:结构体变量可以作为函数参数传递,也可以作为函数返回值。
- 使用结构体指针时,访问其成员
id的正确语法是p->id还是p.id?
本章小结
- 结构体(struct)是一种复合数据类型,用于将多个不同类型的变量捆绑成一个逻辑整体,便于管理。通过点运算符(
.)访问成员,通过箭头运算符(->)访问指针指向的结构体成员。 - 联合体(union)的所有成员共享同一块内存空间,其大小由最大成员决定。它用于节省内存、实现数据的多种解释,但使用时需配合类型标签以确保安全。
- 结构体和联合体可以嵌套使用,构建复杂的数据模型。“标签联合”是处理变体数据的常用且安全的模式。
- 理解结构体的内存对齐至关重要,它影响结构体的大小和内存布局,尤其在涉及底层操作(如网络通信、硬件交互)时。
- 位域是结构体的高级特性,用于精确控制成员占用的比特数,但牺牲了可移植性,应谨慎使用。
微信扫码关注
抖音扫码关注