一、联合体(共用体,union)
1. 核心概念
联合体是一种特殊的自定义数据类型,和结构体类似 ,但所有成员共享同一块内存空间,因此也叫 "共用体"。
- 关键字:
union - 内存分配规则:编译器只按最大成员的大小分配内存,保证能装下所有成员中占空间最大的那个。
- 特点:给其中一个成员赋值,其他成员的值会被覆盖(因为共享内存)。
2. 基础示例解析
#include <stdio.h>
// 联合体类型声明
union Un
{
char c; // 1字节
int i; // 4字节
};
int main()
{
union Un un = {0}; // 联合体变量初始化
printf("%d\n", sizeof(un)); // 输出:4
return 0;
}
为什么输出是 4?
- 联合体的大小 =最大成员的大小 (这里
int i****占 4 字节 ,char c占 1 字节 ),所以**sizeof(un)为 4。** - 注意:如果有内存对齐规则 ,联合体大小会是最大成员大小的整数倍,这里刚好满足,所以直接是 4。
3. 联合体的关键特性:地址共享
代码 1:验证地址相同
#include <stdio.h>
union Un
{
char c;
int i;
};
int main()
{
union Un un = {0};
printf("%p\n", &un.i); // 成员i的地址
printf("%p\n", &un.c); // 成员c的地址
printf("%p\n", &un); // 联合体变量本身的地址
return 0;
}
输出结果:三个地址完全相同
- 这直接证明了联合体的所有成员共享同一块内存的起始地址。
代码 2:验证赋值互相影响
#include <stdio.h>
union Un
{
char c;
int i;
};
int main()
{
union Un un = {0};
un.i = 0x11223344; // 给int成员赋值(十六进制)
un.c = 0x55; // 给char成员赋值
printf("%x\n", un.i); // 输出:11223355(小端机器)
return 0;
}
为什么输出是11223355?
- 小端机器下,
int 0x11223344在内存中按低地址到高地址存储为:44 33 22 11 - 给
un.c = 0x55,本质是修改了低地址的第 1 个字节 (也就是**44的位置** ),变成55, 所以最终的**int值变成0x11223355。**
4. 结构体 vs 联合体:内存对比(抽象一点就是富家千金和穷苦人家的区别)
| 特性 | 结构体(struct) |
联合体(union) |
|---|---|---|
| 内存分配 | 每个成员独立分配内存,总大小是所有成员大小之和(含对齐) | 所有成员共享同一块内存 ,大小为最大成员的大小(含对齐) |
| 成员关系 | 成员之间互不影响 | 成员之间互相覆盖 ,赋值会影响其他成员 |
| 适用场景 | 描述包含多个独立属性的对象 | 描述同一时间只需要存储其中一个属性的对象(比如多种类型的数据共用内存) |
5. 联合体大小的计算(核心规则)
联合体的大小计算遵循两条铁律:
- 基础规则 :联合体的大小至少等于最大成员的大小 (要能装下最大的那个成员)。
- 对齐规则 :联合体的大小必须是最大对齐数的整数倍 。
- 联合体的「最大对齐数」= 所有成员中,对齐数最大的那个(对齐数通常等于成员自身的大小 ,比如
int是 4 字节,对齐数就是 4)。
- 联合体的「最大对齐数」= 所有成员中,对齐数最大的那个(对齐数通常等于成员自身的大小 ,比如
题目计算:union Un1 和 union Un2
我们来一步步算:
1. union Un1
union Un1
{
char c[5]; // 大小:5字节,对齐数:1
int i; // 大小:4字节,对齐数:4
};
- 第一步:找最大成员大小 →
char c[5]是 5 字节。 - 第二步:找最大对齐数 →
int i的对齐数是 4。 - 第三步:向上对齐到最大对齐数的整数倍 →比 5 大的、4 的倍数是 8。
- 所以:
sizeof(union Un1) = 8
2. union Un2
union Un2
{
short c[7]; // 大小:7×2=14字节,对齐数:2
int i; // 大小:4字节,对齐数:4
};
- 第一步:找最大成员大小 →
short c[7]是 14 字节。 - 第二步:找最大对齐数 →
int i的对齐数是 4。 - 第三步:向上对齐到最大对齐数的整数倍 →比 14 大的、4 的倍数是 16。
- 所以:
sizeof(union Un2) = 16
✅ 最终输出结果:
8
16
6. 联合体的实际应用:礼品兑换单内存优化
⽐如,我们要搞⼀个活动,要上线⼀个礼品兑换单,礼品兑换单中有三种商品 :图书、杯⼦、衬衫。
每⼀种商品 都有:库存量、价格、商品类型和商品类型相关的其他信息。
图书:书名、作者、⻚数
杯⼦:设计
衬衫:设计、可选颜⾊、可选尺⼨
这个例子**完美体现了联合体「互斥属性共享内存」**的优势:
6.1 传统结构体写法(浪费内存)
struct gift_list
{
// 公共属性
int stock_number; // 4字节
double price; // 8字节
int item_type; // 4字节
// 所有商品的特殊属性(全部独立存储)
char title[20]; // 20字节(图书用)
char author[20]; // 20字节(图书用)
int num_pages; // 4字节(图书用)
char design[30]; // 30字节(杯子/衬衫用)
int colors; // 4字节(衬衫用)
int sizes; // 4字节(衬衫用)
};
- 问题:不管当前商品是哪种 ,所有属性都占用内存,比如图书会白白浪费杯子 / 衬衫的属性空间。
- 粗略大小(含对齐):
4+8+4 + 20+20+4 + 30+4+4 =98****字节 (实际对齐后更大)。
6.2 联合体优化写法(节省内存)
struct gift_list
{
// 公共属性(独立内存)
int stock_number; // 4字节
double price; // 8字节
int item_type; // 4字节
// 特殊属性(联合体共享内存)
union {
struct {
char title[20]; // 图书属性:20+20+4=44字节
char author[20];
int num_pages;
} book;
struct {
char design[30]; // 杯子属性:30字节
} mug;
struct {
char design[30]; // 衬衫属性:30+4+4=38字节
int colors;
int sizes;
} shirt;
} item; // 联合体大小 = 最大成员大小(44字节,对齐后为48字节)
};
- 优势:三种商品的特殊属性共享同一块内存,同一时间只占用其中一种属性的大小。
- 总大小(含对齐):
4+8+4 + 48 = 64字节,相比传统写法节省了近一半内存。
7. 联合体经典练习:判断大小端
这段代码利用联合体「成员共享内存」的特性,是 C 语言的经典面试题:
int check_sys()
{
union
{
int i; // 4字节
char c; // 1字节
} un;
un.i = 1;
return un.c; // 返回1:小端;返回0:大端
}
原理拆解
给**un.i = 1后,int 1** 在内存中的存储方式由大小端决定:
表格
| 机器类型 | 低地址字节(un.c读取的位置) |
高地址字节 | un.c的值 |
判断结果 |
|---|---|---|---|---|
| 小端(低地址存低位) | 0x01 |
0x00 0x00 0x00 |
1 |
小端 |
| 大端(低地址存高位) | 0x00 |
0x00 0x00 0x01 |
0 |
大端 |
8. 易错点提醒
- 联合体大小不是简单的 "最大成员大小" :必须考虑对齐规则,比如
char c[5]的联合体最终大小是 8 而不是 5。 - 联合体同一时间只能用一个成员 :给一个成员赋值后,其他成员的值会被覆盖,不要同时读写多个成员。
- 联合体不能直接整体赋值 :C 语言中联合体不能直接赋值,只能通过成员访问赋值。
二、枚举类型是什么?
枚举(enum)就是把变量所有可能的取值一一列举出来 ,专门用来表示有限且固定的选项 ,比如星期、性别、颜色、状态等。
- 关键字:
enum - 核心特点:枚举常量本质是
int类型的常量 ,但自带类型检查 ,比直接用**数字或#define**更安全。
1. 枚举类型的声明与赋值
1.1 基础声明(默认值)
// 星期枚举:默认从0开始,依次+1
enum Day {
Mon, // 0
Tues, // 1
Wed, // 2
Thur, // 3
Fri, // 4
Sat, // 5
Sun // 6
};
// 性别枚举
enum Sex {
MALE, // 0
FEMALE, // 1
SECRET // 2
};
{}里的内容叫枚举常量 ,默认从0开始 ,后面的常量依次在前一个基础上 + 1。
2. 手动指定初始值
你也可以手动给枚举常量赋值,后续常量会在前一个的基础上 + 1:
enum Color {
RED=2, // 手动赋值为2
GREEN=4, // 手动赋值为4
BLUE=8 // 手动赋值为8
};
2. 枚举 vs #define:为什么更推荐用枚举?
枚举的 5 个核心优势
| 对比项 | #define 定义常量 |
枚举(enum) |
|---|---|---|
| 类型检查 | 无类型检查,只是文本替换,容易出现非法赋值 | 有严格的类型检查 ,只能赋值枚举常量,更安全 |
| 可读性 | 常量分散,语义弱,不知道这些常量属于同一类 | 集中定义,一看就知道是同一类数据,比如RED/GREEN/BLUE |
| 调试友好 | 预处理阶段会被替换成数字,调试时看不到符号名 | 编译器保留符号名 ,调试时能直接看到常量名 |
| 作用域 | 全局作用域,容易和其他宏 / 变量重名冲突 | 遵循作用域规则**,函数内声明的枚举只能在函数内使用** |
| 定义效率 | 一次只能定义一个常量 | 一次可以定义多个相关常量 ,代码更简洁 |
3. 枚举类型的使用与易错点
1. 正确用法:用枚举常量赋值
enum Color {
RED=1,
GREEN=2,
BLUE=4
};
int main()
{
enum Color clr = GREEN; // 正确:用枚举常量给枚举变量赋值
return 0;
}
2. 整数赋值的区别
- C 语言中 :可以直接用整数给枚举变量赋值 (比如 clr = 2
;),但不推荐,会失去枚举的类型检查优势。 - C++ 中 :不允许直接用整数赋值 ,必须用枚举常量 ,类型检查更严格。
4. 补充:枚举的常见实用场景
-
状态机(游戏 / 业务逻辑)
enum PlayerState { IDLE, // 待机 RUN, // 跑步 JUMP, // 跳跃 ATTACK, // 攻击 DEAD // 死亡 }; -
选项开关(位运算场景)
enum Permission { READ = 1, // 读权限 0001 WRITE = 2, // 写权限 0010 EXECUTE = 4 // 执行权限 0100 }; // 同时赋予读+写权限 int user_perm = READ | WRITE;
5. 易错点提醒
- 枚举常量本质是
int:sizeof(enum Color)通常等于**sizeof(int)**(4 字节),可以直接当整数用。 - 不要超出枚举范围赋值:虽然 C 语言允许,但超出范围的值会让代码逻辑混乱,失去枚举的意义。
- 枚举常量名要避免冲突 :枚举常量本质是全局符号,不要和其他变量 / 宏重名。