【C语言-自定义数据类型】2. 共用体——多个成员共用同一块内存

开发技术 作者: 精灵星空 SKY315.COM 更新于: 浏览: 3
码字发文不易,转载请注明出处【精灵星空 SKY315.COM】

上节课咱们把结构体讲透了。结构体是什么?就是把一堆不同类型的数据排排坐,每个成员各占各的地盘。你定义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看)

  • 判断大小端

跟结构体的区别:

  • 结构体是“排排坐,各占各的”

  • 共用体是“挤一挤,共用一块”

共用体在实际开发里用得没有结构体那么多,但你要知道它存在,知道它跟结构体的区别,知道它适合解决什么问题。面试的时候“结构体和共用体的区别”是高频考点,你今天把这节课听明白了,这道题就稳了。

回去自己动手,把上面那个消息系统的例子敲一遍。再写个程序,判断你的电脑是小端还是大端。写完跑一跑,共用体这玩意儿你就不会再觉得邪门了。下节课咱们讲枚举——给整数起个有意义的名字,让代码可读性上一个台阶。