上节课咱们把结构体讲透了。结构体是什么?就是把一堆不同类型的数据排排坐,每个成员各占各的地盘。你定义struct Student里面有id、name、score,那内存里就给你留出三块独立的区域,互不干扰。
今天咱们讲个跟结构体长得特别像、但脾气完全相反的东西——共用体。
共用体是啥?用一句话说就是:好几个成员,挤在同一块内存里。 你改了其中一个,另一个也跟着变——因为它们压根就是同一块内存,只是换了个名字而已。
听着有点邪门是吧?咱们慢慢说。
先看共用体长啥样
定义格式跟结构体几乎一模一样,就是把struct换成union:
union Data { int i; float f; char c; };
这就定义了一个叫Data的共用体类型,里面有三个成员:一个int、一个float、一个char。
定义变量也一样:
union Data d;
访问成员也跟结构体一样,用点号:
d.i = 10; d.f = 3.14; d.c = 'A';
看着跟结构体没啥区别对吧?但关键区别在内存布局上。
结构体 vs 共用体:内存布局的差别
这是理解共用体的核心,咱们画图说。
结构体:每个成员各占各的空间,总大小等于所有成员大小之和(可能还有内存对齐)。
struct S { int i; // 4字节 float f; // 4字节 char c; // 1字节 };
内存里长这样:
+--------+--------+-----+ | i | f | c | | 4字节 | 4字节 |1字节| +--------+--------+-----+ 总大小:12字节(考虑对齐)
每个成员都有自己的房间,互不干扰。
共用体:所有成员共用同一块内存,总大小等于最大的那个成员的大小。
union U { int i; // 4字节 float f; // 4字节 char c; // 1字节 };
内存里长这样:
+----------+ | i/f/c | | 共用4字节| +----------+ 总大小:4字节(最大的成员是4字节)
所有成员都住在同一个房间,只是从不同的角度去看这块内存。你给i赋值,实际上就是往这块内存里写了一个整数;你读f,实际上就是把同一块内存里的0和1按浮点数来解释。
这就是共用体的本质:同一块内存,多种解读方式。
一个例子让你彻底明白
#include <stdio.h> union Data { int i; float f; char c; }; int main() { union Data d; // 先给i赋值 d.i = 65; printf("d.i = %d\n", d.i); // 输出65 printf("d.f = %f\n", d.f); // 输出一个乱七八糟的数 printf("d.c = %c\n", d.c); // 输出'A'(ASCII 65) printf("\n"); // 再给f赋值 d.f = 3.14; printf("d.i = %d\n", d.i); // 输出一个乱七八糟的整数 printf("d.f = %f\n", d.f); // 输出3.14 printf("d.c = %c\n", d.c); // 输出一个乱七八糟的字符 return 0; }
你看,当你给i赋值65的时候,c也变成了'A'(因为65就是'A'的ASCII码)。因为它们共用同一块内存,你往里面写了一个65,i把它解读成整数65,c把它解读成字符'A'。
但你给f赋值3.14的时候,i和c就变成乱七八糟的值了——因为浮点数3.14在内存里的0和1排列方式,跟整数和字符完全不同。你用i去读,读到的是一堆你根本不认识的数字。
这就是共用体的脾气:同一块内存,你最后一次写进去的是什么,它就是什么。你换个成员去读,读到的还是那串0和1,只不过按新成员的类型去解释,结果可能完全不是你想要的。
共用体的大小怎么算?
共用体的大小等于最大的那个成员的大小。
union U1 { int i; // 4 char c; // 1 }; // 大小是4 union U2 { double d; // 8 int i; // 4 char c; // 1 }; // 大小是8 union U3 { char c; // 1 short s; // 2 }; // 大小是2
但有个细节:共用体的大小还要考虑内存对齐。
union U4 { char c; // 1 int i; // 4 }; // 大小不是4,是4(对齐到4字节)
这个例子刚好是4。再看:
union U5 { char arr[5]; // 5 int i; // 4 }; // 大小是8(对齐到4的倍数)
因为int是4字节,要求起始地址是4的倍数,所以整个共用体的大小要向上对齐到4的倍数,5向上对齐就是8。
共用体到底有啥用?
你可能会想:这玩意儿邪门得很,一不小心就读出乱七八糟的值,那还要它干啥?
共用体在实际开发里有两个典型用途:
用途一:节省内存
有些场景下,你明确知道同一时间只会用到其中一个成员,那用共用体就能省内存。
比如你写一个“消息”结构体,消息可以是一个整数、一个浮点数、或者一个字符串,但一次只会是其中一种:
struct Message { int type; // 标记当前是哪种类型 union { int i; float f; char str[20]; } data; // 共用体作为成员 };
你根据type的值来决定当前data里存的是啥。type=1就按int读,type=2就按float读,type=3就按字符串读。这样data只占20字节(最大的成员),而不是4+4+20=28字节。
用途二:判断大小端
这个在面试里经常考。所谓“大小端”就是:一个多字节的数据(比如int的0x12345678),在内存里是从低位字节开始存还是从高位字节开始存?
-
小端:低位字节存在低地址。0x78 0x56 0x34 0x12
-
大端:高位字节存在低地址。0x12 0x34 0x56 0x78
用共用体可以轻松判断:
#include <stdio.h> int main() { union { int i; char c; } u; u.i = 1; if (u.c == 1) { printf("小端\n"); // 低位字节存在低地址 } else { printf("大端\n"); // 高位字节存在低地址 } return 0; }
你看,给i赋值1,然后读c——c是第一个字节。如果是小端,低位字节1存在低地址,c读到1;如果是大端,高位字节0存在低地址,c读到0。
用途三:类型转换
有时候你想把一个float的二进制表示原封不动地转成int来操作,用共用体就能做到:
union { float f; int i; } u; u.f = 3.14; printf("3.14的二进制表示:%d\n", u.i);
这样你就能看到3.14在内存里到底是什么样的0和1。
共用体跟结构体的区别,一张表说清楚
| 结构体 | 共用体 | |
|---|---|---|
| 关键字 | struct |
union |
| 成员内存 | 各占各的,互不干扰 | 共用同一块内存 |
| 总大小 | 所有成员大小之和(考虑对齐) | 最大成员的大小(考虑对齐) |
| 修改一个成员 | 不影响其他成员 | 覆盖所有成员 |
| 典型用途 | 描述一个对象的多个属性 | 节省内存、类型转换、判断大小端 |
几个常见的坑
坑1:给一个成员赋值,用另一个成员读
union Data d; d.i = 10; printf("%f\n", d.f); // 输出乱七八糟的值
这是共用体的特性,不是Bug。你最后一次写的是int,读的时候用了float,当然读出来不是你想要的东西。用共用体的时候,你自己得记住当前存的是什么类型。
坑2:共用体不能直接初始化多个成员
union Data d = {10, 3.14, 'A'}; // 错误!共用体只能初始化第一个成员 union Data d = {10}; // 正确,初始化第一个成员
共用体初始化只能给第一个成员赋值,后面的成员共享同一块内存,没法同时给多个成员赋不同的值。
坑3:共用体不能比较相等
union Data d1, d2; if (d1 == d2) // 错误!共用体不能直接比较
跟结构体一样,要比较得逐个成员比较(但你得知道当前哪个成员有效)。
坑4:共用体里不能放带构造函数的类型
C语言里没有构造函数这个概念,所以这条主要是在C++里要注意。C语言里共用体成员可以是任意基本类型和结构体。
坑5:指针和共用体混用要小心
union Data d; d.i = 10; int *p = &d.i; *p = 20; // 改变了d.i,也改变了d.f和d.c
一个完整的例子:简单消息系统
#include <stdio.h> #include <string.h> // 定义消息类型 enum MsgType { INT_MSG, FLOAT_MSG, STR_MSG }; struct Message { enum MsgType type; union { int i; float f; char str[20]; } data; }; void print_message(struct Message *msg) { switch (msg->type) { case INT_MSG: printf("整数消息:%d\n", msg->data.i); break; case FLOAT_MSG: printf("浮点消息:%.2f\n", msg->data.f); break; case STR_MSG: printf("字符串消息:%s\n", msg->data.str); break; } } int main() { struct Message m1, m2, m3; m1.type = INT_MSG; m1.data.i = 100; m2.type = FLOAT_MSG; m2.data.f = 3.14; m3.type = STR_MSG; strcpy(m3.data.str, "Hello"); print_message(&m1); // 输出:整数消息:100 print_message(&m2); // 输出:浮点消息:3.14 print_message(&m3); // 输出:字符串消息:Hello return 0; }
这个例子把共用体、结构体、枚举全串起来了。Message结构体里有一个type标记当前消息类型,还有一个共用体data存实际数据。你根据type来决定怎么读data。这是共用体最经典的应用场景之一。
总结一下:
共用体就一句话:多个成员共用同一块内存,总大小等于最大成员的大小。
核心特点:
-
同一时间只能有一个成员有效
-
改一个成员会覆盖其他成员
-
用哪个成员读,就按哪个类型解释内存里的0和1
典型用途:
-
节省内存(同一时间只用一种类型)
-
类型转换(把float的二进制当int看)
-
判断大小端
跟结构体的区别:
-
结构体是“排排坐,各占各的”
-
共用体是“挤一挤,共用一块”
共用体在实际开发里用得没有结构体那么多,但你要知道它存在,知道它跟结构体的区别,知道它适合解决什么问题。面试的时候“结构体和共用体的区别”是高频考点,你今天把这节课听明白了,这道题就稳了。
回去自己动手,把上面那个消息系统的例子敲一遍。再写个程序,判断你的电脑是小端还是大端。写完跑一跑,共用体这玩意儿你就不会再觉得邪门了。下节课咱们讲枚举——给整数起个有意义的名字,让代码可读性上一个台阶。