3.9.2 联合
3.9.2 联合
联合提供了一种方式,能够规避 C 语言的类型系统,允许以多种类型来引用一个对象。联合声明的语法与结构的语法一样,只不过语义相差比较大。它们是用不同的字段来引用相同的内存块。
考虑下面的声明:
struct S3 {
char c;
int i[2];
double v;
};
union U3 {
char c;
int i[2];
double v;
};在一台 x86-64 Linux 机器上编译时,字段的偏移量、数据类型 S3 和 U3 的完整大小如下:
| 类型 | c | i | v | 大小 |
|---|---|---|---|---|
| S3 | 0 | 4 | 16 | 24 |
| U3 | 0 | 0 | 0 | 8 |
(稍后会解释 S3 中 i 的偏移量为什么是 4 而不是 1,以及为什么 v 的偏移量是 16 而不是 9 或 12。)对于类型 union U3 * 的指针 p,p->c、p->i[0] 和 p->v 引用的都是数据结构的起始位置。还可以观察到,一个联合的总的大小等于它最大字段的大小。
在一些上下文中,联合十分有用。但是,它也能引起一些讨厌的错误,因为它们绕过了 C 语言类型系统提供的安全措施。一种应用情况是,我们事先知道对一个数据结构中的两个不同字段的使用是互斥的,那么将这两个字段声明为联合的一部分,而不是结构的一部分,会减小分配空间的总量。
例如,假设我们想实现一个二叉树的数据结构,每个叶子节点都有两个 double 类型的数据值,而每个内部节点都有指向两个孩子节点的指针,但是没有数据。如果声明如下:
struct node_s {
struct node_s *left;
struct node_s *right;
double data[2];
};那么每个节点需要 32 个字节,每种类型的节点都要浪费一半的字节。相反,如果我们如下声明一个节点:
union node_u {
struct {
union node_u *left;
union node_u *right;
} internal;
double data[2];
};那么,每个节点就只需要 16 个字节。如果 n 是一个指针,指向 union node_u * 类型的节点,我们用 n->data[0] 和 n->data[1] 来引用叶子节点的数据,而用 n->internal.left 和 n->internal.right 来引用内部节点的孩子。
不过,如果这样编码,就没有办法来确定一个给定的节点到底是叶子节点,还是内部节点。通常的方法是引入一个枚举类型,定义这个联合中可能的不同选择,然后再创建一个结构,包含一个标签字段和这个联合:
typedef enum { N_LEAF, N_INTERNAL } nodetype_t;
struct node_t {
nodetype_t type;
union {
struct {
struct node_t *left;
struct node_t *right;
} internal;
double data[2];
} info;
};这个结构总共需要 24 个字节:type 是 4 个字节,info.internal.left 和 info.internal.right 各要 8 个字节,或者是 info.data 要 16 个字节。我们后面很快会谈到,在字段 type 和联合的元素之间需要 4 个字节的填充,所以整个结构大小为 4 + 4 + 16 = 24。在这种情况中,相对于给代码造成的麻烦,使用联合带来的节省是很小的。对于有较多字段的数据结构,这样的节省会更加吸引人。
联合还可以用来访问不同数据类型的位模式。例如,假设我们使用简单的强制类型转换将一个 double 类型的值 d 转换为 unsigned long 类型的值 u:
unsigned long u = (unsigned long) d;值 u 会是 d 的整数表示。除了 d 的值为 0.0 的情况以外,u 的位表示会与 d 的很不一样。再看下面这段代码,从一个 double 产生一个 unsigned long 类型的值:
unsigned long double2bits(double d) {
union {
double d;
unsigned long u;
} temp;
temp.d = d;
return temp.u;
}在这段代码中,我们以一种数据类型来存储联合中的参数,又以另一种数据类型来访问它。结果会是 u 具有和 d 一样的位表示,包括符号位字段、指数和尾数,如 3.11 节中描述的那样。u 的数值与 d 的数值没有任何关系,除了 d 等于 0.0 的情况。
当用联合来将各种不同大小的数据类型结合到一起时,字节顺序问题就变得很重要了。例如,假设我们写了一个过程,它以两个 4 字节的 unsigned 的位模式,创建一个 8 字节的 double:
double uu2double(unsigned word0, unsigned word1)
{
union {
double d;
unsigned u[2];
} temp;
temp.u[0] = word0;
temp.u[1] = word1;
return temp.d;
}在 x86-64 这样的小端法机器上,参数 word0 是 d 的低位 4 个字节,而 word1 是高位 4 个字节。在大端法机器上,这两个参数的角色刚好相反。
练习题 3.43 假设给你一个任务,检查一下 C 编译器为结构和联合的访问产生正确的代码。你写了下面的结构声明:
typedef union {
struct {
long u;
short v;
char w;
} t1;
struct {
int a[2];
char *p;
} t2;
} u_type;你写了一组具有下面这种形式的函数:
void get(u_type *up, type *dest) {
*dest = expr;
}这组函数有不一样的访问表达式 expr,而且根据 expr 的类型来设置目的数据类型 type。然后再检查编译这些函数时产生的代码,看看它们是否与你预期的一样。
假设在这些函数中,up 和 dest 分别被加载到寄存器 %rdi 和 %rsi 中。填写下表中的数据类型 type,并用 1~3 条指令序列来计算表达式,并将结果存储到 dest 中。
| expr | type | 代码 |
|---|---|---|
up->t1.u |
long |
movq (%rdi), %rax;movq %rax, (%rsi) |
up->t1.v |
||
&up->t1.w |
||
up->t2.a |
||
up->t2.a[up->t1.u] |
||
*up->t2.p |