23. 【C语言】共用体与枚举类型:当数据需要“变脸”

前面我们用结构体把不同数据打包在一起,一个 struct Student 里同时有姓名、学号、成绩,各占各的空间,互不干扰。

但有时候,我们需要的恰恰相反:同一个存储空间,在不同时刻存放不同类型的数据。 比如一个变量有时存整数,有时存浮点数,但从来不同时存在;或者我们要用一种紧凑的方式去“拆解”一个整数的各个字节。这时候,结构体的“孪生兄弟”——共用体(union) 就登场了。

同时,我们还会认识一个让代码更优雅的工具:枚举(enum)。它能把一堆有关系的整数常量组织起来,让代码的可读性上一个台阶。


一、共用体是什么?

共用体(union) 是一种特殊的复合类型,它所有的成员共享同一块内存空间。同一时刻,只有一个成员是“活跃”的,修改一个成员会覆盖其他成员的值。

定义共用体的语法和结构体几乎一样,只是把 struct 换成 union

union Data {
    int    i;
    double d;
    char   c;
};

union Data 的大小不是三个成员大小之和,而是最大成员的大小(加上可能的对齐填充)。因为所有成员都用同一个地址,存不同的类型时就是“变脸”。

#include <stdio.h>

union Data {
    int    i;
    double d;
    char   c;
};

int main(void) {
    union Data u;
    printf("sizeof(union Data) = %zu\n", sizeof(u));  // 通常是 8(double 大小)

    u.i = 42;
    printf("u.i = %d\n", u.i);   // 42
    printf("u.d = %f\n", u.d);   // 未定义行为,输出垃圾值
    printf("u.c = %c\n", u.c);   // 未定义行为

    u.d = 3.14;
    printf("u.i = %d\n", u.i);   // 未定义行为,被覆盖了
    printf("u.d = %f\n", u.d);   // 3.14

    return 0;
}

要点:

  • 同一时刻,共用体只能保存一个成员的值。
  • 读取一个非最后一次写入的成员,结果是未定义行为(虽然多数实现只是把位模式重新解释)。
  • 共用体的大小等于其最宽成员的大小。

共用体与结构体的核心区别

结构体(struct)共用体(union)
成员存储各自独立,同时存在共享同一空间
大小≥ 所有成员大小之和(对齐后)≥ 最大成员大小
同时有效成员数全部一个
典型用途打包不同类型数据类型多态、节省内存、拆解数据

二、共用体的典型应用场景

1. 节省内存:多种类型不同时使用

比如你正在开发一个绘图程序,图形属性里有一个“填充色”,但填充色可能是 RGB 值(三个整数),也可能是灰度值(一个整数)。它们不会同时使用。

struct Shape {
    int type;          // 0=灰度, 1=RGB
    union {
        int gray;      // 灰度值 0~255
        struct {
            int r, g, b;
        } rgb;         // RGB 三色
    } color;
};

int main(void) {
    struct Shape s;
    s.type = 1;
    s.color.rgb.r = 255;
    s.color.rgb.g = 128;
    s.color.rgb.b = 64;
    // 现在 s.color.gray 是无意义的

    return 0;
}

2. 判断大小端(字节序)

大小端(Endianness)是指多字节数据在内存中的存储顺序。大端模式将高字节存低地址,小端模式反之。利用共用体可以轻易检测当前平台:

#include <stdio.h>

int is_little_endian(void) {
    union {
        int  i;
        char c[sizeof(int)];
    } u;
    u.i = 1;
    return u.c[0] == 1;  // 小端:低字节(1)在最低地址
}

int main(void) {
    if (is_little_endian()) {
        printf("小端模式\n");
    } else {
        printf("大端模式\n");
    }
    return 0;
}

写入 int 值为 1(四字节:01 00 00 00 或 00 00 00 01),然后读第一个字节,若是 1,说明低地址存低字节,即小端。

3. 拆解数据:按类型和按字节访问

网络编程、协议解析中,常需要一个 32 位整数,但又要能单独操作每个字节。共用体非常方便:

union IPAddress {
    uint32_t addr;
    uint8_t  bytes[4];
};

int main(void) {
    union IPAddress ip;
    ip.bytes[0] = 192;
    ip.bytes[1] = 168;
    ip.bytes[2] = 1;
    ip.bytes[3] = 100;
    printf("IP: %u.%u.%u.%u\n", ip.bytes[0], ip.bytes[1], ip.bytes[2], ip.bytes[3]);
    printf("作为 32 位整数: 0x%08X\n", ip.addr);
    return 0;
}

注意:此类用法依赖平台的字节序,不同平台结果可能不同。


三、枚举:给整数常量起个有意义的名字

有时程序中需要一组相关的整数常量,比如星期(1~7)、颜色(红黄蓝绿)、状态码(成功、失败、超时)。直接用数字写满代码,可读性差又容易出错:

int color = 1;   // 1 是红色?还是蓝色?得翻文档

枚举(enum) 就是为这个场景设计的。它定义一组命名整数常量,让代码自解释。

enum Weekday {
    MONDAY = 1,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
};

规则:

  • 如果不手动赋值,枚举值默认从 0 开始,依次递增。
  • 可以手动指定某个值,后续值自动递增(MONDAY = 1,则 TUESDAY 自动为 2)。
  • 多个枚举常量可以有相同的值(但不推荐)。

声明枚举变量:

enum Weekday today = WEDNESDAY;
if (today == SATURDAY || today == SUNDAY) {
    printf("周末愉快!\n");
}

C 语言中,枚举类型底层是 int,所以枚举变量可以和整数混用(虽然编译器可能不警告),但为了类型清晰,应该避免把裸整数赋给枚举变量。


四、枚举与 switch 是好搭档

枚举配合 switch 非常自然,某些编译器(如 GCC/Clang)在开启特定警告选项时,可以检测 switch 是否覆盖了所有枚举值。

#include <stdio.h>

enum Direction {
    NORTH,
    SOUTH,
    EAST,
    WEST
};

void move(enum Direction dir) {
    switch (dir) {
        case NORTH: printf("向北走\n"); break;
        case SOUTH: printf("向南走\n"); break;
        case EAST:  printf("向东走\n"); break;
        case WEST:  printf("向西走\n"); break;
        default:    printf("未知方向\n"); break;
    }
}

五、枚举 vs 宏:为什么优先用枚举?

#define 也能定义常量:

#define RED   0
#define GREEN 1
#define BLUE  2

但枚举有不可替代的优势:

#defineenum 枚举
类型检查无(纯文本替换)有,枚举是独立类型
调试信息调试器只能看到数字可以看到枚举常量名
作用域宏全局有效(除非 #undef受作用域控制,可放在结构体/函数内
自增赋值需手动指定自动递增

因此,能用枚举的地方,尽量不要用宏定义一堆零散的整数常量。


六、常见错误与陷阱

1. 读取共用体非活跃成员

union Data u;
u.i = 10;
printf("%f\n", u.d);   // 未定义行为!值是垃圾

严格来说这是 UB,尽管常被用于类型双关。若确实需要类型双关,C99 起可以使用 union 进行类型双关(在 GCC/Clang 等编译器中是支持的扩展,但不是严格标准行为),更安全的做法是使用 memcpy。。

2. 枚举值当成字符串

enum Color { RED, GREEN, BLUE };
printf("%s\n", RED);   // 错误!打印出数字或崩溃

枚举值是整数,不能直接当字符串输出。如果需要在调试中输出名称,通常手工写转换函数。

3. 枚举类型混用导致意外赋值

enum Color { RED=0, GREEN=1, BLUE=2 };
enum Direction { NORTH=0, SOUTH=1 };
enum Color c = NORTH;  // 编译可能不报错,但逻辑上是错的

虽然都是 int,但不同枚举类型混用会让代码混乱。尽量保持类型一致。

4. 对共用体使用 sizeof 误当成成员之和

union U {
    int a;
    double b;
};
printf("%zu\n", sizeof(union U)); // 通常是 8,不是 12

共用体大小只需容下最大的那个。


七、小结

今天你认识了结构体的两个“亲戚”:

  • 共用体 让多个成员共享同一块内存,用于节省空间、检查字节序、协议解析等。使用时务必清楚当前活跃的成员是谁。
  • 枚举 给整数常量赋予了有意义的名字,让代码更可读、更安全。配合 switch 使用优雅高效。

共用体与结构体有时会联合使用,比如前面看到的带标记的 Shapetype 字段指示当前共用体的含义),这其实是 C 语言实现“变体记录”或“tagged union”的经典手法。

现在,你已经可以自由组合结构体、共用体、枚举来构建复杂的数据模型。但数据只在程序运行时存在——一旦程序退出,变量就消失了。怎么把数据长久保存?下一篇,我们进入文件操作的世界:fopenfclosefprintffscanf,让你的程序能读写磁盘上的文件,真正“留下痕迹”。


课后小练习

  1. 定义一个共用体 Number,包含 intfloatdouble 三个成员。写一个函数 print_number(union Number n, int type),根据 type 的值打印对应的成员。在 main 中测试。
  2. 利用共用体写一个函数,输入一个 unsigned int,交换它的高低 16 位并返回(例如 0x12345678 变成 0x56781234)。提示:用共用体配合 unsigned short 数组。
  3. 定义一个枚举 HttpStatus,包含常见的 HTTP 状态码(200, 301, 404, 500 等),并手动指定值。写一个函数 get_status_message(enum HttpStatus code),返回对应的字符串描述。
  4. (小挑战)设计一个“配置文件解析器”的数据模型:配置项的值可能是整数、浮点数或字符串。用共用体和枚举结合实现一个 ConfigValue 类型,并编写设置和打印函数,根据类型打印不同格式的值。

我们下期见!

💡获取本系列示例代码请访问 GitCode 仓库

随着全民健身事业的深入推进户外运动的快速普及,定向越野赛事举办频次持续提升,赛事规模人数不断增长,参组织者对赛事组织效率、服务质量及管理规范化的要求日益提高。然而,传统定向越野赛事管理仍依赖人工登记、线下核对、纸质记录等方式,普遍存在信息同步滞后、流程繁琐易错、数据统计低效、成绩核算耗时、资金签到管理不规范等突出问题。例如,人工报名信息核对易出现遗漏错误,现场签到排队拥堵影响参赛体验,成绩人工录入误差率高,赛事资金物资管理缺乏透明化监管。这些问题不仅大幅增加赛事组织成本人力消耗,还制约赛事运营效率整体服务水平提升。在此背景下,构建一套数字化、一体化的定向越野赛事管理系统,成为赛事运营主体优化管理模式、提升服务质量的迫切需求。本研究旨在通过信息化技术重构赛事管理全流程,解决传统模式下的信息孤岛操作低效问题,为定向越野赛事规范化、智能化管理提供可落地的解决方案。 本研究基于 Spring Boot Vue 技术栈,采用前后端分离架构设计并实现了一套定向越野赛事管理系统。技术层面:后端依托 Spring Boot 框架搭建 RESTful API 服务,利用其自动配置模块化特性简化开发流程,集成 MyBatis-Plus 优化数据持久化操作;前端采用 Vue.js 框架实现组件化开发,通过 Element UI 组件库构建交互友好的可视化界面,利用 Axios 实现前后端数据动态交互;数据库选用 MySQL 保障数据高效存储事务一致性,同时采用手机号短信验证、JWT 令牌等机制强化系统安全性用户权限管理。 本系统的实施为定向越野赛事运营管理提供了显著的现实价值:其一,通过线上报名、信息筛选自动化核对,大幅降低人工操作误差,提升赛事组织效率 30% 以上;其二,定位打卡签到实时成绩同步功能,实现参赛流程无纸化、智能化,显著改善参赛者体验;其
打开链接下载源码: https://pan.quark.cn/s/a4b39357ea24 ARM公司特别为ARM架构的处理器,尤其是STM32系列微控制器,开发了一套高效的数字信号处理软件包。这个软件包内含多种基础的数字信号处理技术,例如快速傅里叶变换(FFT)和比例积分微分(PID)调节器,其目的是辅助开发者在嵌入式环境中达成卓越的音频、图像处理及其他信号处理任务。 FFT(快速傅里叶变换)是一种高效计算离散傅里叶变换(DFT)的方法,在频谱分析、滤波器构造等方面有广泛应用。ARM的DSP软件包所提供的FFT功能通常配备多种尺寸的预制模块,用以满足不同数据长度的需求。使用者能够借助这些功能迅速将时域数据转化为频域数据,从而执行频谱分析或设计滤波器。 PID控制器是一种成熟的控制策略,由比例、积分及微分三个环节构成,用于调节系统的响应性能。在ARM的DSP软件包中,PID控制器的示范程序能够指导开发者如何设定和改善PID参数,以实现系统的高精度控制。使用PID控制器一般需要调节Kp(比例系数)、Ki(积分系数)和Kd(微分系数),以达成所需的响应速度和稳定性。 在"Documentation"这份资料中,应当包含详尽的操作说明、API参考以及可能的示范程序。这些资料会阐释如何在工程中整合并运用ARM DSP软件包,以及各个函数的具体功能和参数说明。例如,它可能会说明如何启动库,设定FFT的输入输出存储区,以及如何启动和结束FFT运算。对于PID控制器,资料会说明如何建立和配置PID对象,如何更新和获取控制器的状态,以及如何调整增益系数。 在实际项目执行中,掌握这些关键点对于提升嵌入式系统的运作效率至关重要。采用ARM官方的DSP软件包不仅可以增强代码的执行效能,...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值