位字段

操控位的第2种方法是位字段(bit field)。位字段是一个signed int或unsigned int类型变量中的一组相邻的位(C99和C11新增了_Bool类型的位字段)。位字段通过一个结构声明来建立,该结构声明为每个字段提供标签,并确定该字段的宽度。例如,下面的声明建立了一个4个1位的字段:

  1. struct {
  2. unsigned int autfd : 1;
  3. unsigned int bldfc : 1;
  4. unsigned int undln : 1;
  5. unsigned int itals : 1;
  6. } prnt;

根据该声明,prnt 包含 4 个 1 位的字段。现在,可以通过普通的结构成员运算符(.)单独给这些字段赋值:

  1. prnt.itals = 0;
  2. prnt.undln = 1;

由于每个字段恰好为1位,所以只能为其赋值1或0。变量prnt被储存在int大小的内存单元中,但是在本例中只使用了其中的4位。带有位字段的结构提供一种记录设置的方便途径。许多设置(如,字体的粗体或斜体)就是简单的二选一。例如,开或关、真或假。如果只需要使用 1 位,就不需要使用整个变量。内含位字段的结构允许在一个存储单元中储存多个设置。有时,某些设置也有多个选择,因此需要多位来表示。这没问题,字段不限制 1 位大小。可以使用如下的代码:

  1. struct {
  2. unsigned int code1 : 2;
  3. unsigned int code2 : 2;
  4. unsigned int code3 : 8;
  5. } prcode;

以上代码创建了两个 2 位的字段和一个 8 位的字段。可以这样赋值:

  1. prcode.code1 = 0;
  2. prcode.code2 = 3;
  3. prcode.code3 = 102;

但是,要确保所赋的值不超出字段可容纳的范围。如果声明的总位数超过了一个unsigned int类型的大小会怎样?会用到下一个unsigned int类型的存储位置。
一个字段不允许跨越两个unsigned int之间的边界。编译器会自动移动跨界的字段,保持unsigned int的边界对齐。一旦发生这种情况,第1个unsigned int中会留下一个未命名的“洞”。
可以用未命名的字段宽度“填充”未命名的“洞”。使用一个宽度为0的未命名字段迫使下一个字段与下一个整数对齐(注意:只有未命名字段的大小才能为0,命名字段的大小不能设为0):

  1. struct {
  2. unsigned int field1 : 1 ;
  3. unsigned int : 2 ;
  4. unsigned int field2 : 1 ;
  5. unsigned int : 0 ;
  6. unsigned int field3 : 1 ;
  7. } stuff;
  8. 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 中。

  1. struct {
  2. unsigned int field1 : 1 ;
  3. unsigned int : 2 ;
  4. unsigned int field2 : 1 ;
  5. unsigned int : 1 ;
  6. unsigned int field3 : 1 ;
  7. } stuff;
  8. 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声明如下:

    1. struct box_props {
    2. bool opaque         : 1 ;
    3. unsigned int fill_color  : 3 ;
    4. unsigned int        : 4 ;
    5. bool show_border      : 1 ;
    6. unsigned int border_color : 3 ;
    7. unsigned int border_style : 2 ;
    8. unsigned int        : 2 ;
    9. };

    加上未命名的字段,该结构共占用 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简单的颜色表示
image.png

程序清单中的程序使用 box_props 结构,该程序用#define 创建供结构成员使用的符号常量。注意,只打开一位即可表示三原色之一。其他颜色用三原色的组合来表示。例如,紫色由打开的蓝色位和红色位组成,所以,紫色可表示为 BLUE|RED。
程序清单 fields.c 程序:

  1. #include <stdbool.h> // C99定义了bool、true、false
  2. #include <stdio.h>
  3. /* 线的样式 */
  4. #define SOLID 0
  5. #define DOTTED 1
  6. #define DASHED 2
  7. /* 三原色 */
  8. #define BLUE 4
  9. #define GREEN 2
  10. #define RED 1
  11. /* 混合色 */
  12. #define BLACK 0
  13. #define YELLOW (RED | GREEN)
  14. #define MAGENTA (RED | BLUE)
  15. #define CYAN (GREEN | BLUE)
  16. #define WHITE (RED | GREEN | BLUE)
  17. const char *colors[8] = {"black", "red", "green", "yellow",
  18. "blue", "magenta", "cyan", "white"};
  19. struct box_props {
  20. bool opaque : 1;
  21. // 或者 unsigned int (C99以前)
  22. unsigned int fill_color : 3;
  23. unsigned int : 4;
  24. bool show_border : 1;
  25. // 或者 unsigned int (C99以前)
  26. unsigned int border_color : 3;
  27. unsigned int border_style : 2;
  28. unsigned int : 2;
  29. };
  30. void show_settings(const struct box_props *pb);
  31. int main(void) {
  32. /* 创建并初始化 box_props 结构 */
  33. struct box_props box = {true, YELLOW, true, GREEN, DASHED};
  34. printf("size of box:%lu\n", sizeof(box));
  35. printf("Original box settings:\n");
  36. show_settings(&box);
  37. box.opaque = false;
  38. box.fill_color = WHITE;
  39. box.border_color = MAGENTA;
  40. box.border_style = SOLID;
  41. printf("\nModified box settings:\n");
  42. show_settings(&box);
  43. return 0;
  44. }
  45. void show_settings(const struct box_props *pb) {
  46. printf("Box is %s.\n", pb->opaque == true ? "opaque" : "transparent");
  47. printf("The fill color is %s.\n", colors[pb->fill_color]);
  48. printf("Border %s.\n", pb->show_border == true ? "shown" : "not shown");
  49. printf("The border color is %s.\n", colors[pb->border_color]);
  50. printf("The border style is ");
  51. switch (pb->border_style) {
  52. case SOLID:
  53. printf("solid.\n");
  54. break;
  55. case DOTTED:
  56. printf("dotted.\n");
  57. break;
  58. case DASHED:
  59. printf("dashed.\n");
  60. break;
  61. default:
  62. printf("unknown type.\n");
  63. }
  64. }

下面是该程序的输出:

  1. Original box settings:
  2. Box is opaque.
  3. The fill color is yellow.
  4. Border shown.
  5. The border color is green.
  6. The border style is dashed.
  7. Modified box settings:
  8. Box is transparent.
  9. The fill color is white.
  10. Border shown.
  11. The border color is magenta.
  12. 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。