百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 文章教程 > 正文

C语言精华:结构体与联合体的灵活应用深度解析

yund56 2025-06-02 22:04 10 浏览



结构体(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 联合体的应用场景

  1. 节省内存:当一组数据项互斥,即任何时候只需要使用其中一个时,联合体可以节省大量内存。
  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])) 的空间
  3. 类型转换/重新解释位模式 (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;
     }
  1. 实现变体类型 (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 对齐规则 (常见规则,具体依赖编译器和架构)

  1. 结构体成员的对齐:每个成员变量的存储起始地址必须是其自身类型大小默认对齐值 (通常由架构决定,如4或8字节)较小者的整数倍。
  2. 例如,char 通常按 1 字节对齐,short 按 2 字节,int 按 4 字节,double 按 8 字节。
  3. 结构体的整体对齐:整个结构体的总大小必须是其所有成员中最大对齐要求的整数倍。这个最大对齐要求称为结构体的对齐模数 (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访问效率。
  • 结构体成员顺序:调整结构体成员的声明顺序可以影响填充字节的数量,从而可能减小结构体总大小。通常将对齐要求较高的成员放在前面,或者将对齐要求相同的成员放在一起,有助于减少填充。 (如 Example2Example1 更紧凑)。

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 packalignas (C11) 等方式控制对齐,但这可能影响性能和可移植性。

理解结构体、联合体以及内存对齐的原理和应用,是编写高效、健壮且能与底层细节交互的C代码的关键。在追求内存紧凑性和性能时,需要权衡对齐带来的影响,并根据具体场景选择最合适的实现方式。

相关推荐

没有获得Windows 10 20H2升级通知,怎样直接升级

微软公司已经正式发布Windows1020H2操作系统,在正常情况下,微软只会首先推送到少量电脑,然后一边推送一边采集遥测数据。收集遥测数据可以确定哪些电脑可以更新,哪些电脑在更新后可能会失败,微...

不想让人随便卸载你安装的程序,用这四招,他将无计可施

Windows10不提供设置删除应用程序限制的功能,有几种间接方法可以防止用户删除操作系统中的程序和游戏。一、WindowsInstaller服务使用Windows工具,可以部分限制用户的权限。如...

一文看懂苹果全球开发者大会 五大系统全面升级

来源:环球网【环球网智能报道记者张阳】北京时间6月23日凌晨1点,苹果全球开发者大会(WWDC2020)如期举行,还是那个熟悉的乔布斯剧院,依旧是高水准的视频展示,但是这届WWDC,却是苹果历史...

无需等待微软分批推送,23H2可借助注册表快速获取Win11 24H2更新

IT之家10月15日消息,Windows1124H2正在分批推送,但由于存在多种Bug,微软已经开始放缓其推送节奏。WindowsLatest发现,Windows1123H2...

办公小技巧:剑走偏锋 PPT中打造动态图表

年底到了少不了又要制作各种总结报表,为了让自己的报表与众不同,我们可以借助PowerPoint动画组件+报表的方式,打造出更为出彩的动态图表。下面以PowerPoint2016为例,介绍如何使用三维...

文档表格 版本差异何在

在办公过程中,对文档或表格的修改是司空见惯的事。那么,一份文档做了内容改动,如何知道差异在哪里?一份表格改动部分数据,如何知道哪些有所变动?不要说审阅和修订功能,因为不是所有人都会用这些功能来标注的,...

Excel VBA自制日历组件16色可选 完美替代VBA日期控件

本日期组件可跟随单元格跟随窗体中ActiveX文本框组合框控件16种配色可选私信回复880日历可体验效果使用说明1打开自己需要应用日历面板的Excel表,注意必须是启用VBA的格式2在...

如何从交互角度读懂产品需求文档

作为设计师,理解产品经理提供的需求文档是交互设计工作的重要前提与起点,然而对于很多设计师来说,需求文档内容通常非常复杂,设计师们需要花费大量时间去消化、理解和归纳。本文作者结合公司示例,分析设计师如何...

植入让文档变得更强大

有效地利用文档置入技术,会让我们的常用文档功能变得更加强大,实现更加高效或有趣的应用。1.写字板文档嵌入其他文档有时,我们要组织一个大型的文档,但是这些文档的内容可能来自于不同种类的文档编辑器,比如...

Office 2016滚动文本框 顺手就来

【电脑报在线】如果一页PPT内容较多无法在完全显示,就需要用到滚动文本框,在PPT2016中借助控件即可快速制作滚动文本框。在“告诉我你想要做什么”输入“文本框控件”,在搜索结果点击“文本框(Acti...

Axure的多状态复选树

本文将详细介绍如何在Axure中实现一种增强型的多状态复选树组件,它不仅支持全选、半选和未选等状态,还具备动态加载、关键字筛选等高级功能。多状态复选树(Multi-StateCheckboxTre...

办公小技巧:PPT中控件图表巧联动

在利用PPT进行图表演示时,操作者有可能要与图表进行交互联动,比如通过输入数据来预测产品的生产情况等,这时就需要用到“开发工具”中的控件了。几个控件配合几句VBA代码,就可以轻松实现上述交互联动效果(...

用好插件——找回火狐的旧功能

现在的软件,特别是浏览器类软件,更新换代速度都很快,而且无论是外观界面还是系统组件都会有较大的变化,这样会让很多朋友无所适从。以大家常用的火狐浏览器为例,它就已经升级到了最新的35版,而且在新版中对很...

重新认识控件(二)

图片和文字,都是一种数据形式。我平时对文本框的录入,报错和提交的设计比较多。最近涉及到图片控件的设计,细细琢磨一下,这玩意还有一些平时没太注意的细节点,感觉对于其他控件的设计有指导意义,特此总结一下传...

JSA宏教程——在文档中添加复合框控件

上一期,我们初步认识了控件Control,本节我们将继续控件的相关内容。这几期我们将逐一介绍相关控制。本节先介绍复合框(也叫组合框)Combobox。复合框的作用复合框就是一个下拉选项框,一次显示一个...