C语言里的union到底是什么?

对于C/C++这一块,因为只有上课的时候姑且用了一下,平日里开发基本上不会接触到C/C++,所以这一块相对而言有很多知识是不知道的,但是考虑到在找工作的过程中会遇到,所以针对性的补一些基础的。

这一篇主要是对C里面的union做一些笔记。

Keyword: C, union

union,可称为联合体,也可以称为是共同体,它在声明结构上和struct,即结构体是比较相似的,他们都可以由多种不同类型的成员构成。

和结构体不同的是,联合体声明之后,联合体内的所有成员并没有各自独立的内存空间,而是所有成员共用一个空间,在联合体内只有一个成员的值可以存储在里面。

比如下面的联合体:

1
2
3
4
5
union foo {
char c;
int i;
double k;
}

在这个联合体中c、i、k都是共用同一块内存空间的,这一块内存空间的长度等同于其成员中最长的那一个数据类型所占的内存空间长度。

例如在上面的共用体中,共用体所占的内存空间长度为一个double的长度。

举另外一个例子:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

union A{
int i;
char s[20];
};

int main() {
union A a;
printf("%d", sizeof(a));
}

执行该程序得到的结果将会是20。说明union只根据其成员最长的那个来决定整个共用体的大小,以确保所有的成员都能放进去不会溢出。

在共用体里面,由于所有的成员都共用同一块内存、它只能同时存储一个成员的值,所以在下面这个例子中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

union A{
int i;
double j;
};

int main() {
union A a;
a.i = 10;
a.j = 11;
printf("%d\n", a.i);
printf("%lf", a.j);
}

得到的第一行输出是0,第二行输出是11.000000,我们给a.i赋的值10实际上已经不存在了,因为在共用体内它只能存放一个值,这个值是双精度浮点数11。

这个例子具有一定的迷惑性,实际上a.i输出是0是因为,在它能够打印的一个整数的内存长度中,所有的数据刚好是0,所以输出了0,并不是意味着a.i不存在了,完全被a.j取代掉了所以变成了0。

把这个例子改一下,我们就能够得到一个完全不一样的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

union A{
int i;
double j;
};

int main() {
union A a;
a.i = 10;
a.j = 11.01596111264819;
printf("%d\n", a.i);
printf("%lf", a.j);
}

假如我们不给a.j赋予一个整数,而是一个带有小数部分的实数,输出的第一行是236038619,第二行式11.015961。

为什么会这样呢?其实只是在开头一个整数长度的内存空间里,里面的数据转化成10进制是236038619,所以它输出了这个内容。

有意思的是假如我们把a.i和a.j的赋值反过来,比如说这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

union A{
int i;
double j;
};

int main() {
union A a;
a.j = 123;
a.i = 1111;
printf("%d\n", a.i);
printf("%lf", a.j);
}

输出第一行是1111,第二行是123.000000,两个数字都存到了这个共同体里面,而且看上去没有发生错误,好像共同体一下子变成了结构体,能存两个成员的数据了。

但实际上是这样吗?通过动态调试我们可以发现,实际上我们存入的a.i仍然影响到了a.j的内存空间。

a.j只是因为输出的长度只有小数点后6位,所以看起来像是没有发生变化,实际上它还是发生了变化的,毕竟整个内存空间已经填入了a.i的值。

比较有趣的是,联合体里面的某个成员得到了新值之后,它并不会把整个内存空间都清理一遍再填入新值,而是直接覆盖填入对应长度的位置,长度以外的位置不动,这可以说是联合体的一个重要特性。

在联合体这种存放数据的情况下,我们又能引出一个新的问题。比如下面这样一个例子,它输出的值会是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

union A{
int i;
char s[2];
};

int main() {
union A a;
a.s[0] = 1;
a.s[1] = 2;
printf("%d\n", a.i);
}

对于这个a.i,很显然它输出的实际上是s[2]的内存内容作为一个整数表达出来的结果。

实际运行的得到的是513。

通过神奇的Windows 10计算器,我们可以发现这个513对应的是

1
0010 0000 0001

一个char是一个字节,等于8位,但是我们输出的东西实际上没有完整地输出,原因是最前面的一个0000其实是被printf忽略掉了的,内存里实际上是:

1
0000 0010 0000 0001

Intel CPU是小端序,先到的数据在高位,从高位向低位存,所以2在前面,1在后面。

如果反过来的话又能得到另一种存储的方式,输出的a.i也自然不一样,会是下面这个存储结构:

1
0000 0001 0000 0010

按照大端序,实际的输出就会是258,即二进制数1 0000 0010对应的十进制数。

只要理解了union这个共享内存的特性,union这个东西就基本上算是理解到了,剩下的就是具体应用场景具体分析。