1.联合体
1.1联合体类型的说明
联合体(union)与结构体类似,由一个或多个成员构成,这些成员可以是不同的类型。但联合体的特点是所有成员共用同一块内存空间,因此联合体也称为共用体。
c
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
// 联合类型的声明
union Un
{
char c;//1
int i;//4
};
int main()
{
// 联合变量的定义
union Un un = { 0 };
// 计算整个变量的大小
printf("%zd\n", sizeof(un)); // 输出:4
return 0;
}

那么为什么会是4呢,接下来我们来探究一下。
-
联合体的大小至少是最大成员的大小
-
这里最大成员是int i,占4个字节
-
所以union Un的大小是4字节
1.2联合体的特点
联合的成员是共用同⼀块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合体至少得有能力保存最大的那个成员)。
1.2.1代码1:地址验证
c
#include <stdio.h>
union Un
{
char c;
int i;
};
int main()
{
union Un un = {0};
// 下面输出的结果是一样的吗?
printf("%p\n", &(un.i));
printf("%p\n", &(un.c));
printf("%p\n", &un);
return 0;
}

解释: 三个地址完全相同,证明所有成员共用同一块内存空间。
1.2.2代码2:内存共享验证
c
#include <stdio.h>
union Un
{
char c;
int i;
};
int main()
{
union Un un = {0};
un.i = 0x11223344; // 设置整数值
un.c = 0x55; // 修改char成员
printf("%x\n", un.i); // 输出:11223355
return 0;
}

我们可以发现,我们的值确实被修改了:
c
内存地址: 低地址 ----> 高地址
原始i的值: 44 33 22 11 (小端存储)
修改c后: 55 33 22 11 (只修改了第一个字节)
c
地址 修改前 修改后
0x001AF85C 44 -> 55 (c修改的位置)
0x001AF85D 33 -> 33
0x001AF85E 22 -> 22
0x001AF85F 11 -> 11
1.3相同成员的结构体和联合体对比
结构体 vs 联合体:
结构体:
c
struct S
{
char c; // 1字节
int i; // 4字节
};
struct S s = {0};
// sizeof(struct S) = 8字节(考虑内存对齐)
联合体:
c
union Un
{
char c; // 1字节
int i; // 4字节
};
union Un un = {0};
// sizeof(union Un) = 4字节
c
结构体struct S(8字节):
0x001AF85C: [c][填充][填充][填充] // char c + 3字节填充
0x001AF860: [i][i][i][i] // int i
联合体union Un(4字节):
0x001AF85C: [c/i] // char c和int i共用同一块内存
0x001AF85D: [i]
0x001AF85E: [i]
0x001AF85F: [i]
结构体的大小计算我们在上篇中说过了,这里就不再赘述了,而联合体是所有成员公用同一块空间。
区别:
-
结构体:每个成员有独立的内存空间,总大小是各成员大小之和(考虑对齐)
-
联合体:所有成员共用同一块内存,总大小由最大成员决定
1.4联合体大小的计算
计算规则:
1.联合的大小至少是最大成员的大小
2.当最大成员大小不是最大对齐数的整数倍时,要对齐到最大对齐数的整数倍
示例分析:
c
#include <stdio.h>
union Un1
{
char c[5]; // 大小5字节,对齐数1
int i; // 大小4字节,对齐数4
};
union Un2
{
short c[7]; // 大小14字节,对齐数2
int i; // 大小4字节,对齐数4
};
int main()
{
printf("%d\n", sizeof(union Un1)); // 输出:8
printf("%d\n", sizeof(union Un2)); // 输出:16
return 0;
}

在这里我们需要注意的一个点就是,大家可千万不要误以为联合体的大小就为所有成员中大小最大的那个数,你要这么说的话5不是比4大吗,那大小应该是5才对,所以在这里我们就需要注意了,这里不仅仅是看最大空间,还要考虑最大对齐数的整数倍才行。
1.5 联合体的实际应用
礼品兑换单优化示例
原始设计(浪费内存):
c
struct gift_list
{
// 公共属性
int stock_number; // 库存量
double price; // 定价
int item_type; // 商品类型
// 特殊属性 - 所有商品的属性都包含,浪费内存
char title[20]; // 书名 - 只有图书需要
char author[20]; // 作者 - 只有图书需要
int num_pages; // 页数 - 只有图书需要
char design[30]; // 设计 - 杯子和衬衫需要
int colors; // 颜色 - 只有衬衫需要
int sizes; // 尺寸 - 只有衬衫需要
};
上述的结构其实设计的很简单,用起来也方便,但是结构的设计中包含了所有礼品的各种属性,这样使得结构体的大小就会偏大,比较浪费内存。因为对于礼品兑换单中的商品来说,只有部分属性信息是常用的。比如:
商品是图书,就不需要design、colors、sizes。
所以我们就可以把公共属性单独写出来,剩余属于各种商品本⾝的属性使用联合体起来,这样就可以
介绍所需的内存空间,一定程度上节省了内存。
优化设计(使用联合体节省内存):
c
struct gift_list
{
int stock_number; // 库存量
double price; // 定价
int item_type; // 商品类型
union {
struct
{
char title[20]; // 书名
char author[20]; // 作者
int num_pages; // 页数
} book; // 图书特有属性
struct
{
char design[30]; // 设计
} mug; // 杯子特有属性
struct
{
char design[30]; // 设计
int colors; // 颜色
int sizes; // 尺寸
} shirt; // 衬衫特有属性
} item;
};
优势:
-
根据商品类型只存储相关的属性
-
显著节省内存空间
-
提高内存使用效率
1.6联合体练习:判断字节序
我们在之前就写过判断字节序的代码,在这里我们重新温习下之前的那种方式,然后再用联合体来搞定这道题目
c
int check_system()
{
int n = 1;
return *(char*)&n;
}
int main()
{
int ret = check_system();
if (ret == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}

那么对于上述代码我们都没有什么疑问对吧,接下来我们看看如何使用联合体来解决这道题目呢?
c
int check_sys()
{
union
{
int i;
char c;
} un;
un.i = 1;
return un.c; // 返回1是小端,返回0是大端
}
int main()
{
int ret = check_sys();
if (ret == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}

这里就巧妙的运用了联合体的特点对吧,i和c存储在同一块空间,un.c为第一个字节的内容,我们把第一个字节的内容返回是不是就可以了,这种思想就很巧妙,让人大吃一惊可谓是。
原理:
-
小端模式:低位字节存储在低地址
-
大端模式:高位字节存储在低地址
将int值1存入联合体,通过char读取第一个字节:
小端:第一个字节是1 → 返回1
大端:第一个字节是0 → 返回0
总结:
联合体的核心特性:
1.内存共享:所有成员共用同一块内存
2.大小优化:大小为最大成员的大小(考虑对齐)
3.类型安全:同一时间只能有效使用一个成员
适用场景:
-
需要节省内存的场景
-
同一时间只会使用其中一种类型的场景
-
需要实现变体记录的场景
-
系统编程中处理硬件寄存器
注意事项:
-
使用时要清楚当前哪个成员是有效的
-
修改一个成员会影响其他成员的值
-
不能同时使用多个成员存储不同的值
2.枚举类型
2.1枚举类型的声明
枚举(enumeration)顾名思义就是------列举,把可能的取值一一列举出来
现实生活中的枚举例子:
-
一周的星期一到星期日是有限的7天,可以一一列举
-
性别有:男、女、保密,也可以一一列举
-
月份有12个月,也可以一一列举
-
三原色,也可以一一列举
枚举类型的定义语法:
c
// 星期的枚举
enum Day
{
Mon, // 星期一
Tues, // 星期二
Wed, // 星期三
Thur, // 星期四
Fri, // 星期五
Sat, // 星期六
Sun // 星期日
};
// 性别的枚举
enum Sex
{
MALE, // 男
FEMALE, // 女
SECRET // 保密
};
// 颜色的枚举
enum Color
{
RED, // 红色
GREEN, // 绿色
BLUE // 蓝色
};
枚举常量的值:
枚举常量默认从0开始,依次递增1:
c
enum Color
{
RED, // 值为0
GREEN, // 值为1
BLUE // 值为2
};
也可以手动指定初始值:
c
enum Color
{
RED = 2, // 手动赋值为2
GREEN = 4, // 手动赋值为4
BLUE = 8 // 手动赋值为8
};
或者部分指定,部分自动递增:
c
enum Color
{
RED = 5, // 值为5
GREEN, // 自动为6
BLUE // 自动为7
};
2.2枚举类型的优点
为什么使用枚举?相比#define的优势:
使用#define定义常量:
c
#define RED 0
#define GREEN 1
#define BLUE 2
使用枚举定义常量:
c
enum Color
{
RED,
GREEN,
BLUE
};
枚举的优点:
1.增加代码的可读性和可维护性
比如一下例子,我们说等于0时,可能常与上下文或者菜单有联系,但是使用枚举我们就不知道具体是干什么的,是什么意思
c
// 可读性差
if (color == 0) { /* ... */ }
// 可读性好
if (color == RED) { /* ... */ }
2.类型检查,更加严谨
c
enum Color color = RED; // 正确
enum Color color = 100; // C语言中可能允许,但C++中会报错,有类型检查
因为本质上enum的数也是常量,而众所周知,我们是不能给常量复制的,所以会报错
3.便于调试
-
#define定义的符号在预处理阶段被替换,调试时看不到原始标识符
-
枚举常量在调试时可以看到有意义的名称
2.3 枚举类型的使用
基本使用方法:
c
enum Color
{
RED = 1,
GREEN = 2,
BLUE = 4
};
int main()
{
enum Color clr = GREEN; // 使用枚举常量给枚举变量赋值
printf("颜色值: %d\n", clr); // 输出: 颜色值: 2
return 0;
}