C语言结构体位域及其存储

前言

在一些应用中,比如网络协议,经常会涉及对数据的某些比特位进行操作,尽管可以使用位的相关运算,但是C语言提供了位域用以支持对一个字节的某几个位进行访问,操作起来也更加方便。本文关注于说明C语言中位域的使用及其在内存中的排列规则,尤其在大小端平台下位域存储的差异。

位域的定义与引用

位域不同于一般的结构体成员,它以位为单位来定义成员的长度,因此在结构体中定义位域时,必须要指明位域成员所需要占用的二进制位数。一个简单的定义位域的示例如下:

struct Foo {
    int a: 5;   // 数据类型名    变量名:二进制位数
    int b: 3;
    int c: 2;
};

由于位域本质上是一种特殊的结构体成员,因此一般结构体成员的引用方法同样适用于位域成员。不过,需要特别注意的是,位域成员存储是以二进制位作为单位的,而内存的最小寻址单元是字节,所以不能直接引用位域成员的地址。

匿名位域

在使用位域时,如果有需要可以选择跳过某些位不使用,其方法是在结构体类型中定义位域成员时,只指定成员占用的二进制位数,而不定义成员名。由于被跳过的这些位域成员没有名字,因此在程序中也无法进行引用。

struct Foo {
   int a: 3;
   int b: 4;
   int : 5;    // 定义匿名位域,选择性跳过部分位不使用
   int c: 3;
};

特别的,我们可以根据需要定义二进制位数为0的匿名位域成员,目的是指定某些位域从一个新的内存单元开始存放。

位域的存储

结构体中位域在内存中的存储遵循以下规则:

  • 若相邻的位域成员的类型相同,且其占用二进制位数未超过该类型可容纳的范围,则后面的成员紧接着前一个成员进行存储;
  • 若相邻的位域成员的类型相同,但其占用二进制位数超过该类型可容纳的范围,则后面的成员从该类型占用空间之后的内存单元开始存储;
  • 若相邻的位域成员的类型不同,则取决于编译器的实现。对于GCC编译器会尽量利用空闲的位对数据进行存储;
  • 若位域之间定义有匿名位域成员,则匿名位域成员指定的空闲位不用于后续成员的数据存储;

说明示例1

考虑如下结构体,该结构体内的定义几乎使用到了上面提到的所有规则:
在这里插入图片描述

  1. 对于成员a、b、c再加上匿名成员占用的总内存空间并未超过unsigned short类型空间的大小,因此在unsigned short类型可容纳的范围内,这些成员可以紧挨着存放;
  2. 当后续存放成员d时,前一个unsigned short类型数据剩余的空间已不足以容纳d,因此选择下一个内存单元进行存放。

特别地,如果定义上述结构体中匿名成员占用的位数为0,那么对于第一个unsigned short类型数据在除被a、b占用区域的其它剩余空间都将不会被使用,则成员c需要从byte2开始进行存储。

说明示例2

现在考虑相邻位域成员类型不同的情况,定义如下结构体并画出其在内存中的布局如下(这个结果基于gcc编译器):
在这里插入图片描述
GCC编译器会尽可能地利用空闲的内存位进行位域成员的存放,这里有一点需要注意的是,尽管对于第一个unsigned char类型可容纳的单字节范围在存放完成员a和b后,剩余的一位已不足以存放成员c,但是GCC编译器仍然将这一位分配给了c。

位域与大小端

系统的大小端差异会同时牵涉到字节序和比特序问题,对于结构体位域这种会涉及比特位层面数据的操作,几乎需要时刻考虑平台大小端的差异。结构体内位域成员在大小端系统上的内存分配规则如下:

  • 无论是大端或小端模式,位域的存储都是由内存低地址向高地址分配,即从低地址字节的低位bit开始向高地址字节的高位bit分配空间;
  • 位域成员在已分配的内存区域内,按照机器定义的比特序对数据的各个bit位进行排列。即在小端模式中,位域成员的最低有效位存放在内存低bit位,最高有效位存放在内存高bit位;大端模式则相反。

程序示例

为了说明上述的规则,参考如下代码:

struct Foo2 {
    unsigned short a: 1;
    unsigned short b: 3;
    unsigned short c: 4;
    unsigned short d: 4;
    unsigned short e: 4;
};

void test_foo2(void)
{
    union {
        struct Foo2 foo;
        unsigned short s_data;
    }val;

    val.foo.a = 1;
    val.foo.b = 3;
    val.foo.c = 5;
    val.foo.d = 7;
    val.foo.e = 15;

    printf("val is 0x%x.\n", val.s_data);

    return;
}

运行上述程序:

  • 在小端设备上的输出的结果为:0xf757
  • 换成大端设备,程序的运行结果为:0xb57f

我们画出程序所定义结构体位域成员在内存中的存储布局如下所示(因为平时多数都是在使用小端机器,在此有意将字节的顺序和字节内的位顺序进行颠倒,方便对比):
在这里插入图片描述

为了更便于理解位域在内存中的排列规则,建议将位域成员内存空间的分配和解析分开来看:

  1. 第一步先考虑内存空间的分配,从上图中可以看到,不论大小端都是从内存地址的低位开始;
  2. 当位域成员占用空间确定之后,考虑于位域成员数据位的排布,可以看到小端系统从低bit位开始存放数据,这是符合我们预期的,而大端设备则恰恰相反,大端系统从高bit位开始存放数据,因此在大端设备中,我们需要转换下思维从内存的高位开始解析数据位。

相关参考

  • 《C语言深度剖析》
  • 《程序设计艺术》
posted @ 2020-06-21 21:37  Aspiresky  阅读(274)  评论(0编辑  收藏  举报