联合提供了一种方法,能够规避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 |
许多计算机系统对基本数据类型的合法地址做出了一些限制,要求某种类型对象的地址必须是某个值K(通常是2、4或8)的倍数。这种对齐限制简化形成处理器和内存系统之间接口的硬件设计,即数据对齐。这也就是为什么 s3 中 i 的偏移量为什么是 4 而不是 1 ,以及为什么 v 的偏移量是 16 而不是 9 或 12 。
在一些上下文中,联合十分有用。但是,它也能引起一些讨厌的错误,因为它们绕过了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; // d 的类型为double
值 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 一样的位标识,包括符号位字段、指数和尾数,但是 u 的数值与 d 的数值没有任何关系,除了 d 等于 0.0 的情况。
当用联合来将各种不同大小的数据类型结合到一起时,字节顺序就变得很重要了。例如,假如我们写了一个过程,它以两个四字的 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 个字节。在大端法的机器上,这两个参数的角色刚好相反。