c进阶篇(六)——结构体位域

前言:
  结构体位域可以方便对Bit级数据的操作,可以更有效的利用Bit数据空间。

1 数据存储大小端

  大小端表示数据在计算机中的存放顺序。

  • 小端模式:低字节保存在内存低地址,高字节保存在内存高地址。
  • 大端模式:低字节保存在内存高地址,高字节保存在内存低地址。

  在使用结构体位域操作和联合嵌套结构体时尤其需要注意数据存储的大小端问题。如果使用的是单片机,可以查看用户手册了解所用型号的单片机。

2 定义位域结构体

   在符号 : 之后给出数据类型所占的Bit数,对数据位域长度进行限制,以下为了方便均假设数据存储为小段模式,即结构体中先定义的成员变量存储在低地址和低Bit字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdint.h>

typedef uint8_t Byte;
typedef uint16_t Word;
typedef uint32_t DWord;

typedef struct DatFld
{
Byte byDat0 : 4u;
Byte byDat1 : 2u;
Byte byDat2 : 2u;
}STDatFld;

int main(void)
{
STDatFld stDatFld = {.byDat0 = 0xFFu, .byDat1 = 0xFFu, .byDat2 = 0xFFu};

printf("%d\n", sizeof(STDatFld));
printf("%d, %d, %d\n", stDatFld.byDat0, stDatFld.byDat1, stDatFld.byDat2);
}

  以上代码有警告,因为对结构体初始化赋值超过了其存储范围。该结构体的大小只占1个字节,如果不用位域那么该结构体将占3个Byte。也即位域提高了存储空间的利用率,节省了空间。
  若想要方便的操作一个Byte中的每个Bit,可以定义如下位域结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
#include <stdint.h>

typedef uint8_t Byte;
typedef uint32_t DWord;

typedef union ByteFld
{
Byte byDat;

struct
{
Byte byBit0 : 1u;
Byte byBit1 : 1u;
Byte byBit2 : 1u;
Byte byBit3 : 1u;
Byte byBit4 : 1u;
Byte byBit5 : 1u;
Byte byBit6 : 1u;
Byte byBit7 : 1u;
};
}UByteFld;

int main(void)
{
UByteFld uByteFld = {.byDat = 0x55};

printf("%d\n", uByteFld.byBit0);
printf("%d\n", uByteFld.byBit1);
printf("%d\n", uByteFld.byBit2);
printf("%d\n", uByteFld.byBit3);
printf("%d\n", uByteFld.byBit4);
printf("%d\n", uByteFld.byBit5);
printf("%d\n", uByteFld.byBit6);
printf("%d\n", uByteFld.byBit7);
}

  以上代码通过访问结构体位域成员,直接访问了Byte中Bit数据,也即结构体方便了Bit数据的操作。

3 无名位域

  在使用位域时,如果想要挑过中间的字段,可以使用无名位域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <stdint.h>

typedef uint8_t Byte;
typedef uint16_t Word;
typedef uint32_t DWord;

typedef union DatFld
{
Byte byDat;

struct
{
Byte byDat0_3 : 4u;
Byte : 2u; //无名位域。
Byte byDat6_7 : 2u;
};
}UDatFld;

int main(void)
{
UDatFld uDatFld = {.byDat = 0xFFu};

printf("%d, %d\n", uDatFld.byDat0_3, uDatFld.byDat6_7);
}

  上面的结构体位域跳过了第4、5Bit字段,这就是无名位域的好处。
  需要注意的是无名位域不能跨结构体成员,以下代码是错误示范。

1
2
3
4
5
6
7
8
9
10
11
typedef union DatFld
{
Word wDat;

struct
{
Byte byDat0_6 : 7u;
Byte : 2u; //Error,此处错误,不能跨结构体成员。
Byte byDat9_15 : 7u;
};
}UDatFld;

  因为结构体成员是Byte类型,而无名位域想要跳过第7Bit和8Bit,而这两个Bit的存储空间原本分别属于两个结构体成员内。 应该如下正确的使用位域。

1
2
3
4
5
6
7
8
9
10
11
12
typedef union DatFld
{
Word wDat;

struct
{
Byte byDat0_6 : 7u;
Byte : 1u; //无名位域。
Byte : 1u; //无名位域。
Byte byDat9_15 : 7u;
};
}UDatFld;

4 空域

  当结构体成员的位域划分没有超过一个数据类型的空间时,其将会共同占用一个类型的存储空间。但不允许单个位域的空间划分超过一个结构体成员数据类型的空间。以下代码是错误示范。

1
2
3
4
5
6
7
8
9
10
typedef union DatFld
{
Word wDat;

struct
{
Byte byDat0_8 : 9u; //Error,位域空间划分超过了Byte
Byte byDat9_15 : 6u;
};
}UDatFld;

  正确的位域划分应该如下。

1
2
3
4
5
6
7
8
9
10
typedef union DatFld
{
Word wDat;

struct
{
Byte byDat0_7 : 8u;
Byte byDat8_15 : 8u;
};
}UDatFld;

  若想将 Word 类型的数据低4Bit放在一个结构体成员变量中,剩下的8Bit放在另一个结构体变量中,也即跳过了4Bit,此时可以使用空域。空域可以放弃当前数据类型剩下的空间,空间划分从下一个字节继续。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <stdint.h>

typedef uint8_t Byte;
typedef uint16_t Word;
typedef uint32_t DWord;

typedef union DatFld
{
Word wDat;

struct
{
Byte byDat0_3 : 4u;
Byte : 0u; //空域。
Word wDat8_15 : 8u;
};
}UDatFld;

int main(void)
{
UDatFld uDatFld = {.wDat = 0xFFFFu};

printf("%d, %d\n", uDatFld.byDat0_3, uDatFld.wDat8_15);
}

  使用空域的方式可以被无名域等价代替。同等效果的无名域使用如下。

1
2
3
4
5
6
7
8
9
10
11
typedef union DatFld
{
Word wDat;

struct
{
Byte byDat0_3 : 4u;
Byte : 4su; //无名域。
Word wDat8_15 : 8u;
};
}UDatFld;