结构体(struct)和联合体(union)是C语言中两种重要的数据聚合类型,它们允许程序员将不同类型的数据项组合成一个逻辑单元。结构体用于将一组相关但可能不同类型的数据打包在一起,每个成员都有自己独立的内存空间。联合体则允许多个成员共享同一块内存空间,在任何时候只有一个成员是有效的。
熟练掌握结构体和联合体的灵活应用,包括嵌套、匿名成员、内存对齐与填充等概念,对于编写高效、清晰且能与底层硬件或数据格式紧密交互的C代码至关重要。
本文将深入探讨结构体和联合体的各种应用技巧,重点关注它们的嵌套使用、匿名特性,并详细解析内存对齐和填充的规则及其影响。
1. 结构体 (struct)
结构体用于将逻辑上相关的不同类型数据项捆绑在一起,形成一个新的复合数据类型。
1.1 基本定义与使用
#include <stdio.h>
#include <string.h>
// 定义一个表示学生的结构体
struct Student {
char name[50];
int id;
float gpa;
};
int main() {
// 声明结构体变量
struct Student s1;
// 访问和修改成员 (使用点运算符 .)
strcpy(s1.name, "Alice");
s1.id = 101;
s1.gpa = 3.8f;
printf("Student Name: %s\n", s1.name);
printf("Student ID: %d\n", s1.id);
printf("Student GPA: %.2f\n", s1.gpa);
// 结构体初始化
struct Student s2 = {"Bob", 102, 3.5f};
printf("\nStudent Name: %s, ID: %d, GPA: %.2f\n", s2.name, s2.id, s2.gpa);
// 指定成员初始化 (C99及以后)
struct Student s3 = {
.name = "Charlie",
.id = 103,
.gpa = 3.9f
};
printf("\nStudent Name: %s, ID: %d, GPA: %.2f\n", s3.name, s3.id, s3.gpa);
// 结构体指针
struct Student *ptr_s = &s1;
// 访问成员 (使用箭头运算符 ->)
printf("\nAccess via pointer: Name=%s\n", ptr_s->name);
ptr_s->gpa = 3.85f; // 修改成员
printf("Updated GPA: %.2f\n", s1.gpa);
return 0;
}
1.2 结构体嵌套
结构体的成员本身也可以是另一个结构体类型,这允许创建更复杂的数据结构。
#include <stdio.h>
#include <string.h>
struct Date {
int day;
int month;
int year;
};
struct Employee {
char name[50];
int employee_id;
struct Date hire_date; // 嵌套 Date 结构体
float salary;
};
int main() {
struct Employee emp1;
strcpy(emp1.name, "David");
emp1.employee_id = 201;
emp1.salary = 60000.0f;
// 访问嵌套结构体的成员
emp1.hire_date.day = 15;
emp1.hire_date.month = 6;
emp1.hire_date.year = 2022;
printf("Employee: %s (ID: %d)\n", emp1.name, emp1.employee_id);
printf("Hired on: %d/%d/%d\n", emp1.hire_date.month, emp1.hire_date.day, emp1.hire_date.year);
printf("Salary: %.2f\n", emp1.salary);
// 嵌套初始化
struct Employee emp2 = {
"Eve",
202,
{20, 8, 2023}, // 初始化嵌套结构体
65000.0f
};
printf("\nEmployee: %s, Hired: %d/%d/%d\n",
emp2.name, emp2.hire_date.month, emp2.hire_date.day, emp2.hire_date.year);
return 0;
}
1.3 匿名结构体 (Anonymous Structs)
匿名结构体是指在定义时没有给出结构体标签(名称)的结构体。它们通常作为另一个结构体或联合体的成员使用。
注意: C11 标准正式支持匿名结构体和联合体。在 C11 之前的编译器(如 GCC、Clang)可能通过扩展支持它们。
#include <stdio.h>
struct Point {
int x;
int y;
};
struct Rectangle {
// 匿名结构体作为成员
struct {
int x;
int y;
} top_left; // 这个匿名结构体本身是有名字的 (top_left)
// C11 匿名结构体 (无成员名)
struct {
int width;
int height;
}; // 这个匿名结构体的成员可以直接访问
// 也可以嵌套已命名的结构体
struct Point bottom_right;
};
int main() {
struct Rectangle rect;
// 访问第一个匿名结构体的成员 (通过其名称 top_left)
rect.top_left.x = 10;
rect.top_left.y = 20;
// 访问 C11 匿名结构体的成员 (直接访问)
rect.width = 100;
rect.height = 50;
// 访问嵌套的已命名结构体成员
rect.bottom_right.x = rect.top_left.x + rect.width;
rect.bottom_right.y = rect.top_left.y + rect.height;
printf("Rectangle Top-Left: (%d, %d)\n", rect.top_left.x, rect.top_left.y);
printf("Rectangle Dimensions: Width=%d, Height=%d\n", rect.width, rect.height);
printf("Rectangle Bottom-Right: (%d, %d)\n", rect.bottom_right.x, rect.bottom_right.y);
return 0;
}
优点:
- 减少层级:对于 C11 的无名匿名结构体,可以减少访问成员时的层级(如 rect.width 而不是 rect.dimensions.width)。
- 组织相关数据:可以将一组密切相关的成员组织在一起,即使它们逻辑上属于外部结构。
2. 联合体 (union)
联合体允许其所有成员共享同一块内存区域。联合体的大小由其最大成员的大小决定(可能因对齐而更大)。在任何时刻,只有一个成员可以被有效地存储和访问。
2.1 基本定义与使用
#include <stdio.h>
// 定义一个可以存储整数、浮点数或字符的联合体
union Data {
int i;
float f;
char c;
};
int main() {
union Data data;
printf("Size of union Data: %zu bytes\n", sizeof(union Data));
// 大小通常是 sizeof(int) 或 sizeof(float) 中较大的那个 (通常是 4 或 8)
data.i = 10;
printf("Data as int: %d\n", data.i);
// 此时访问 f 或 c 是未定义行为 (UB),但常用于类型转换 (Type Punning)
// printf("Data as float (UB): %f\n", data.f);
data.f = 3.14f;
printf("Data as float: %f\n", data.f);
// 此时访问 i 或 c 是 UB
// printf("Data as int (UB): %d\n", data.i);
data.c = 'A';
printf("Data as char: %c\n", data.c);
// 联合体初始化 (只能初始化第一个成员)
union Data data2 = { 20 }; // 初始化 data2.i 为 20
printf("\nInitialized data2.i: %d\n", data2.i);
// 指定成员初始化 (C99及以后)
union Data data3 = { .f = 2.71f }; // 初始化 data3.f
printf("Initialized data3.f: %f\n", data3.f);
return 0;
}
2.2 联合体的应用场景
- 节省内存:当一组数据项互斥,即任何时候只需要使用其中一个时,联合体可以节省大量内存。
- struct Packet {
int type; // 标识 data 中存储的是哪种类型
union {
int command_id;
float sensor_value;
char message[64];
} data; // 匿名联合体 (C11)
};
// 如果不用联合体,需要为 command_id, sensor_value, message[64] 都分配空间
// 使用联合体,只需要分配 max(sizeof(int), sizeof(float), sizeof(char[64])) 的空间 - 类型转换/重新解释位模式 (Type Punning):通过联合体,可以用一种类型的成员写入数据,然后用另一种类型的成员读出数据,从而重新解释底层的二进制位模式。这是未定义行为 (Undefined Behavior) 在 C 标准中,但在实践中被广泛用于某些底层编程技巧,尤其是在与硬件交互或需要特定位操作时。 编译器可能会进行优化,导致结果不符合预期。更安全、标准的类型转换方式是使用 memcpy。
#include <stdio.h>
#include <string.h>
union FloatInt {
float f;
unsigned int i;
};
int main() {
union FloatInt fi;
fi.f = -1.0f;
// 通过联合体查看 float 的二进制表示 (依赖于实现)
printf("Float: %f, Int representation: 0x%X\n", fi.f, fi.i);
// 更安全的方式:使用 memcpy
float f_val = -1.0f;
unsigned int i_val;
// 确保 sizeof(float) == sizeof(unsigned int)
memcpy(&i_val, &f_val, sizeof(float));
printf("Float: %f, Int representation (memcpy): 0x%X\n", f_val, i_val);
return 0;
}
- 实现变体类型 (Variant Types):结合结构体和枚举,可以创建能存储多种不同类型值的变体类型。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef enum { TYPE_INT, TYPE_FLOAT, TYPE_STRING } VariantType;
typedef struct {
VariantType type;
union {
int i;
float f;
char *s; // 注意:字符串需要动态分配和释放
} value;
} Variant;
void print_variant(const Variant *v) {
switch (v->type) {
case TYPE_INT:
printf("Int: %d\n", v->value.i);
break;
case TYPE_FLOAT:
printf("Float: %f\n", v->value.f);
break;
case TYPE_STRING:
printf("String: \"%s\"\n", v->value.s ? v->value.s : "(null)");
break;
default:
printf("Unknown type\n");
}
}
void free_variant_string(Variant *v) {
if (v->type == TYPE_STRING && v->value.s) {
free(v->value.s);
v->value.s = NULL;
}
}
int main() {
Variant v1, v2, v3;
v1.type = TYPE_INT;
v1.value.i = 123;
v2.type = TYPE_FLOAT;
v2.value.f = 45.6f;
v3.type = TYPE_STRING;
v3.value.s = strdup("Hello Variant"); // 需要释放
print_variant(&v1);
print_variant(&v2);
print_variant(&v3);
free_variant_string(&v3);
return 0;
}
2.3 匿名联合体 (Anonymous Unions)
与匿名结构体类似,匿名联合体通常作为结构体或联合体的成员使用,允许直接访问其成员,而无需通过联合体名称。
#include <stdio.h>
struct Configuration {
int config_type; // 0: Network, 1: Serial
union { // C11 匿名联合体
struct { // 匿名结构体
char ip_address[16];
int port;
} network_config;
struct { // 匿名结构体
char device_name[32];
int baud_rate;
} serial_config;
}; // 匿名联合体的成员可以直接访问
};
int main() {
struct Configuration cfg;
cfg.config_type = 0; // Network config
// 直接访问匿名联合体内的匿名结构体成员
strcpy(cfg.network_config.ip_address, "192.168.1.100");
cfg.network_config.port = 8080;
printf("Config Type: Network\n");
printf("IP: %s, Port: %d\n", cfg.network_config.ip_address, cfg.network_config.port);
cfg.config_type = 1; // Serial config
strcpy(cfg.serial_config.device_name, "/dev/ttyS0");
cfg.serial_config.baud_rate = 9600;
printf("\nConfig Type: Serial\n");
printf("Device: %s, Baud Rate: %d\n", cfg.serial_config.device_name, cfg.serial_config.baud_rate);
return 0;
}
3. 内存对齐 (Memory Alignment)
内存对齐是指数据在内存中存储的起始地址必须是某个值(通常是数据类型大小或CPU架构要求的特定值)的倍数。CPU访问对齐的数据通常比访问未对齐的数据更快,甚至某些架构(如ARM的某些模式)根本不允许访问未对齐的数据,会触发硬件异常。
编译器为了满足CPU的对齐要求,并优化访问速度,会自动在结构体成员之间或结构体末尾插入填充字节 (Padding)。
3.1 对齐规则 (常见规则,具体依赖编译器和架构)
- 结构体成员的对齐:每个成员变量的存储起始地址必须是其自身类型大小和默认对齐值 (通常由架构决定,如4或8字节) 中较小者的整数倍。
- 例如,char 通常按 1 字节对齐,short 按 2 字节,int 按 4 字节,double 按 8 字节。
- 结构体的整体对齐:整个结构体的总大小必须是其所有成员中最大对齐要求的整数倍。这个最大对齐要求称为结构体的对齐模数 (Alignment Modulus)。
3.2 填充示例
#include <stdio.h>
#include <stddef.h> // For offsetof
struct Example1 {
char c1; // 大小 1, 对齐 1
int i; // 大小 4, 对齐 4
char c2; // 大小 1, 对齐 1
};
// 布局分析 (假设 int 对齐为 4):
// c1: offset 0, size 1
// padding: offset 1, size 3 (为了让 i 在 4 的倍数地址开始)
// i: offset 4, size 4
// c2: offset 8, size 1
// padding: offset 9, size 3 (为了让整个结构体大小是最大对齐要求 4 的倍数)
// Total size = 12
struct Example2 {
char c1; // 大小 1, 对齐 1
char c2; // 大小 1, 对齐 1
int i; // 大小 4, 对齐 4
};
// 布局分析:
// c1: offset 0, size 1
// c2: offset 1, size 1
// padding: offset 2, size 2 (为了让 i 在 4 的倍数地址开始)
// i: offset 4, size 4
// Total size = 8 (已经是最大对齐要求 4 的倍数,无需末尾填充)
int main() {
printf("Size of Example1: %zu bytes\n", sizeof(struct Example1)); // 通常输出 12
printf("Offsets in Example1:\n");
printf(" c1: %zu\n", offsetof(struct Example1, c1)); // 0
printf(" i: %zu\n", offsetof(struct Example1, i)); // 4
printf(" c2: %zu\n", offsetof(struct Example1, c2)); // 8
printf("\nSize of Example2: %zu bytes\n", sizeof(struct Example2)); // 通常输出 8
printf("Offsets in Example2:\n");
printf(" c1: %zu\n", offsetof(struct Example2, c1)); // 0
printf(" c2: %zu\n", offsetof(struct Example2, c2)); // 1
printf(" i: %zu\n", offsetof(struct Example2, i)); // 4
return 0;
}
offsetof 宏:定义在 <stddef.h> 中,用于获取结构体成员相对于结构体起始地址的字节偏移量。
3.3 影响与优化
- 内存占用增加:填充字节会增加结构体的实际内存占用。
- 性能提升:保证对齐可以提高CPU访问效率。
- 结构体成员顺序:调整结构体成员的声明顺序可以影响填充字节的数量,从而可能减小结构体总大小。通常将对齐要求较高的成员放在前面,或者将对齐要求相同的成员放在一起,有助于减少填充。 (如 Example2 比 Example1 更紧凑)。
3.4 控制对齐
虽然默认对齐通常是最佳选择,但有时需要显式控制对齐:
- #pragma pack(n) (编译器特定):指示编译器以 n 字节对齐。#pragma pack(1) 常用于创建紧凑的、无填充的结构体,例如用于文件格式或网络协议,但这可能牺牲性能并导致未对齐访问。
#pragma pack(push, 1) // 保存当前对齐设置,并设置为 1 字节对齐
struct PackedStruct {
char c1;
int i;
char c2;
};
#pragma pack(pop) // 恢复之前的对齐设置
// sizeof(PackedStruct) 通常为 1 + 4 + 1 = 6
- _Alignas / alignas (C11及以后):用于指定变量或类型的最小对齐要求。
#include <stdalign.h> // For alignas
struct AlignedStruct {
alignas(16) int i; // 要求 i 至少按 16 字节对齐
char c;
};
// 结构体的整体对齐要求会提升到 16
- aligned_alloc (C11及以后):分配具有指定对齐方式的内存。
注意: 修改默认对齐方式可能导致性能下降或未对齐访问问题,应谨慎使用,并充分了解目标平台的特性。
4. 总结
- 结构体 (struct) 将不同类型数据组合,成员各自独立存储,支持嵌套和匿名成员(C11)。
- 联合体 (union) 将不同类型数据组合,成员共享同一内存,大小由最大成员决定,常用于节省内存、类型转换(需谨慎)和变体类型。
- 内存对齐 是编译器为了优化性能和满足硬件要求而采取的策略,会在结构体中插入填充字节。
- 结构体成员的声明顺序会影响填充和总大小,合理安排顺序可以优化内存占用。
- 可以通过 #pragma pack 或 alignas (C11) 等方式控制对齐,但这可能影响性能和可移植性。
理解结构体、联合体以及内存对齐的原理和应用,是编写高效、健壮且能与底层细节交互的C代码的关键。在追求内存紧凑性和性能时,需要权衡对齐带来的影响,并根据具体场景选择最合适的实现方式。