第十七章、联合体

上一章我们学习了结构体。结构体是由多个成员变量按先后顺序在内存中依次向后摆放的数据类型。结构体内的成员变量默认按各自类型进行字节对齐。结构体变量本身也会按结构体的对齐字节数整体对齐。

这一章我们学习联合体数据类型,它是同结构体类型类似的数据类型,也可以认为它是一种特殊的结构体。

1. 联合体

联合体(Union),也叫共用体,它的允许你在同一块内存位置存储不同的数据类型。

联合体是一种和结构体一样的复合数据类型。它的内部同样可以定义多个成员变量,但这些成员变量的起始地址都是联合体变量的起始地址,即这些成员变量重叠的摆放在以联合体开始的同一内存地址中,共用同一内存的起始地址。

联合体的声明的语法和结构体声明的语法基本完全相同,只是联合体声明使用 union 关键字。

语法:

union [联合体名] {
    数据类型1  成员变量名1[: 占用位宽1], 成员变量名2[: 占用位宽2], ...;
    数据类型2  成员变量名4;
    // ... 联合体内其它成员变量
}[变量名1[={初始化列表1}]][,变量名2[={初始化列表2}], ...];

说明:

  1. 语法中的中括号([])表示其中的内容可以省略。
  2. 联合体名必须是标识符。
  3. union 是联合体声明的关键字,声明的联合体类型名为 union 联合体名
  4. 联合体名可是省略不写,如果省略不写则必须在声明此联合体是定义变量。否则后续则无法为该联合体创建变量。
  5. 成员变量名必须是标识符,可以使用位域的形式。
  6. 联合体的成员变量(包含位域)共用联合体变量的起始地址。
  7. 前面 union [联合体名] {...} 部分是类型声明,是数据类型说明部分。
  8. 后面 [变量名1[={初始化列表1}]] 是联合体变量声明,此部分可以省略不写。后续可以使用联合体类型来声明变量。
  9. 联合体声明必须以分号(;)结束。

示例:

我们定义一个联合体 union myunion 并同时声明一个变量 u

union myunion {
    char c;
    short int s;
    int i;
} u;

此时这个联合体变量 u 占用的内存长度是 4 个字节。其中成员变量 c 引用最低位的 1 个字节;成员变量 s 引用最低位的 2 个字节;成员变量 i 引用结构体的全部 4 个字节。 由于 成员变量 csi 共用同一内存空间,当我们修改 成员变量 c 时,成员变量 si 的最低位的一个字节也会被同时修改。当我们修改成员变量 s 时,成员变量 c 也会被修改,成员变量 i 的最低位的 2 个字节也会被同时修改。如果成员变量 i 被修改,则成员变量 cs 也会随之改变。因为它们共用同一内存空间。

上述联合体变量 u 的内存结构如下图所示:

union myunion u;
// u 占用的 4 字节内存如下
+-----+-----+-----+-----+
|     |     |     |     |
+-----+-----+-----+-----+
// u.c 在内存中的位置
+-----+
|     |
+-----+
// u.s 在内存中的位置
+-----+-----+
|     |     |
+-----+-----+
// u.i 在内存中的位置
+-----+-----+-----+-----+
|     |     |     |     |
+-----+-----+-----+-----+
^
|
&u.c
&u.s
&u.i
&u
// 此四个字节的起始位置也是三个成员变量的起始位置

可见其中的第一个字节是三个成员都共用的字节。

我们在来看一下结构体的内存结构。

我们定义如下结构体。

struct mystruct {
    char c;
    short int s;
    int i;
} s;

则结构体成员变量 csi 会按各自成员的对齐方式依次向后排列,不会叠加。

上述结构体变量 s 的内存结构如下图所示:

struct mystruct s;
// s 占用的 8 字节内存如下
+-----+-----+-----+-----+-----+-----+-----+-----+
|     |     |     |     |     |     |     |     |
+-----+-----+-----+-----+-----+-----+-----+-----+
// s.c 在内存中的位置
+-----+
|     |
+-----+
^           // s.s 在内存中的位置
|           +-----+-----+
&s.c        |     |     |
&s          +-----+-----+
            ^           // s.i 在内存中的位置
            |           +-----+-----+-----+-----+
            &s.s        |     |     |     |     |
                        +-----+-----+-----+-----+
                        ^
                        |
                        &s.i

可见,结构体和联合体的区别就是结构体的成员变量不会叠加,而且为提高内存读/写效率还会字节对齐。联合体的各个成员变量在内存中叠加(共用内存),且各个成员变量按联合体的起始地址对齐,而联合体变量按自己的对齐字节数整体对齐。

示例

// filename: union_demo.c
#include <stdio.h>

union myunion {
    char c;
    short int s;
    int i;
} u;

struct mystruct {
    char c;
    short int s;
    int i;
} s;

int main(int argc, char * argv[]) {
    printf("sizeof(u): %ld\n", sizeof(u));
    printf("sizeof(s): %ld\n", sizeof(s));
    printf("  &u: %p\n", &u);
    printf("&u.c: %p\n", &u.c);
    printf("&u.s: %p\n", &u.s);
    printf("&u.i: %p\n", &u.i);

    printf("  &s: %p\n", &s);
    printf("&s.c: %p\n", &s.c);
    printf("&s.s: %p\n", &s.s);
    printf("&s.i: %p\n", &s.i);

    u.i = 0x04030201;
    printf("u.c: 0x%08x\n", u.c);
    printf("u.s: 0x%08x\n", u.s);
    printf("u.i: 0x%08x\n", u.i);
    return 0;
}

编译和运行结果如下:

weimingze@mzstudio:~$ gcc -o union_demo union_demo.c
weimingze@mzstudio:~$ ./union_demo
sizeof(u): 4
sizeof(s): 8
  &u: 0x5f14a9032018
&u.c: 0x5f14a9032018
&u.s: 0x5f14a9032018
&u.i: 0x5f14a9032018
  &s: 0x5f14a9032020
&s.c: 0x5f14a9032020
&s.s: 0x5f14a9032022
&s.i: 0x5f14a9032024
u.c: 0x00000001
u.s: 0x00000201
u.i: 0x04030201

从运行结果可以看出,有相同个数和类型的成员变量的结构体和联合体占用的内存字节数是不一样的。联合体各个成员变量的内存起始地址是一样的,结构体中各个成员变量的内存起始地址则依次向后排列。

当我们修改 联合体中的一个成员变量。则内存改变。其它成员变量的值也会受到影响。

使用联合体的场景

在如下的两种情况我们通常会使用联合体:

  1. 节省内存:在多个变量不同时使用的情况下,可以定义在一个联合体内,共用同一空间可以节省内存。
  2. 以多种方式解析同一内存中的数据:在网络传输的字节串数据中,可以方便的解析出每个字节或某些字节对应的含义。

实验

使用联合体分析无符号整数、有符号整数、单精度浮点数、双精度浮点数的内存结构。我们定义如下联合体。

union {
    unsigned char b[8];
    unsigned int ui;
    int i;
    float f;
    double d;
} m;

我们使用赋值语句 m.i = 100;, 然后打印 b[0]b[1]b[2]b[3] 值就可以知道 m.i 在内存中的每一个位对应则值了。同理我们可以分别对 m.uim.fm.d 赋值,也可看到内存中每一个字节的值对应的值。用此方法我们可以分析出各种数据类型在内存中的存储结构。