位字段
操控位的第2种方法是位字段(bit field)。位字段是一个signed int或unsigned int类型变量中的一组相邻的位(C99和C11新增了_Bool类型的位字段)。位字段通过一个结构声明来建立,该结构声明为每个字段提供标签,并确定该字段的宽度。例如,下面的声明建立了一个4个1位的字段:
struct {unsigned int autfd : 1;unsigned int bldfc : 1;unsigned int undln : 1;unsigned int itals : 1;} prnt;
根据该声明,prnt 包含 4 个 1 位的字段。现在,可以通过普通的结构成员运算符(.)单独给这些字段赋值:
prnt.itals = 0;prnt.undln = 1;
由于每个字段恰好为1位,所以只能为其赋值1或0。变量prnt被储存在int大小的内存单元中,但是在本例中只使用了其中的4位。带有位字段的结构提供一种记录设置的方便途径。许多设置(如,字体的粗体或斜体)就是简单的二选一。例如,开或关、真或假。如果只需要使用 1 位,就不需要使用整个变量。内含位字段的结构允许在一个存储单元中储存多个设置。有时,某些设置也有多个选择,因此需要多位来表示。这没问题,字段不限制 1 位大小。可以使用如下的代码:
struct {unsigned int code1 : 2;unsigned int code2 : 2;unsigned int code3 : 8;} prcode;
以上代码创建了两个 2 位的字段和一个 8 位的字段。可以这样赋值:
prcode.code1 = 0;prcode.code2 = 3;prcode.code3 = 102;
但是,要确保所赋的值不超出字段可容纳的范围。如果声明的总位数超过了一个unsigned int类型的大小会怎样?会用到下一个unsigned int类型的存储位置。
一个字段不允许跨越两个unsigned int之间的边界。编译器会自动移动跨界的字段,保持unsigned int的边界对齐。一旦发生这种情况,第1个unsigned int中会留下一个未命名的“洞”。
可以用未命名的字段宽度“填充”未命名的“洞”。使用一个宽度为0的未命名字段迫使下一个字段与下一个整数对齐(注意:只有未命名字段的大小才能为0,命名字段的大小不能设为0):
struct {unsigned int field1 : 1 ;unsigned int : 2 ;unsigned int field2 : 1 ;unsigned int : 0 ;unsigned int field3 : 1 ;} stuff;printf("size of stuff = %zu\n", sizeof(stuff));//结果是size of stuff = 8
这里,在 stuff.field1 和 stuff.field2 之间,有一个 2 位的空隙;stuff.field2和stuff.field3之间是一个宽度为0的未命名字段,那么,stuff.field3 将储存在下一个 unsigned int 中。
struct {unsigned int field1 : 1 ;unsigned int : 2 ;unsigned int field2 : 1 ;unsigned int : 1 ;unsigned int field3 : 1 ;} stuff;printf("size of stuff = %zu\n", sizeof(stuff));//结果是size of stuff = 4
字段储存在一个 int 中的顺序取决于机器。在有些机器上,存储的顺序是从左往右,而在另一些机器上,是从右往左。另外,不同的机器中两个字段边界的位置也有区别。由于这些原因,位字段通常都不容易移植。尽管如此,有些情况却要用到这种不可移植的特性。例如,以特定硬件设备所用的形式储存数据。
位字段示例
通常,把位字段作为一种更紧凑储存数据的方式。例如,假设要在屏幕上表示一个方框的属性。为简化问题,我们假设方框具有如下属性:
- 方框是透明的或不透明的;
 - 方框的填充色选自以下调色板:黑色、红色、绿色、黄色、蓝色、紫色、青色或白色;
 - 边框可见或隐藏;
 - 边框颜色与填充色使用相同的调色板;
 边框可以使用实线、点线或虚线样式。
可以使用单独的变量或全长(full-sized)结构成员来表示每个属性,但是这样做有些浪费位。例如,只需 1 位即可表示方框是透明还是不透明;只需 1 位即可表示边框是显示还是隐藏。8 种颜色可以用 3 位单元的 8 个可能的值来表示,而 3 种边框样式也只需 2 位单元即可表示。总共 10 位就足够表示方框的 5 个属性设置。
一种方案是:一个字节储存方框内部(透明和填充色)的属性,一个字节储存方框边框的属性,每个字节中的空隙用未命名字段填充。struct box_props声明如下:struct box_props {bool opaque : 1 ;unsigned int fill_color : 3 ;unsigned int : 4 ;bool show_border : 1 ;unsigned int border_color : 3 ;unsigned int border_style : 2 ;unsigned int : 2 ;};
加上未命名的字段,该结构共占用 16 位。如果不使用填充,该结构占用 10 位。但是要记住,C 以 unsigned int 作为位字段结构的基本布局单元。因此,即使一个结构唯一的成员是 1 位字段,该结构的大小也是一个 unsigned int 类型的大小,unsigned int 在我们的系统中是 32 位。另外,以上代码假设 C99 新增的_Bool 类型可用,在 stdbool.h 中,bool 是_Bool 的别名。
对于 opaque 成员,1 表示方框不透明,0 表示透明。show_border 成员也用类似的方法。对于颜色,可以用简单的 RGB(即 red-green-blue 的缩写)表示。这些颜色都是三原色的混合。显示器通过混合红、绿、蓝像素来产生不同的颜色。在早期的计算机色彩中,每个像素都可以打开或关闭,所以可以使用用 1 位来表示三原色中每个二进制颜色的亮度。常用的顺序是,左侧位表示蓝色亮度、中间位表示绿色亮度、右侧位表示红色亮度。下表 1 列出了这 8 种可能的组合。fill_color 成员和 border_color 成员可以使用这些组合。最后,border_style 成员可以使用 0、1、2 来表示实线、点线和虚线样式。
表1简单的颜色表示
  程序清单中的程序使用 box_props 结构,该程序用#define 创建供结构成员使用的符号常量。注意,只打开一位即可表示三原色之一。其他颜色用三原色的组合来表示。例如,紫色由打开的蓝色位和红色位组成,所以,紫色可表示为 BLUE|RED。
  程序清单 fields.c 程序:
#include <stdbool.h> // C99定义了bool、true、false#include <stdio.h>/* 线的样式 */#define SOLID 0#define DOTTED 1#define DASHED 2/* 三原色 */#define BLUE 4#define GREEN 2#define RED 1/* 混合色 */#define BLACK 0#define YELLOW (RED | GREEN)#define MAGENTA (RED | BLUE)#define CYAN (GREEN | BLUE)#define WHITE (RED | GREEN | BLUE)const char *colors[8] = {"black", "red", "green", "yellow","blue", "magenta", "cyan", "white"};struct box_props {bool opaque : 1;// 或者 unsigned int (C99以前)unsigned int fill_color : 3;unsigned int : 4;bool show_border : 1;// 或者 unsigned int (C99以前)unsigned int border_color : 3;unsigned int border_style : 2;unsigned int : 2;};void show_settings(const struct box_props *pb);int main(void) {/* 创建并初始化 box_props 结构 */struct box_props box = {true, YELLOW, true, GREEN, DASHED};printf("size of box:%lu\n", sizeof(box));printf("Original box settings:\n");show_settings(&box);box.opaque = false;box.fill_color = WHITE;box.border_color = MAGENTA;box.border_style = SOLID;printf("\nModified box settings:\n");show_settings(&box);return 0;}void show_settings(const struct box_props *pb) {printf("Box is %s.\n", pb->opaque == true ? "opaque" : "transparent");printf("The fill color is %s.\n", colors[pb->fill_color]);printf("Border %s.\n", pb->show_border == true ? "shown" : "not shown");printf("The border color is %s.\n", colors[pb->border_color]);printf("The border style is ");switch (pb->border_style) {case SOLID:printf("solid.\n");break;case DOTTED:printf("dotted.\n");break;case DASHED:printf("dashed.\n");break;default:printf("unknown type.\n");}}
下面是该程序的输出:
Original box settings:Box is opaque.The fill color is yellow.Border shown.The border color is green.The border style is dashed.Modified box settings:Box is transparent.The fill color is white.Border shown.The border color is magenta.The border style is solid.
  该程序要注意几个要点。
  首先,初始化位字段结构与初始化普通结构的语法相同:
  struct box_props box = {YES, YELLOW , YES, GREEN, DASHED};
  类似地,也可以给位字段成员赋值:
  box.fill_color = WHITE;
  另外,switch 语句中也可以使用位字段成员,甚至还可以把位字段成员用作数组的下标:
  printf(“The fill color is %s.\n”, colors[pb->fill_color]);
  注意,根据 colors 数组的定义,每个索引对应一个表示颜色的字符串,而每种颜色都把索引值作为该颜色的数值。例如,索引 1 对应字符串”red”,枚举常量 red 的值是 1。
