C语言入门指南:联合体与枚举
目录
引言
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):代码的“翻译官”
引言
如果你的C语言学到“自定义类型”这一块儿,特别是“联合体”和“枚举”,说明你的学习已经进入了一个非常关键的阶段!别担心,这些概念初看可能有点抽象,但它们其实非常实用,而且理解了之后,你会觉得“哇,原来程序还能这么写!”。
所以今天我们来聊点能让你们代码瞬间“高大上”起来的东西——联合体(Union)和枚举(Enum)。
很多初学者会觉得,我有int
、char
、float
这些基本类型,有struct
结构体就够了,为啥还要搞个“联合体”和“枚举”?这俩玩意儿是干啥的?有啥用?
问得好!这正是我们今天要解决的核心问题。我会用最通俗的语言,配合生动的比喻和实际的例子,让你不仅知道它们“是什么”,更明白“为什么”要用它们,以及“怎么用”它们。
1. 联合体 (Union) —— “内存共享大师”
我们先从“联合体”开始。这个名字听起来就有点“联合”、“共同”的意思,没错,它的核心思想就是共享。
1.1 什么是联合体?—— 一个“精打细算”的内存管家
想象一下,你是一个非常节俭的人,你的衣柜里只有一件衣服。这件衣服很神奇,它既可以是一件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
的地盘。它们是同一块物理内存。
这就是联合体最核心的特点:所有成员共享同一块内存空间。
1.2 什么联合体的大小是4?—— 代码验证
第一个例子:
#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恤了。
#include <stdio.h>union Un {char c;int i;
};int main() {union Un un;un.i = 0x11223344; // 给整数成员赋值一个十六进制数printf("赋值后 i 的值: %x\n", un.i); // 输出: 11223344un.c = 0x55; // 给字符成员赋值printf("修改 c 后 i 的值: %x\n", un.i); // 输出: 11223355return 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
本身,指向的是内存中的同一个地方。
1.4 联合体 vs 结构体 —— “合租” vs “整租”
为了更直观地理解,我们对比一下联合体和结构体。
// 结构体:每个成员都有自己的“单间”
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 c
和int i
都住在这个单间里。任何时候,这个房间里只能放c
或者i
的东西,不能同时放。虽然挤了点,但房租(内存)便宜多了!
1.5 联合体有啥用?—— “精打细算”的实战案例
讲了这么多,联合体到底能干啥?难道就是为了让我们写出让同事看不懂的“炫技”代码吗?当然不是!它的核心价值在于节省内存,尤其是在嵌入式开发或处理大量数据时,这一点至关重要。
讲义里给了一个非常经典的例子:礼品兑换系统。
假设我们要设计一个系统,里面有三种礼品:图书、杯子、衬衫。
每种礼品都有一些公共属性:库存量、价格、商品类型。
但也有一些专属属性:
- 图书:书名、作者、页数。
- 杯子:设计图案。
- 衬衫:设计图案、颜色、尺寸。
笨办法(只用结构体):
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
占用的总内存都大致相同,而且比“笨办法”小得多!因为我们不再为每种商品都预留所有可能用到的字段,而是“按需分配”,用联合体实现了内存的“动态共享”。
这就是联合体在实际项目中的巨大价值——在保证功能的前提下,最大限度地节省宝贵的内存资源。
1.6 小练习:用联合体判断“大端机”还是“小端机”
这是一个非常经典的面试题,也是联合体的一个巧妙应用。
什么是大端/小端?
这指的是计算机存储多字节数字时,字节的排列顺序。
- 小端 (Little-Endian):低位字节存放在内存的低地址处。这是我们最常见的,比如Intel的CPU。
- 大端 (Big-Endian):高位字节存放在内存的低地址处。比如一些网络协议、老式Mac电脑。
怎么用联合体判断?
#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
的第一个字节。 - 如果第一个字节是1,说明数字1的低位字节(就是1本身)存放在了低地址,那就是小端。
- 如果第一个字节是0,说明数字1的高位字节(一堆0)存放在了低地址,那就是大端。
是不是很巧妙?这个小练习完美体现了联合体“共享内存”的特性。
2. 枚举 (Enum) —— “让代码会说话”的命名大师
讲完了“内存共享大师”联合体,我们再来认识一下“代码可读性大师”——枚举(Enumeration)。
2.1 什么是枚举?—— 给数字穿上“有意义的外衣”
枚举,顾名思义,就是一一列举。
在编程中,我们经常会遇到一些变量,它的取值是有限的、固定的几个选项。
比如:
- 一周有7天:星期一、星期二...星期日。
- 性别:男、女、保密。
- 交通灯:红、黄、绿。
- 游戏角色状态:站立、行走、奔跑、跳跃、攻击。
在没有枚举之前,我们可能会用#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
... 这些叫做枚举常量。- 默认情况下,编译器会给这些常量自动分配整数值,从0开始递增。所以
Mon=0
,Tues=1
, ...,Sun=6
。 - 在代码中,我们可以直接用
Mon
、Tues
这些有意义的名字,而不是冷冰冰的数字0、1。
效果: 代码瞬间变得清晰易懂!if (today == Mon)
,一看就知道是在判断是不是星期一,根本不需要注释!
2.2 枚举常量的值可以自定义
虽然默认从0开始,但我们可以手动指定。
enum Color {RED = 2,GREEN = 4,BLUE = 8
};
这样,RED
的值就是2,GREEN
是4,BLUE
是8。为什么要这么设计?有时候是为了和硬件寄存器、网络协议等已有的数值规范对齐。
你也可以只给部分赋值,未赋值的会从上一个的值+1开始:
enum Status {OFF = 0,ON, // 未赋值,默认是 0+1 = 1STANDBY, // 未赋值,默认是 1+1 = 2ERROR = 100,FATAL // 未赋值,默认是 100+1 = 101
};
2.3 为什么用枚举?—— 它的五大优点
-
增加代码的可读性和可维护性 (最重要!) 这是枚举最大的价值。
if (status == ERROR)
比if (status == 3)
好懂得多。半年后你回来看代码,或者你的同事接手你的项目,都能快速理解。维护起来也方便,想改某个状态的值,在枚举定义里改一下就行,不用全局搜索替换数字。 -
有类型检查,更严谨
#define
定义的宏,在编译预处理阶段就被替换成数字了,编译器不知道它原来代表什么含义。 枚举则不同,enum Day today;
明确告诉编译器,today
是一个“星期”类型的变量。如果你不小心写today = 100;
(100不是一个合法的星期),一些严格的编译器(或在C++中)会发出警告。这能帮你提前发现潜在的逻辑错误。 -
便于调试 当你用调试器(Debugger)查看变量时,如果变量是枚举类型,调试器通常会显示
Mon
、Tues
这样的名字,而不是0、1。这能让你更快地定位问题。 -
使用方便,一次定义多个常量 用
#define
,你得写7行来定义一周的7天。用枚举,一个{}
就搞定了,整洁又高效。 -
遵循作用域规则 和变量一样,枚举类型可以定义在函数内部、全局等不同作用域,管理起来更灵活。
2.4 枚举怎么用?—— 实战演练
基本用法:定义、声明、赋值
#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++中,编译器会阻止你这样做,强制你写出更安全、更清晰的代码。
总结:
我们花了这么长的篇幅,终于把联合体和枚举这两个“自定义类型”讲完了。让我们最后再总结一下它们的精髓:
联合体 (Union):内存的“共享公寓”
- 核心思想:所有成员共用同一块内存。
- 目的:节省内存空间。特别适用于那些成员不会同时被使用的情况(如礼品系统的不同商品属性)。
- 特点:改一个,动全身。要时刻记住,你操作的是同一块内存的不同“视角”。
- 大小计算:至少是最大成员的大小,并且要考虑内存对齐。
- 使用场景:嵌入式开发、网络协议解析、需要极致优化内存的场合。
枚举 (Enum):代码的“翻译官”
- 核心思想:为一组相关的整型常量提供有意义的名字。
- 目的:大幅提升代码的可读性、可维护性和安全性。
- 特点:默认从0开始递增,值可自定义。有类型检查(比
#define
强)。 - 使用场景:任何有固定、有限选项的地方!星期、月份、状态、颜色、方向... 无处不在!
当然,这是C语言入门指南的最后一篇啦,完结撒花*★,°*:.☆( ̄▽ ̄)/$:*.°★* 。
后续会更新数据结构及C++的内容
敬请期待哦
加油!期待看到你写出更棒的代码!