位域的赋值是否与字节顺序实现相关?

4

众所周知,计算机有两种字节序:大端字节序和小端字节序。

假设一个整数占据4个字节,那么整数1在小端字节序下的布局应该是0x01 0x00 0x00 0x00,在大端字节序下的布局应该是0x00 0x00 0x00 0x01。

要检查一台计算机是小端字节序还是大端字节序,可以编写以下代码:

int main()
{
    int a = 1;
    char *p = (char *)&a;
    // *p == 1 means little endian, otherwise, big endian

    return 0;
}

据我所知,对于小端字节序,*p 被赋值为第一个八位字节:0x01,而对于大端字节序,则被赋值为 0x00(以上两个加粗的部分),这就是代码的工作方式。

现在我不太理解不同字节序下位域(bit field)是如何工作的。

假设我们有如下结构体:

typedef struct {
    unsigned char a1 : 1;
    unsigned char a2 : 1;
    unsigned char a6 : 3;
}Bbit;

接下来我们按照以下方式进行任务分配:

Bbit bit;
bit.a1 = 1;
bit.a2 = 1;

这段代码是不是跟具体的实现相关呢?我的问题是,在小端序的情况下,bit.a1bit.a2的值是否为1,在大端序的情况下是否为0?还是说无论大小端序如何,它们的值都一定是1

8
位域的布局是由实现定义的。编译器可以把位放置在任何位置。 - paddy
1
假设一个整数占用4个字节。有趣的是关注不同的字节序,而不是不同的int大小。 - chux - Reinstate Monica
1
位域存在许多实现定义问题。 "位域应具有限定或未限定版本的_Bool、signed int、unsigned int或其他实现定义类型。" 因此,即使是 OP 的 unsigned char a1: 1; 也缺乏可移植性。 - chux - Reinstate Monica
1
@Yves 位域是C语言中一个int可能与signed int不同的地方 --> "具体实现未定义,指定符int是指signed int还是unsigned int。" int a1 : 1; 可能编码为-1,0或0,1。 - chux - Reinstate Monica
7
我们有两种字节序: --> 无法抵制:PDP-endian - chux - Reinstate Monica
显示剩余11条评论
3个回答

2

使用位域,不仅字节序是实现定义的,位序也是如此。

C标准第6.7.2.1条款11关于结构体的规定如下:

一个实现可以为了容纳位域而分配任何可寻址存储单元。如果剩余空间足够,则立即跟在另一个结构体中的位域后面的位域应该被打包到同一单元的相邻位。如果剩余空间不足,未能放入的位域将根据实现定义,放入下一个单元或与相邻单元重叠。在单元内分配位域的顺序(从高位到低位或从低位到高位)由实现定义。可寻址存储单元的对齐方式是未指定的。

因此,编译器可以随意重新排列结构体中的位域。以下是Linux上/usr/include/netinet/ip.h中表示IP头的结构体的示例:

struct iphdr
  {
#if __BYTE_ORDER == __LITTLE_ENDIAN
    unsigned int ihl:4;
    unsigned int version:4;
#elif __BYTE_ORDER == __BIG_ENDIAN
    unsigned int version:4;
    unsigned int ihl:4;
#else
# error "Please fix <bits/endian.h>"
#endif
    u_int8_t tos;
    u_int16_t tot_len;
    u_int16_t id;
    u_int16_t frag_off;
    u_int8_t ttl;
    u_int8_t protocol;
    u_int16_t check;
    u_int32_t saddr;
    u_int32_t daddr;
    /*The options start here. */
  };

在这里,你可以看到有两个位域字段,它们的声明顺序取决于使用的字节序。

这意味着,如果你发送一个原始结构体到网络上,就不能依赖于任何特定的字节(或位)顺序。

以你的示例为基础,加上一些查看表示的内容:

Bbit bit;
bit.a1 = 1;
bit.a2 = 1;
unsigned char *p = (unsigned char *)&bit;
printf("%02x\n", *p);

一个大端系统可能会打印出a0,而一个小端系统可能会打印出03。这是假设未使用的位被设置为0的情况下。

1

Let's say we have a struct:

typedef struct {
    unsigned char a1 : 1;
    unsigned char a2 : 1;
    unsigned char a6 : 3;
}Bbit;

以及一个定义:

Bbit byte;

假设一个字节(byte)被存储在单个字节中,并且当前为零: 0000 0000
byte.a1 = 1;

这将把名为a1的位设置为1。如果a1是第一位,则byte变为1000 0000,但如果a1是第五位,则byte变为0000 1000,如果a1是第八位,则byte变为0000 0001
byte.a2 = 1;

这将把名为a2的位设置为1。如果a2是第二位,那么byte(很可能)变为1100 0000,但如果a2是第六位,则byte(很可能)变为0000 1100,如果a2是第七位,则byte变为0000 0011。(这些只是“可能”,因为没有保证位跟随某些合理的顺序。编译器不太可能刻意搞砸这个例子。)
端序与存储的值无关。每次赋值时,仅更改表示指定位域的位,并将要分配的值减少到该位数(如果该值对于该位数过大,则具有实现定义的行为)。

使用 unsigned char 作为位域底层类型是不可移植的,唯一保证支持的类型是 _Boolsigned intunsigned int - M.M

1
C标准甚至不要求表示整数的字节必须按照大端序或小端序排列(它们可能混合),更不用说位域的顺序。这些内容是实现定义的,也就是说C标准没有明确规定它们,但要求在编译器手册或其他文档中记录它们。位域在字节或其他单元中的顺序不必与对象中字节的顺序匹配。

C11 6.7.2.1/11 确实表明下一个位域必须与前一个字段相邻(除非单元中剩余的空间不足),并且它们必须按一致的顺序排列(无论是低位到高位还是高位到低位),我认为这应该在比特的逻辑顺序的背景下进行解释,而不考虑字节的大小端。 - M.M
“在结构体中紧随另一个位域后面的位域”是指按照源代码中定义的位域,且“相邻位域被打包到相邻的位中”并不意味着任何顺序,它只是指“相邻”。 - Andrew Henle
@AndrewHenle:如果a相邻于b,且b相邻于c,则必须有abc或cba。它不能是acb。因此,在用于存储它们的存储单元的位的逻辑排序中,位字段必须按升序或降序排列。 - Eric Postpischil
@EricPostpischil 我并不严格地阅读6.7.2.1p11。我没有看到对一致排序的要求。如果一个实现决定将在源代码中定义为abcde的位域拆分,那么即使它将字段放置在一个单元中的cba中,然后在下一个单元中放置de,它仍将符合规范。我在“单元内位域分配顺序(从高位到低位或从低位到高位)是由实现定义的”中没有看到要求从一个单元到另一个单元保持一致性,尽管这样的实现可能看起来毫无意义。 - Andrew Henle

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接