操控位的第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类型的大小会怎样?会用到下一个unsignedint类型的存储位置。一个字段不允许跨越两个unsigned int之间的边界。编译器会自动移动跨界的字段,保持unsigned int的边界对齐。一旦发生这种情况,第1个unsignedint中会留下一个未命名的“洞”。 可以用未命名的字段宽度“填充”未命名的“洞”。使用一个宽度为0的未命名字段迫使下一个字段与下一个整数对齐:
struct { unsigned int field1 : 1; unsigned int : 2; unsigned int field2 : 1; unsigned int : 0; unsigned int field3 : 1;} stuff;
这里,在stuff.field1和stuff.field2之间,有一个2位的空隙;stuff.field3将存储在下一个unsigned int中。 字段存储在一个int中的顺序取决于机器。在有些机器上,存储的顺序是从左往右,而在另一些机器上,是从右往左。另外,不同的机器中两个字段边界的位置也有区别。由于这些原因,位字段通常都不容易移植。尽管如此,有些情况却要用到这种不可移植的特性。例如,以特定硬件设备所用的形式存储数据。
1 位字段示例
通常,把位字段作为一种更紧凑存储数据的方式。例如,假设要在屏幕上表示一个方框的属性。为简化问题,我们假设方框具有如下属性:
- 方框是透明的或不透明的;
- 方框的填充色选自以下调色板:黑色、红色、绿色、黄色、蓝色、紫色、青色或白色;
- 边框可见或隐藏;
- 边框颜色与填充色使用相同的调色板;
- 边框可以使用实线、点线或虚线样式。
可以使用单独的变量或全长(full-sized)结构成员来表示每个属性,但是这样做有些浪费位。例如,只需1位即可表示方框是透明还是不透明;只需1位即可表示边框是显示还是隐藏。8种颜色可以用3位单元的8个可能的值来表示,而3种边框样式也只需2位单元即可表示。总共10位就足够表示方框的5个属性设置。 一种方案是:一个字节存储方框内部(透明和填充色)的属性,一个字节存储方框边框的属性,每个字节中的空隙用未命名字段填充。struct boxprops声明如下:
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表示透明。showborder成员也用类似的方法。对于颜色,可以用简单的RGB(即red-green-blue的缩写)表示。这些颜色都是三原色的混合。显示器通过混合红、绿、蓝像素来产生不同的颜色。在早期的计算机色彩中,每个像素都可以打开或关闭,所以可以使用用1位来表示三原色中每个二进制颜色的亮度。常用的顺序是,左侧位表示蓝色亮度、中间位表示绿色亮度、右侧位表示红色亮度。表15.3列出了这8种可能的组合。fillcolor成员和bordercolor成员可以使用这些组合。最后,borderstyle成员可以使用0、1、2来表示实线、点线和虚线样式。
程序 fields.c 中的程序使用boxprops结构,该程序用#define创建供结构成员使用的符号常量。注意,只打开一位即可表示三原色之一。其他颜色用三原色的组合来表示。例如,紫色由打开的蓝色位和红色位组成,所以,紫色可表示为BLUE|RED。
The fields.c Program
/* fields.c -- define and use fields */#include #include //C99, defines bool, true, false/* line styles */#define SOLID 0#define DOTTED 1#define DASHED 2/* primary colors */#define BLUE 4#define GREEN 2#define RED 1/* mixed colors */#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; // or unsigned int (pre C99) unsigned int fill_color : 3; unsigned int : 4; bool show_border : 1; // or unsigned int (pre 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){ /* create and initialize box_props structure */ struct box_props box = {true, YELLOW , true, GREEN, DASHED}; 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。
2 位字段和按位运算符
在同类型的编程问题中,位字段和按位运算符是两种可替换的方法,用哪种方法都可以。例如,前面的例子中,使用和unsigned int类型大小相同的结构存储图形框的信息。也可使用unsigned int变量存储相同的信息。如果不想用结构成员表示法来访问不同的部分,也可以使用按位运算符来操作。一般而言,这种方法比较麻烦。接下来,我们来研究这两种方法(程序中使用了这两种方法,仅为了解释它们的区别,我们并不鼓励这样做)。 可以通过一个联合把结构方法和位方法放在一起。假定声明了struct boxprops类型,然后这样声明联合:
union Views /* look at data as struct or as unsigned short */{ struct box_props st_view; unsigned short us_view;};
在某些系统中,unsigned int和boxprops类型的结构都占用16位内存。但是,在其他系统中(例如我们使用的系统),unsigned int和boxprops都是32位。无论哪种情况,通过联合,都可以使用stview成员把一块内存看作是一个结构,或者使用usview成员把相同的内存块看作是一个unsigned short。结构的哪一个位字段与unsigned short中的哪一位对应?这取决于实现和硬件。下面的程序示例假设从字节的低阶位端到高阶位端载入结构。也就是说,结构中的第1个位字段对应计算机字的0号位(为简化起见,图15.3以16位单元演示了这种情况)。
程序dualview.c使用Views联合来比较位字段和按位运算符这两种方法。在该程序中,box是Views联合,所以box.stview是一个使用位字段的boxprops类型的结构,box.usview把相同的数据看作是一个unsigned short类型的变量。联合只允许初始化第1个成员,所以初始化值必须与结构相匹配。该程序分别通过两个函数显示box的属性,一个函数接受一个结构,一个函数接受一个unsigned short类型的值。这两种方法都能访问数据,但是所用的技术不同。该程序还使用了本章前面定义的itobs()函数,以二进制字符串形式显示数据,以便读者查看每个位的开闭情况。
The dualview.c Program
/* dualview.c -- bit fields and bitwise operators */#include #include #include /* BIT-FIELD CONSTANTS *//* line styles */#define SOLID 0#define DOTTED 1#define DASHED 2/* primary colors */#define BLUE 4#define GREEN 2#define RED 1/* mixed colors */#define BLACK 0#define YELLOW (RED | GREEN)#define MAGENTA (RED | BLUE)#define CYAN (GREEN | BLUE)#define WHITE (RED | GREEN | BLUE)/* BITWISE CONSTANTS */#define OPAQUE 0x1#define FILL_BLUE 0x8#define FILL_GREEN 0x4#define FILL_RED 0x2#define FILL_MASK 0xE#define BORDER 0x100#define BORDER_BLUE 0x800#define BORDER_GREEN 0x400#define BORDER_RED 0x200#define BORDER_MASK 0xE00#define B_SOLID 0#define B_DOTTED 0x1000#define B_DASHED 0x2000#define STYLE_MASK 0x3000const char * colors[8] = {"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"};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;};union Views /* look at data as struct or as unsigned short */{ struct box_props st_view; unsigned short us_view;};void show_settings(const struct box_props * pb);void show_settings1(unsigned short);char * itobs(int n, char * ps);int main(void){ /* create Views object, initialize struct box view */ union Views box = {{true, YELLOW , true, GREEN, DASHED}}; char bin_str[8 * sizeof(unsigned int) + 1]; printf("Original box settings:n"); show_settings(&box.st_view); printf("nBox settings using unsigned int view:n"); show_settings1(box.us_view); printf("bits are %sn", itobs(box.us_view,bin_str)); box.us_view &= ~FILL_MASK; /* clear fill bits */ box.us_view |= (FILL_BLUE | FILL_GREEN); /* reset fill */ box.us_view ^= OPAQUE; /* toggle opacity */ box.us_view |= BORDER_RED; /* wrong approach */ box.us_view &= ~STYLE_MASK; /* clear style bits */ box.us_view |= B_DOTTED; /* set style to dotted */ printf("nModified box settings:n"); show_settings(&box.st_view); printf("nBox settings using unsigned int view:n"); show_settings1(box.us_view); printf("bits are %sn", itobs(box.us_view,bin_str)); 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"); }}void show_settings1(unsigned short us){ printf("box is %s.n", (us & OPAQUE) == OPAQUE? "opaque": "transparent"); printf("The fill color is %s.n", colors[(us >> 1) & 07]); printf("Border %s.n", (us & BORDER) == BORDER? "shown" : "not shown"); printf ("The border style is "); switch(us & STYLE_MASK) { case B_SOLID : printf("solid.n"); break; case B_DOTTED : printf("dotted.n"); break; case B_DASHED : printf("dashed.n"); break; default : printf("unknown type.n"); } printf("The border color is %s.n", colors[(us >> 9) & 07]);}char * itobs(int n, char * ps){ int i; const static int size = CHAR_BIT * sizeof(int); for (i = size - 1; i >= 0; i--, n >>= 1) ps[i] = (01 & n) + '0'; ps[size] = '0'; return ps;}
下面是该程序的输出:
Original box settings: Box is opaque. The fill color is yellow. Border shown. The border color is green. The border style is dashed.
Box settings using unsigned int view: box is opaque. The fill color is yellow. Border shown. The border style is dashed. The border color is green. bits are 00000000000000000010010100000111
Modified box settings: Box is transparent. The fill color is cyan. Border shown. The border color is yellow. The border style is dotted.
Box settings using unsigned int view: box is transparent. The fill color is cyan. Border not shown. The border style is dotted. The border color is yellow. bits are 00000000000000000001011100001100
这里要讨论几个要点。位字段视图和按位视图的区别是,按位视图需要位置信息。例如,程序中使用BLUE表示蓝色,该符号常量的数值为4。但是,由于结构排列数据的方式,实际存储蓝色设置的是3号位(位的编号从0开始,参见图15.1),而且存储边框为蓝色的设置是11号位。因此,该程序定义了一些新的符号常量:
#define FILL_BLUE 0x8#define BORDER_BLUE 0x800
这里,0x8是3号位为1时的值,0x800是11号位为1时的值。可以使用第1个符号常量设置填充色的蓝色位,用第2个符号常量设置边框颜色的蓝色位。用十六进制记数法更容易看出要设置二进制的哪一位,由于十六进制的每一位代表二进制的4位,那么0x8的位组合是1000,而0x800的位组合是100000000000,0x800的位组合比0x8后面多8个0。但是以等价的十进制来看就没那么明显,0x8是8,0x800是2048。 如果值是2的幂,那么可以使用左移运算符来表示值。例如,可以用下面的#define分别替换上面的#define:
#define FILL_BLUE 1<<3#define BORDER_BLUE 1<<11
这里,<
enum { OPAQUE = 0x1, FILL_BLUE = 0x8, FILL_GREEN = 0x4, FILL_RED = 0x2, FILL_MASK = 0xE, BORDER = 0x100, BORDER_BLUE = 0x800, BORDER_GREEN = 0x400, BORDER_RED = 0x200, BORDER_MASK = 0xE00, B_DOTTED = 0x1000, B_DASHED = 0x2000, STYLE_MASK = 0x3000};
如果不想创建枚举变量,就不用在声明中使用标记。注意,按位运算符改变设置更加复杂。例如,要设置填充色为青色。只打开蓝色位和绿色位是不够的:
box.us_view |= (FILL_BLUE | FILL_GREEN); /* reset fill */
问题是该颜色还依赖于红色位的设置。如果已经设置了该位(比如对于黄色),这行代码保留了红色位的设置,而且还设置了蓝色位和绿色位,结果是产生白色。解决这个问题最简单的方法是在设置新值前关闭所有的颜色位。因此,程序中使用了下面两行代码:
box.us_view &= ~FILL_MASK; /* clear fill bits */box.us_view |= (FILL_BLUE | FILL_GREEN); /* reset fill */
如果不先关闭所有的相关位,程序中演示了这种情况:
box.us_view |= BORDER_RED; /* wrong approach */
因为BORDERGREEN位已经设置过了,所以结果颜色是BORDERGREEN |BORDERRED,被解释为黄色。这种情况下,位字段版本更简单:
box.st_view.fill_color = CYAN; /*bit-field equivalent */
这种方法不用先清空所有的位。而且,使用位字段成员时,可以为边框和框内填充色使用相同的颜色值。但是用按位运算符的方法则要使用不同的值(这些值反映实际位的位置)。 其次,比较下面两个打印语句:
printf("The border color is %s.n", colors[pb->border_color]);printf("The border color is %s.n", colors[(us >> 9) & 07]);
第1条语句中,表达式pb->bordercolor的值在0~7的范围内,所以该表达式可用作colors数组的索引。用按位运算符获得相同的信息更加复杂。一种方法是使用us>>9把边框颜色右移至最右端(0号位~2号位),然后把该值与掩码07组合,关闭除了最右端3位以外所有的位。这样结果也在0~7的范围内,可作为colors数组的索引。