为什么要为一个不给出相同值的位字段赋值?

iam*_*ind 95 c c++ bit-fields signed-integer implementation-defined-behavior

我在Quora帖子中看到了以下代码:

#include <stdio.h>

struct mystruct { int enabled:1; };
int main()
{
  struct mystruct s;
  s.enabled = 1;
  if(s.enabled == 1)
    printf("Is enabled\n"); // --> we think this to be printed
  else
    printf("Is disabled !!\n");
}
Run Code Online (Sandbox Code Playgroud)

在C&C++中,代码的输出都是意外的,

被禁用 !!

虽然在那篇文章中给出了"符号位"相关的解释,但是我无法理解,我们如何设置某些内容然后它没有反映出来.

有人可以给出更详细的解释吗?


注意:标签都是必需的,因为它们的标准在描述位字段时略有不同.请参阅C规范C++规范的答案.

Lun*_*din 76

标准对比特字段的定义非常差.鉴于此代码struct mystruct {int enabled:1;};,我们知道:

  • 占用多少空间 - 如果有填充位/字节以及它们位于内存中的位置.
  • 位位于存储器中.未定义,也取决于endianess.
  • 是否将int:n位域视为有符号或无符号.

关于最后一部分,C17 6.7.2.1/10说:

位字段被解释为具有由指定位数组成的有符号或无符号整数类型125)

解释上述内容的非规范性说明:

125)如上面6.7.2中所规定的,如果使用的实际类型说明符是int或者定义为typedef-name int,则无论位字段是有符号还是无符号,它都是实现定义的.

如果要将位域视为signed int并且您制作了一些大小1,则没有数据空间,仅用于符号位.这就是为什么你的程序可能会在某些编译器上给出奇怪结果的原因.

好的做法:

  • 切勿将bit-fields用于任何目的.
  • 避免使用带符号int类型进行任何形式的位操作.

  • 在工作中,我们对位域的大小和地址进行static_assert,以确保它们没有填充.我们在固件中使用位域作为硬件寄存器. (4认同)
  • @Michael*对于bitfields,编译器会为你处理.*嗯,如果你的"照顾"的标准是"非便携式"和"不可预测的",那就没关系.我的高于此. (3认同)
  • @AndrewHenle对,我同意这两点.我的观点是,我认为你对Leushenko的不同意见归结为这样一个事实:你使用"实现定义"只是指那些既不是C标准严格定义的东西,也不是平台ABI严格定义的东西,而是他用它来引用任何不仅仅是C标准严格定义的东西. (3认同)
  • @Lundin:#define-d掩码和偏移的丑陋之处在于你的代码充满了移位和逐位的AND/OR运算符.使用位域编译器会为您处理. (2认同)
  • @AndrewHenle Leushenko说,从*C标准本身*的角度来看,无论是否选择遵循x86-64 ABI,都取决于实施. (2认同)

Hos*_*ork 57

我无法理解,我们如何设置某些内容然后它不会显示出来.

你问为什么它编译而不是给你一个错误?

是的,它理想情况下应该给你一个错误.如果您使用编译器的警告,它确实如此.在GCC中,有-Werror -Wall -pedantic:

main.cpp: In function 'int main()':
main.cpp:7:15: error: overflow in conversion from 'int' to 'signed char:1' 
changes value from '1' to '-1' [-Werror=overflow]
   s.enabled = 1;
           ^
Run Code Online (Sandbox Code Playgroud)

为什么这是由于实现定义与错误相关的原因可能更多地与历史使用有关,其中需要强制转换意味着破坏旧代码.该标准的作者可能认为警告足以弥补相关人员的不足.

为了引入一些规定主义,我将回应@Lundin的声明:"永远不要将比特字段用于任何目的." 如果你有充分的理由让你的内存布局细节低级别具体,那么你首先会想到你需要的位域,你几乎可以肯定的其他相关要求将会遇到他们的不足之处.

(TL; DR - 如果你足够复杂,合法地"需要"位字段,那么它们的定义不够明确,无法为你服务.)

  • 该标准的作者在节日章节设计的那天度假.所以看门人必须这样做.有关如何设计位域的_anything_没有理由. (14认同)
  • 没有连贯的*技术*理由.但这使我得出结论,有一个*政治*理由:避免使任何现有的代码或实现不正确.但结果是你可以依赖的位域很少. (8认同)
  • @JohnBollinger肯定存在政治,这对C90造成了很大的伤害.我曾经与委员会的一名成员交谈,他解释了许多废话的来源 - 不能允许ISO标准支持某些现有技术.这就是为什么我们陷入了一些愚蠢的事情,比如支持1的补码和签名的量级,实现定义的`char`的签名,支持非8位的字节等等.他们不允许将moronic计算机作为市场坏处. (5认同)

Nat*_*ica 22

这是实现定义的行为.我假设你运行它的机器使用twos-compliment signed整数并int在这种情况下将其视为有符号整数来解释为什么你不输入if语句的真正部分.

struct mystruct { int enabled:1; };
Run Code Online (Sandbox Code Playgroud)

声明enable为1位位字段.由于它已签名,因此有效值为-10.将字段设置为1溢出该位返回-1(这是未定义的行为)

基本上在处理有符号的位字段时,最大值2^(bits - 1) - 1就是0这种情况.

  • @Lundin 1位二进制补码号只有两个可能的值.如果该位置位,那么因为它是符号位,所以它是-1.如果未设置则为"正"0.我知道这是实现定义的,我只是使用最常见的植入来解释结果 (5认同)

Pau*_*vie 10

你可以把它想象成2的补码系统,最左边的位是符号位.因此,设置最左位的任何有符号整数都是负值.

如果您有1位有符号整数,则它只有符号位.因此,分配1给该单个位只能设置符号位.因此,当读回它时,该值被解释为负数,因此为-1.

1位有符号整数可以容纳的值是-2^(n-1)= -2^(1-1)= -2^0= -12^n-1= 2^1-1=0


iam*_*ind 8

根据C++标准n4713,提供了非常相似的代码片段.使用的类型是BOOL(自定义),但它可以应用于任何类型.

12.2.4

4如果将值true或false存储到bool任何大小的类型的位字段(包括一位位字段)中,则原始bool值和位字段的值应相等.如果枚举器的值存储在相同枚举类型的位字段中,并且位字段中的位数足以容纳该枚举类型的所有值(10.2),则原始枚举器值和比特字段的值应相等.[例如:

enum BOOL { FALSE=0, TRUE=1 };
struct A {
  BOOL b:1;
};
A a;
void f() {
  a.b = TRUE;
  if (a.b == TRUE)    // yields true
    { /* ... */ }
}
Run Code Online (Sandbox Code Playgroud)

- 结束例子]


乍一看,大胆的部分似乎是开放的解释.但是,正确的意图在enum BOOL得到的时候变得清晰int.

enum BOOL : int { FALSE=0, TRUE=1 }; // ***this line
struct mystruct { BOOL enabled:1; };
int main()
{
  struct mystruct s;
  s.enabled = TRUE;
  if(s.enabled == TRUE)
    printf("Is enabled\n"); // --> we think this to be printed
  else
    printf("Is disabled !!\n");
}
Run Code Online (Sandbox Code Playgroud)

使用上面的代码,它会发出警告,但不-Wall -pedantic:

警告:'mystruct :: enabled'太小,无法容纳'enum BOOL'的所有值 struct mystruct { BOOL enabled:1; };

输出是:

被禁用 !!(使用时enum BOOL : int)

如果enum BOOL : int变得简单enum BOOL,那么输出就像上面的标准pasage所指定的那样:

启用(使用时enum BOOL)


因此,可以得出结论,即使其他答案很少,该int类型也不足以在单个比特位字段中存储值"1".