联合体是一个可以(在不同时间)保存不同类型和大小的对象的变量,由编译器来跟踪大小和对齐要求。联合体提供了一种不用在程序中嵌入任何与机器相关的信息,而能够在单个存储区域内操作不同类型数据的方式。它们类似于Pascal中的变体记录(variant record)。
以编译器符号表管理器中可能找到的代码为例,我们假定一个常量可能是 int,float 或字符指针。某个特定常量的值必须储存在正确类型的变量中,然而,如果不管该常量的值是什么类型,它都占有相同大小的内存而且保存在同一个位置,那么对表管理而言是最方便的。这就是联合体的目的——单个变量能够合法地保存几种类型中的任意一种。联合体的语法以结构体的为基础:
union u_tag {
int ival;
float fval;
char *sval;
} u;
变量 u 足够大,能容纳这三种类型中最大的;不过具体的大小依赖于实现。这些类型中不管哪一种都能被赋给 u ,然后用在表达式里,但注意使用方式必须是一致的:即取出的类型必须是最近存入的类型。跟踪当前在联合体中保存的是哪种类型,属于程序员的责任【前面也说了,编译只负责内存大小和对齐要求】;如果按一种类型保存,又按另一种类型来取,则结果是由实现定义的。
在语法上,联合体的成员通过如下两种方式来访问
联合体名称 . 成员
或
联合体指针 -> 成员
和结构体一样。
如果用变量 utype 来跟踪 u 中当前储存的类型,我们能看到的代码类似于
if (utype == INT)
printf("%d\n", u.ival);
else if (utype == FLOAT)
printf("%f\n", u.fval);
else if (utype == STRING)
printf("%s\n", u.sval);
else
printf("bad type %d in utype\n", utype)
联合体可以出现在结构体和数组中,反之亦然。访问结构体中联合体的成员(或联合体中的结构体成员)的表示法,与访问嵌套结构体是一样的。例如,结构体数组定义如下
struct {
char *name,
int flags,
int utypes,
union {
int ival;
float fval;
char *sval;
} u;
} systab[NSYM];
则成员 ival 通过如下方式引用:
systab[i].u.ival
字符串 sval 的第一个字符可用如下两种方法引用:
*systab[i].u.sval;
systab[i].u.sval[0];
实际上,联合体就是一个结构体,其所有成员都是从其内存起始地址的偏移量零开始,而且结构体足够大以容纳“最宽”的成员,另外对齐方式也适用于联合体中所有成员。在结构体上能做的操作,在联合体上同样也能做:即作为一个整体单元被赋值或拷贝,以及获取地址,还有访问其成员。
联合体只能被它首个成员类型的值初始化;因此上面描述的联合体 u 只能初始化为整数值。
第八章的存储分配器展示了联合体如何被用来强制让一个变量在特定类型的存储边界对齐。
当存储空间非常稀缺时,可能有必要将多个对象打包到单个机器字中;一种常见的做法是使用一组由单个比特构成的标志位,像编译器符号表就是如此。对外暴露的数据格式,例如硬件设备的接口,也经常需要访问一个机器字中部分比特位的能力。
想象一下编译器中操作符号表的那部分代码片段。程序中的每个标识符都有与之关联的信息,例如是否关键字,是否外部或/且静态,等等。对这些信息进行编码的最紧凑方式,是在单个 char 或 int 中保存一组单比特标志位。
通常的做法是定义一组对应到相应比特位的“掩码”,例如
#define KEYWORD 01
#define EXTERNAL 02
#define STATIC 04
或者是
enum { KEYWORD = 01, EXTERNAL = 02, STATIC = 04 }
这些数必须是2的幂。然后对这些比特位的访问就成了一种“位的摆弄”(bit-fiddling),用的是我们在第二章描述的位移、掩码和补码操作。
某些用法非常频繁,如
flags |= EXTERNAL | STATIC;
把 flags 中的 EXTERNAL 和 STATIC 位打开。而
flags ~= ~(EXTERNAL | STATIC);
则把它们关闭。另外下面的判断
if ((flags & (EXTERNAL | STATIC)) == 0) ...
当这两个位都关闭时为真。
尽管这些用法很容易掌握,但作为替代方案,C 语言还提供了一种不通过位逻辑操作符,而是直接定义和操作机器字内的域的能力。位域,或者简称域,是在单个由实现定义的存储单元(我们称之为“机器字”)中,一系列相邻的比特位。域的定义和访问的语法基于结构体的语法。例如,上面符号表的 #define 语句可以由下列三个域的定义来替代:
struct {
unsigned int is_keyword : 1;
unsigned int is_extern : 1;
unsigned int is_static : 1;
} flags;
这定义了一个名为 flags 的变量,它包含三个1比特的位域。冒号后面的数字代表了位域的宽度,单位为比特。这些域都声明为 unsigned int 以保证它们都是无符号的值。
各位域以结构体成员的同样方式访问:flags.is_keyword、flags.is_extern 等等。位域的行为像是小整数,而且可以和其他整数一样参与到算术运算表达式中,因此前面的例子可以很自然地写成:
flags.is_extern = flags.is_static = 1;
上面会将对应的比特位打开。
flags.is_extern = flags.is_static = 0;
上面会将比特位关闭。
if (flags.is_extern == 0 && flags.is_static == 0)
上面对比特位进行检查。
位域的绝大部分都依赖于实现。一个位域能否跨越机器字的边界,这是由实现定义的。位域甚至不必有名称;未命名位域(只有冒号和位域宽度)用于填充。而特殊的位域宽度 0 可用来在下一个机器字边界处强制对齐。
在某些计算机上位域从左到右分配,而其他机器上是从右到左分配。这意味着,尽管字段对于维护 内部定义的数据结构很有用,但是,在获取外部定义的数据时,必须仔细考虑哪个端在前的问题【大字节序或小字节序】;依赖这些的程序是不可移植的。位域只能声明为 int;为了可移植性,要显式指定 signed 或 unsigned。位域不是数组,也没有地址,因此不能对其使用 & 操作符。
(第六章完)