避免Bug:混用位域bit-fields和联合union时要谨慎

9156阅读 3评论2012-01-17 GFree_Wind
分类:C/C++

作者:gfree.wind@gmail.com
博客:blog.focus-linux.net   linuxfocus.blog.chinaunix.net
 
 
本文的copyleft归gfree.wind@gmail.com所有,使用GPL发布,可以自由拷贝,转载。但转载请保持文档的完整性,注明原作者及原链接,严禁用于任何商业用途。
======================================================================================================
今天在做代码的enhancement,其中涉及到修改一个结构体,类似于下面的代码:
  1. struct my_type {
  2.    unsigned int flag;
  3. #define MY_FLAG_1 0x1
  4. #define MY_FLAG_2 0x2
  5. #define MY_FLAG_3 0x4
  6. };
这里struct my_type->flag作为标志位,通过宏来定义不同的标志。其实我觉得这样的代码是可以接受的。但是我们的工程代码中,更多的是应用位域来表示标志位。为了风格的统一,我决定修改这个结构体,改为:
  1. struct my_type {
  2.    unsigned int flag1:1,
  3.                 flag2:1,
  4.                 flag3:1,
  5.                 spare:29;
  6. };
然后再修改所有使用原标志位的地方。位域确实会比使用标志位更清晰,但是会有如下的老代码:
  1. struct my_type t1, t2;

  2. t1.flag = t2.flag
原来仅仅是对一个变量struct my_type->flag进行赋值。现在使用位域,代码就要写为
  1. struct my_type t1, t2;

  2. t1.flag1 = t2.flag1;
  3. t2.flag2 = t2.flag2;
  4. t3.flag3 = t2.flag3;
比以前使用一个flag变量要麻烦冗余的多——这里不适用memcpy,只是为了复制标志位。这样的代码还有不少地方。改为位域,代码的清晰度和可读性是比以前好了,但是这样的代码看上去比以前麻烦的多。
于是我很自然的使用了union来避免这个问题:参加我这篇关于使用union来重构的文章http://blog.chinaunix.net/space.php?uid=23629988&do=blog&id=158156

新的结构体定义如下:
  1. struct my_type {
  2.    union {
  3.        unsigned int flag1:1,
  4.                     flag2:1,
  5.                     flag3:1,
  6.                     spare:33;
  7.        unsigned int all_flags;
  8.    };
  9. };


  10. struct my_type t1, t2;

  11. t1.all_flags = t2.all_flags;
我的意图很明显,通过使用union,在不增加存储空间的条件下,既可以直接使用flag1,flag2和flag3,而当复制标志位时,通过共享的存储空间,可以直接复制all_flags来复制所有的标志位。

Ok。开始测试自己的修改,发现程序运行有问题——聪明的朋友应该已经知道问题是什么了。
下面是一个示例程序:
  1. #include <stdlib.h>
  2. #include <stdio.h>
  3. #include <string.h>

  4. struct my_type {
  5.    union {
  6.        unsigned int flag1:1,
  7.                     flag2:1,
  8.                     flag3:1,
  9.                     spare:29;
  10.        unsigned int all_flags;
  11.    };
  12. };

  13. int main(void)
  14. {
  15.     struct my_type t;

  16.     memset(&t, 0, sizeof(t));
  17.     t.flag1 = 1;

  18.     printf("%d %d %d\n", t.flag1, t.flag2, t.flag3);

  19.     return 0;

  20. }
编译输出:
  1. [fgao@fgao-vm-fc13 test]$ ./a.out
  2. 1 1 1

看到程序的输出,我就知道自己想的过于草率了。当时认为flag1,flag2,flag3及spare是属于一个unsigned int的不同存储位置,可以和后面的my_flags共享一个unsigned int的存储空间。这里绝对的想岔了。

还是想引用一下C99标准关于union的定义:
—A union type describes an overlapping nonempty set of member objects, each of
which has an optionally specified name and possibly distinct type.

通过union的定义,可以清楚的看到,union针对的是member objects。只要是union的成员,都是共享一个重叠的存储空间。那么对于上面的例子来说,flag1,flag2,flag3和spare都是从union的一个unsigned int的最低位开始存储——小端从最低位开始,大端从最高位开始。也就是说flag1,flag2和flag3共享了最低位。

问题是搞清楚了,但是对自己却是一个提醒。如果说给我写出这些代码,让我review的话,我可能会立刻看出问题。但是这些代码是我自己写出的时候,却会写出这样的代码。看来,还是对union的概念不够清楚。

最后,如果想改正这个问题的话,也比较简单。
  1. #include <stdlib.h>
  2. #include <stdio.h>
  3. #include <string.h>

  4. struct my_type {
  5.    union {
  6.        struct {
  7.            unsigned int flag1:1,
  8.                         flag2:1,
  9.                         flag3:1,
  10.                         spare:29;
  11.        };
  12.        unsigned int all_flags;
  13.    };
  14. };

  15. int main(void)
  16. {
  17.     struct my_type t;

  18.     memset(&t, 0, sizeof(t));
  19.     t.flag1 = 1;

  20.     printf("%d %d %d 0x%X\n", t.flag1, t.flag2, t.flag3, t.all_flags);

  21.     return 0;

  22. }
再用一个独立的匿名struct封装flag1,flag2,flag3和spare。这样对于union来说,将把这一struct视为它的成员变量,就不会出现上面的问题了。但是这样的定义,看上去就有些奇怪了。是否选择这种方法,就是仁者见仁智者见智了。我是没有使用这种方法,而是老老实实地的复制flag1,flag2和flag3。


注:这里的代码union和struct是应用了GNU的扩展特性匿名的union和struct。

上一篇:如何写出健壮的代码
下一篇:避免Bug: C与C++对于全局变量的不同处理之处

文章评论