
如果你的C语言学到“自定义类型”这一块儿,特别是“联合体”和“枚举”,说明你的学习已经进入了一个非常关键的阶段!别担心,这些概念初看可能有点抽象,但它们其实非常实用,而且理解了之后,你会觉得“哇,原来程序还能这么写!”。
所以今天我们来聊点能让你们代码瞬间“高大上”起来的东西——联合体(Union)和枚举(Enum)。
很多初学者会觉得,我有int、char、float这些基本类型,有struct结构体就够了,为啥还要搞个“联合体”和“枚举”?这俩玩意儿是干啥的?有啥用?
问得好!这正是我们今天要解决的核心问题。我会用最通俗的语言,配合生动的比喻和实际的例子,让你不仅知道它们“是什么”,更明白“为什么”要用它们,以及“怎么用”它们。
我们先从“联合体”开始。这个名字听起来就有点“联合”、“共同”的意思,没错,它的核心思想就是共享。
想象一下,你是一个非常节俭的人,你的衣柜里只有一件衣服。这件衣服很神奇,它既可以是一件T恤(夏天穿),也可以是一件羽绒服(冬天穿),还可以是一件西装(面试穿)。
但是! 你一次只能把它当成其中一种衣服来穿。你不能同时穿着T恤、羽绒服和西装出门,对吧?你必须选择一种状态。
联合体(Union)在内存里扮演的就是这个“神奇衣服”的角色。
在C语言里,我们通常用struct(结构体)来把不同类型的数据打包在一起。比如,一个学生的信息:
struct Student {
char name[20]; // 姓名
int age; // 年龄
float score; // 分数
};当你创建一个struct Student变量时,计算机会为name、age、score这三个成员各自分配独立的内存空间。它们互不干扰,井水不犯河水。
而联合体则完全不同!
union Un {
char c; // 一个字符,占1个字节
int i; // 一个整数,通常占4个字节
};当你创建一个union Un变量时,计算机会怎么做呢?它不会傻乎乎地给c和i都分配空间。它会说:“你们俩共用一块地盘吧!这块地盘的大小,就按你们中个头最大的那个来算。”
在这个例子里,int通常是4个字节,char是1个字节,所以计算机会分配4个字节的内存给这个联合体变量。这4个字节,既是c的地盘,也是i的地盘。它们是同一块物理内存。
这就是联合体最核心的特点:所有成员共享同一块内存空间。
第一个例子:
#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,这块内存都够用。
因为所有成员共用同一块内存,所以你给任何一个成员赋值,都会影响到其他成员的值。这就像你把那件“神奇衣服”从T恤状态换成了羽绒服状态,它就不再是T恤了。
#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;
}发生了什么?
un.i设置为0x11223344。在内存中,这4个字节从低到高(假设是小端机)存的是:44, 33, 22, 11。un.c。因为c是char,它只占1个字节,而且它和i共享内存的起始位置。所以,修改c,实际上就是修改了i所占4个字节中的第一个字节。44改成了55,所以整个int的值就从0x11223344变成了0x11223355。再看一个地址的例子,证明它们是“一家人”:
#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.i、un.c和un本身,指向的是内存中的同一个地方。
为了更直观地理解,我们对比一下联合体和结构体。
// 结构体:每个成员都有自己的“单间”
struct S {
char c; // 1字节
int i; // 4字节
};
// sizeof(struct S) 通常是 8 字节 (因为内存对齐)
// 联合体:所有成员“合租”一个“大房间”
union U {
char c; // 1字节
int i; // 4字节
};
// sizeof(union U) 是 4 字节char c住一个小房间(1平米),int i住一个大房间(4平米),中间可能还有过道(内存对齐填充)。你们互不打扰,但总房租(内存)比较高。char c和int i都住在这个单间里。任何时候,这个房间里只能放c或者i的东西,不能同时放。虽然挤了点,但房租(内存)便宜多了!讲了这么多,联合体到底能干啥?难道就是为了让我们写出让同事看不懂的“炫技”代码吗?当然不是!它的核心价值在于节省内存,尤其是在嵌入式开发或处理大量数据时,这一点至关重要。
讲义里给了一个非常经典的例子:礼品兑换系统。
假设我们要设计一个系统,里面有三种礼品:图书、杯子、衬衫。
每种礼品都有一些公共属性:库存量、价格、商品类型。
但也有一些专属属性:
笨办法(只用结构体):
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_title、book_author、book_pages、shirt_color、shirt_size这些字段都是完全用不到的,但它们依然占据着内存空间!一个杯子白白浪费了几十个字节。
聪明办法(联合体闪亮登场):
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是一个联合体,它包含了三个结构体:book、mug、shirt。shirt(假设char[30] + int + int约38字节),所以info只占用大约38字节的内存。info.book部分,info.mug和info.shirt的内存虽然存在,但我们不去碰它,也不会造成逻辑错误。info.mug;是衬衫时,只用info.shirt。效果: 无论商品是什么类型,struct Gift占用的总内存都大致相同,而且比“笨办法”小得多!因为我们不再为每种商品都预留所有可能用到的字段,而是“按需分配”,用联合体实现了内存的“动态共享”。
这就是联合体在实际项目中的巨大价值——在保证功能的前提下,最大限度地节省宝贵的内存资源。
这是一个非常经典的面试题,也是联合体的一个巧妙应用。
什么是大端/小端?
这指的是计算机存储多字节数字时,字节的排列顺序。
怎么用联合体判断?
#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;
}原理:
int和一个char。int赋值1。char的值。因为char和int共享内存的起始地址,所以char读到的就是int的第一个字节。是不是很巧妙?这个小练习完美体现了联合体“共享内存”的特性。
讲完了“内存共享大师”联合体,我们再来认识一下“代码可读性大师”——枚举(Enumeration)。
枚举,顾名思义,就是一一列举。
在编程中,我们经常会遇到一些变量,它的取值是有限的、固定的几个选项。
比如:
在没有枚举之前,我们可能会用#define宏或者直接用数字来表示:
#define MONDAY 0
#define TUESDAY 1
#define WEDNESDAY 2
// ... 以此类推
int today = MONDAY;
if (today == MONDAY) {
printf("又是周一,不想上班!\n");
}或者更偷懒的:
int today = 0; // 0代表周一
if (today == 0) {
printf("又是周一,不想上班!\n");
}这样写有什么问题?
if (today == 0),你得去翻半天注释或者宏定义才知道0代表周一。时间久了,你自己都可能忘记。if (today == 7),而7并没有定义,程序可能出错或者产生不符合预期的结果。枚举就是为了解决这些问题而生的!
// 声明一个枚举类型,叫 Day (星期)
enum Day {
Mon, // 星期一
Tues, // 星期二
Wed, // 星期三
Thur, // 星期四
Fri, // 星期五
Sat, // 星期六
Sun // 星期日
};
// 声明一个枚举变量
enum Day today = Mon;
if (today == Mon) {
printf("又是周一,不想上班!\n");
}发生了什么变化?
enum Day定义了一种新的“数据类型”,它专门用来表示星期。{}里面列举了这种类型所有可能的取值:Mon, Tues, Wed... 这些叫做枚举常量。Mon=0, Tues=1, ..., Sun=6。Mon、Tues这些有意义的名字,而不是冷冰冰的数字0、1。效果: 代码瞬间变得清晰易懂!if (today == Mon),一看就知道是在判断是不是星期一,根本不需要注释!
虽然默认从0开始,但我们可以手动指定。
enum Color {
RED = 2,
GREEN = 4,
BLUE = 8
};这样,RED的值就是2,GREEN是4,BLUE是8。为什么要这么设计?有时候是为了和硬件寄存器、网络协议等已有的数值规范对齐。
你也可以只给部分赋值,未赋值的会从上一个的值+1开始:
enum Status {
OFF = 0,
ON, // 未赋值,默认是 0+1 = 1
STANDBY, // 未赋值,默认是 1+1 = 2
ERROR = 100,
FATAL // 未赋值,默认是 100+1 = 101
};if (status == ERROR) 比 if (status == 3) 好懂得多。半年后你回来看代码,或者你的同事接手你的项目,都能快速理解。维护起来也方便,想改某个状态的值,在枚举定义里改一下就行,不用全局搜索替换数字。
#define定义的宏,在编译预处理阶段就被替换成数字了,编译器不知道它原来代表什么含义。 枚举则不同,enum Day today; 明确告诉编译器,today是一个“星期”类型的变量。如果你不小心写 today = 100; (100不是一个合法的星期),一些严格的编译器(或在C++中)会发出警告。这能帮你提前发现潜在的逻辑错误。
Mon、Tues这样的名字,而不是0、1。这能让你更快地定位问题。
#define,你得写7行来定义一周的7天。用枚举,一个{}就搞定了,整洁又高效。
基本用法:定义、声明、赋值
#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;
}一个更复杂的例子:游戏角色状态
#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类型,这使得函数的意图非常清晰。调用者也只能传入IDLE、WALKING等预定义的状态,减少了传入非法值的可能性。
关于“整数赋值”的小插曲
讲义最后提到一个细节:
那是否可以拿整数给枚举变量赋值呢?在C语言中是可以的,但是在C++是不行的,C++的类型检查比较严格。
是的,在C语言中,下面的代码是合法的:
enum Color clr;
clr = 2; // C语言允许,但强烈不建议!虽然合法,但这完全违背了我们使用枚举的初衷!你又把清晰的代码变回了模糊的数字。所以,作为一个有追求的程序员,请永远使用枚举常量(如RED, GREEN)来给枚举变量赋值,不要用数字!
在C++中,编译器会阻止你这样做,强制你写出更安全、更清晰的代码。
我们花了这么长的篇幅,终于把联合体和枚举这两个“自定义类型”讲完了。让我们最后再总结一下它们的精髓:
#define强)。当然,这是C语言入门指南的最后一篇啦,完结撒花*★,°*:.☆( ̄▽ ̄)/$:*.°★* 。
后续会更新数据结构及C++的内容
敬请期待哦
加油!期待看到你写出更棒的代码!