首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >C语言入门指南:联合体与枚举

C语言入门指南:联合体与枚举

作者头像
给东岸来杯冷咖啡
发布2026-01-12 20:15:03
发布2026-01-12 20:15:03
980
举报

引言

如果你的C语言学到“自定义类型”这一块儿,特别是“联合体”和“枚举”,说明你的学习已经进入了一个非常关键的阶段!别担心,这些概念初看可能有点抽象,但它们其实非常实用,而且理解了之后,你会觉得“哇,原来程序还能这么写!”。

所以今天我们来聊点能让你们代码瞬间“高大上”起来的东西——联合体(Union)和枚举(Enum)

很多初学者会觉得,我有intcharfloat这些基本类型,有struct结构体就够了,为啥还要搞个“联合体”和“枚举”?这俩玩意儿是干啥的?有啥用?

问得好!这正是我们今天要解决的核心问题。我会用最通俗的语言,配合生动的比喻和实际的例子,让你不仅知道它们“是什么”,更明白“为什么”要用它们,以及“怎么用”它们。

1. 联合体 (Union) —— “内存共享大师”

我们先从“联合体”开始。这个名字听起来就有点“联合”、“共同”的意思,没错,它的核心思想就是共享

1.1 什么是联合体?—— 一个“精打细算”的内存管家

想象一下,你是一个非常节俭的人,你的衣柜里只有一件衣服。这件衣服很神奇,它既可以是一件T恤(夏天穿),也可以是一件羽绒服(冬天穿),还可以是一件西装(面试穿)。

但是! 你一次只能把它当成其中一种衣服来穿。你不能同时穿着T恤、羽绒服和西装出门,对吧?你必须选择一种状态。

联合体(Union)在内存里扮演的就是这个“神奇衣服”的角色。

在C语言里,我们通常用struct(结构体)来把不同类型的数据打包在一起。比如,一个学生的信息:

代码语言:javascript
复制
struct Student {
    char name[20]; // 姓名
    int age;       // 年龄
    float score;   // 分数
};

当你创建一个struct Student变量时,计算机会为nameagescore这三个成员各自分配独立的内存空间。它们互不干扰,井水不犯河水。

而联合体则完全不同!

代码语言:javascript
复制
union Un {
    char c; // 一个字符,占1个字节
    int i;  // 一个整数,通常占4个字节
};

当你创建一个union Un变量时,计算机会怎么做呢?它不会傻乎乎地给ci都分配空间。它会说:“你们俩共用一块地盘吧!这块地盘的大小,就按你们中个头最大的那个来算。”

在这个例子里,int通常是4个字节,char是1个字节,所以计算机会分配4个字节的内存给这个联合体变量。这4个字节,既是c的地盘,也是i的地盘。它们是同一块物理内存

这就是联合体最核心的特点:所有成员共享同一块内存空间。

1.2 什么联合体的大小是4?—— 代码验证

第一个例子:

代码语言:javascript
复制
#include <stdio.h>

union Un {
    char c;
    int i;
};

int main() {
    union Un un = {0}; // 定义并初始化联合体变量
    printf("%d\n", sizeof(un)); // 打印它的大小
    return 0;
}

运行结果:

为什么不是1(char的大小),也不是5(1+4)?因为联合体只为最大的成员(int,4字节)分配空间。它必须保证,无论你存的是char还是int,这块内存都够用。

1.3 共享内存意味着什么?—— “牵一发而动全身”

因为所有成员共用同一块内存,所以你给任何一个成员赋值,都会影响到其他成员的值。这就像你把那件“神奇衣服”从T恤状态换成了羽绒服状态,它就不再是T恤了。

代码语言:javascript
复制
#include <stdio.h>

union Un {
    char c;
    int i;
};

int main() {
    union Un un;
    un.i = 0x11223344; // 给整数成员赋值一个十六进制数
    printf("赋值后 i 的值: %x\n", un.i); // 输出: 11223344

    un.c = 0x55; // 给字符成员赋值
    printf("修改 c 后 i 的值: %x\n", un.i); // 输出: 11223355

    return 0;
}

发生了什么?

  1. 我们先把un.i设置为0x11223344。在内存中,这4个字节从低到高(假设是小端机)存的是:44, 33, 22, 11
  2. 然后,我们修改un.c。因为cchar,它只占1个字节,而且它和i共享内存的起始位置。所以,修改c,实际上就是修改了i所占4个字节中的第一个字节
  3. 把第一个字节从44改成了55,所以整个int的值就从0x11223344变成了0x11223355

再看一个地址的例子,证明它们是“一家人”:

代码语言:javascript
复制
#include <stdio.h>

union Un {
    char c;
    int i;
};

int main() {
    union Un un;
    printf("un.i 的地址: %p\n", &(un.i));
    printf("un.c 的地址: %p\n", &(un.c));
    printf("un 的地址: %p\n", &un);
    return 0;
}

运行结果会发现,这三个地址完全一模一样!这铁证如山地说明了,un.iun.cun本身,指向的是内存中的同一个地方。

1.4 联合体 vs 结构体 —— “合租” vs “整租”

为了更直观地理解,我们对比一下联合体和结构体。

代码语言:javascript
复制
// 结构体:每个成员都有自己的“单间”
struct S {
    char c; // 1字节
    int i;  // 4字节
};
// sizeof(struct S) 通常是 8 字节 (因为内存对齐)

// 联合体:所有成员“合租”一个“大房间”
union U {
    char c; // 1字节
    int i;  // 4字节
};
// sizeof(union U) 是 4 字节
  • 结构体 (Struct):就像你租了一套两居室的房子。char c住一个小房间(1平米),int i住一个大房间(4平米),中间可能还有过道(内存对齐填充)。你们互不打扰,但总房租(内存)比较高。
  • 联合体 (Union):就像你租了一个4平米的单间。char cint i都住在这个单间里。任何时候,这个房间里只能放c或者i的东西,不能同时放。虽然挤了点,但房租(内存)便宜多了!

1.5 联合体有啥用?—— “精打细算”的实战案例

讲了这么多,联合体到底能干啥?难道就是为了让我们写出让同事看不懂的“炫技”代码吗?当然不是!它的核心价值在于节省内存,尤其是在嵌入式开发或处理大量数据时,这一点至关重要。

讲义里给了一个非常经典的例子:礼品兑换系统

假设我们要设计一个系统,里面有三种礼品:图书杯子衬衫

每种礼品都有一些公共属性:库存量、价格、商品类型。

但也有一些专属属性

  • 图书:书名、作者、页数。
  • 杯子:设计图案。
  • 衬衫:设计图案、颜色、尺寸。

笨办法(只用结构体):

代码语言:javascript
复制
struct Gift {
    int stock; // 库存
    double price; // 价格
    int type; // 类型:0=图书, 1=杯子, 2=衬衫

    // 下面是所有礼品的属性大杂烩
    char book_title[20]; // 书名
    char book_author[20]; // 作者
    int book_pages; // 页数

    char mug_design[30]; // 杯子设计

    char shirt_design[30]; // 衬衫设计
    int shirt_color; // 颜色
    int shirt_size; // 尺寸
};

这个设计简单粗暴,但问题很大:极度浪费内存!

想象一下,当这个Gift变量代表一个杯子时,book_titlebook_authorbook_pagesshirt_colorshirt_size这些字段都是完全用不到的,但它们依然占据着内存空间!一个杯子白白浪费了几十个字节。

聪明办法(联合体闪亮登场):

代码语言:javascript
复制
struct Gift {
    int stock; // 库存
    double price; // 价格
    int type; // 类型

    // 关键来了!用联合体来存放“专属属性”
    union {
        struct { // 图书的专属属性
            char title[20];
            char author[20];
            int pages;
        } book;

        struct { // 杯子的专属属性
            char design[30];
        } mug;

        struct { // 衬衫的专属属性
            char design[30];
            int color;
            int size;
        } shirt;
    } info; // 我们把这个联合体叫做 info
};

这个设计的精妙之处在哪?

  • info是一个联合体,它包含了三个结构体:bookmugshirt
  • 这三个结构体中,最大的是shirt(假设char[30] + int + int约38字节),所以info只占用大约38字节的内存。
  • 当商品是图书时,我们只使用info.book部分,info.muginfo.shirt的内存虽然存在,但我们不去碰它,也不会造成逻辑错误。
  • 同理,商品是杯子时,只用info.mug;是衬衫时,只用info.shirt

效果: 无论商品是什么类型,struct Gift占用的总内存都大致相同,而且比“笨办法”小得多!因为我们不再为每种商品都预留所有可能用到的字段,而是“按需分配”,用联合体实现了内存的“动态共享”。

这就是联合体在实际项目中的巨大价值——在保证功能的前提下,最大限度地节省宝贵的内存资源。

1.6 小练习:用联合体判断“大端机”还是“小端机”

这是一个非常经典的面试题,也是联合体的一个巧妙应用。

什么是大端/小端?

这指的是计算机存储多字节数字时,字节的排列顺序。

  • 小端 (Little-Endian):低位字节存放在内存的低地址处。这是我们最常见的,比如Intel的CPU。
  • 大端 (Big-Endian):高位字节存放在内存的低地址处。比如一些网络协议、老式Mac电脑。

怎么用联合体判断?

代码语言:javascript
复制
#include <stdio.h>

int check_sys() {
    union {
        int i;
        char c;
    } un;

    un.i = 1; // 给整数赋值1
    // 1的二进制是 00000000 00000000 00000000 00000001
    // 如果是小端机,最低地址存的是 00000001 (即1)
    // 如果是大端机,最低地址存的是 00000000 (即0)
    return un.c; // 返回第一个字节的值
}

int main() {
    if (check_sys() == 1) {
        printf("恭喜!你的电脑是小端机。\n");
    } else {
        printf("你的电脑是大端机。\n");
    }
    return 0;
}

原理:

  1. 我们定义了一个联合体,包含一个int和一个char
  2. int赋值1。
  3. 读取char的值。因为charint共享内存的起始地址,所以char读到的就是int第一个字节
  4. 如果第一个字节是1,说明数字1的低位字节(就是1本身)存放在了低地址,那就是小端
  5. 如果第一个字节是0,说明数字1的高位字节(一堆0)存放在了低地址,那就是大端

是不是很巧妙?这个小练习完美体现了联合体“共享内存”的特性。

2. 枚举 (Enum) —— “让代码会说话”的命名大师

讲完了“内存共享大师”联合体,我们再来认识一下“代码可读性大师”——枚举(Enumeration)

2.1 什么是枚举?—— 给数字穿上“有意义的外衣”

枚举,顾名思义,就是一一列举

在编程中,我们经常会遇到一些变量,它的取值是有限的、固定的几个选项

比如:

  • 一周有7天:星期一、星期二...星期日。
  • 性别:男、女、保密。
  • 交通灯:红、黄、绿。
  • 游戏角色状态:站立、行走、奔跑、跳跃、攻击。

在没有枚举之前,我们可能会用#define宏或者直接用数字来表示:

代码语言:javascript
复制
#define MONDAY 0
#define TUESDAY 1
#define WEDNESDAY 2
// ... 以此类推

int today = MONDAY;
if (today == MONDAY) {
    printf("又是周一,不想上班!\n");
}

或者更偷懒的:

代码语言:javascript
复制
int today = 0; // 0代表周一
if (today == 0) {
    printf("又是周一,不想上班!\n");
}

这样写有什么问题?

  1. 可读性差:看到if (today == 0),你得去翻半天注释或者宏定义才知道0代表周一。时间久了,你自己都可能忘记。
  2. 容易出错:万一手滑写成if (today == 7),而7并没有定义,程序可能出错或者产生不符合预期的结果。
  3. 维护困难:如果你想在星期一和星期二之间加个“星期一点五”,你需要手动调整后面所有宏定义的值,非常麻烦。

枚举就是为了解决这些问题而生的!

代码语言:javascript
复制
// 声明一个枚举类型,叫 Day (星期)
enum Day {
    Mon,  // 星期一
    Tues, // 星期二
    Wed,  // 星期三
    Thur, // 星期四
    Fri,  // 星期五
    Sat,  // 星期六
    Sun   // 星期日
};

// 声明一个枚举变量
enum Day today = Mon;

if (today == Mon) {
    printf("又是周一,不想上班!\n");
}

发生了什么变化?

  • 我们用enum Day定义了一种新的“数据类型”,它专门用来表示星期。
  • {}里面列举了这种类型所有可能的取值:Mon, Tues, Wed... 这些叫做枚举常量
  • 默认情况下,编译器会给这些常量自动分配整数值,从0开始递增。所以Mon=0, Tues=1, ..., Sun=6
  • 在代码中,我们可以直接用MonTues这些有意义的名字,而不是冷冰冰的数字0、1。

效果: 代码瞬间变得清晰易懂!if (today == Mon),一看就知道是在判断是不是星期一,根本不需要注释!

2.2 枚举常量的值可以自定义

虽然默认从0开始,但我们可以手动指定。

代码语言:javascript
复制
enum Color {
    RED = 2,
    GREEN = 4,
    BLUE = 8
};

这样,RED的值就是2,GREEN是4,BLUE是8。为什么要这么设计?有时候是为了和硬件寄存器、网络协议等已有的数值规范对齐。

你也可以只给部分赋值,未赋值的会从上一个的值+1开始:

代码语言:javascript
复制
enum Status {
    OFF = 0,
    ON,      // 未赋值,默认是 0+1 = 1
    STANDBY, // 未赋值,默认是 1+1 = 2
    ERROR = 100,
    FATAL    // 未赋值,默认是 100+1 = 101
};

2.3 为什么用枚举?—— 它的五大优点

  1. 增加代码的可读性和可维护性 (最重要!) 这是枚举最大的价值。if (status == ERROR)if (status == 3) 好懂得多。半年后你回来看代码,或者你的同事接手你的项目,都能快速理解。维护起来也方便,想改某个状态的值,在枚举定义里改一下就行,不用全局搜索替换数字。
  2. 有类型检查,更严谨 #define定义的宏,在编译预处理阶段就被替换成数字了,编译器不知道它原来代表什么含义。 枚举则不同,enum Day today; 明确告诉编译器,today是一个“星期”类型的变量。如果你不小心写 today = 100; (100不是一个合法的星期),一些严格的编译器(或在C++中)会发出警告。这能帮你提前发现潜在的逻辑错误。
  3. 便于调试 当你用调试器(Debugger)查看变量时,如果变量是枚举类型,调试器通常会显示MonTues这样的名字,而不是0、1。这能让你更快地定位问题。
  4. 使用方便,一次定义多个常量#define,你得写7行来定义一周的7天。用枚举,一个{}就搞定了,整洁又高效。
  5. 遵循作用域规则 和变量一样,枚举类型可以定义在函数内部、全局等不同作用域,管理起来更灵活。
2.4 枚举怎么用?—— 实战演练

基本用法:定义、声明、赋值

代码语言:javascript
复制
#include <stdio.h>

// 1. 声明枚举类型
enum TrafficLight {
    RED,
    YELLOW,
    GREEN
};

int main() {
    // 2. 声明枚举变量
    enum TrafficLight current_light;

    // 3. 给枚举变量赋值
    current_light = RED;

    // 4. 使用枚举常量进行判断
    if (current_light == RED) {
        printf("红灯停!\n");
    } else if (current_light == YELLOW) {
        printf("黄灯等一等!\n");
    } else if (current_light == GREEN) {
        printf("绿灯行!\n");
    }

    return 0;
}

一个更复杂的例子:游戏角色状态

代码语言:javascript
复制
#include <stdio.h>

enum PlayerState {
    IDLE,    // 空闲/站立
    WALKING, // 行走
    RUNNING, // 奔跑
    JUMPING, // 跳跃
    ATTACKING // 攻击
};

void update_player_animation(enum PlayerState state) {
    switch (state) {
        case IDLE:
            printf("播放站立动画\n");
            break;
        case WALKING:
            printf("播放行走动画\n");
            break;
        case RUNNING:
            printf("播放奔跑动画\n");
            break;
        case JUMPING:
            printf("播放跳跃动画\n");
            break;
        case ATTACKING:
            printf("播放攻击动画\n");
            break;
        default:
            printf("未知状态,播放默认动画\n");
    }
}

int main() {
    enum PlayerState player = IDLE;
    update_player_animation(player); // 输出: 播放站立动画

    player = RUNNING;
    update_player_animation(player); // 输出: 播放奔跑动画

    return 0;
}

在这个例子中,update_player_animation函数的参数明确要求是enum PlayerState类型,这使得函数的意图非常清晰。调用者也只能传入IDLEWALKING等预定义的状态,减少了传入非法值的可能性。

关于“整数赋值”的小插曲

讲义最后提到一个细节:

那是否可以拿整数给枚举变量赋值呢?在C语言中是可以的,但是在C++是不行的,C++的类型检查比较严格。

是的,在C语言中,下面的代码是合法的:

代码语言:javascript
复制
enum Color clr;
clr = 2; // C语言允许,但强烈不建议!

虽然合法,但这完全违背了我们使用枚举的初衷!你又把清晰的代码变回了模糊的数字。所以,作为一个有追求的程序员,请永远使用枚举常量(如RED, GREEN)来给枚举变量赋值,不要用数字!

在C++中,编译器会阻止你这样做,强制你写出更安全、更清晰的代码。

总结:

我们花了这么长的篇幅,终于把联合体和枚举这两个“自定义类型”讲完了。让我们最后再总结一下它们的精髓:

联合体 (Union):内存的“共享公寓”
  • 核心思想:所有成员共用同一块内存
  • 目的节省内存空间。特别适用于那些成员不会同时被使用的情况(如礼品系统的不同商品属性)。
  • 特点:改一个,动全身。要时刻记住,你操作的是同一块内存的不同“视角”。
  • 大小计算:至少是最大成员的大小,并且要考虑内存对齐。
  • 使用场景:嵌入式开发、网络协议解析、需要极致优化内存的场合。
枚举 (Enum):代码的“翻译官”
  • 核心思想:为一组相关的整型常量提供有意义的名字
  • 目的大幅提升代码的可读性、可维护性和安全性
  • 特点:默认从0开始递增,值可自定义。有类型检查(比#define强)。
  • 使用场景:任何有固定、有限选项的地方!星期、月份、状态、颜色、方向... 无处不在!

当然,这是C语言入门指南的最后一篇啦,完结撒花*★,°*:.☆( ̄▽ ̄)/$:*.°★* 。

后续会更新数据结构及C++的内容

敬请期待哦

加油!期待看到你写出更棒的代码!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-10-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言
  • 1. 联合体 (Union) —— “内存共享大师”
    • 1.1 什么是联合体?—— 一个“精打细算”的内存管家
    • 1.2 什么联合体的大小是4?—— 代码验证
    • 1.3 共享内存意味着什么?—— “牵一发而动全身”
    • 1.4 联合体 vs 结构体 —— “合租” vs “整租”
    • 1.5 联合体有啥用?—— “精打细算”的实战案例
    • 1.6 小练习:用联合体判断“大端机”还是“小端机”
  • 2. 枚举 (Enum) —— “让代码会说话”的命名大师
    • 2.1 什么是枚举?—— 给数字穿上“有意义的外衣”
    • 2.2 枚举常量的值可以自定义
    • 2.3 为什么用枚举?—— 它的五大优点
      • 2.4 枚举怎么用?—— 实战演练
  • 总结:
    • 联合体 (Union):内存的“共享公寓”
    • 枚举 (Enum):代码的“翻译官”
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档