为什么将值分配给位字段却没有返回相同的值?


96

我在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");
}

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

被禁用 !!

尽管在那篇文章中给出了与“符号位”相关的解释,但我无法理解,我们可能会设置一些东西,然后又不能按原样反映。

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


注意:两个标签因为它们的描述位域的标准略有不同,所以是必需的。请参阅有关C规范C ++规范的答案。


46
由于位域被声明为int我认为它只能容纳值0-1
奥西里斯(Osiris)

6
只需考虑一下int如何存储-1。所有位都设置为1。因此,如果只有一位,则显然必须为-1。所以1位int中的1和-1是相同的。将检查更改为“ if(s.enabled!= 0)”,它将起作用。因为不能为0。
尔根

3
的确,这些规则在C和C ++中是相同的。但是根据标签使用政策,我们只应将其标签为C并在不需要时避免交叉标签。我将删除C ++部分,它不应影响任何发布的答案。
伦丁

8
您是否尝试将其更改为struct mystruct { unsigned int enabled:1; };
ChatterOne

4
请仔细阅读C和C ++标记政策,尤其是通过社区共识在此处建立的有关交叉标记C和C ++的部分。我不会进行回滚战争,但是这个问题被错误地标记为C ++。即使由于各种TC导致语言略有不同,也要对C和C ++之间的区别提出一个单独的问题。
伦丁'18

Answers:


77

该标准对位字段的定义非常差。给定此代码struct mystruct {int enabled:1;};,那么我们知道:

  • 这会占用多少空间-如果有填充位/字节以及它们在内存中的位置。
  • 该位在内存中的位置。没有定义,也取决于耐力。
  • 无论是一个int:n位域是带符号被认为。

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

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

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

125)如上文6.7.2所述,如果实际使用的类型说明符为int或typedef-name定义为int,则由位定义是对位字段进行签名还是未签名。

如果将位字段视为,signed int而您又增加了一点位1,那么就没有数据空间,只有符号位。这就是为什么您的程序可能在某些编译器上给出奇怪结果的原因。

良好做法:

  • 切勿将位域用于任何目的。
  • 避免将有符号int类型用于任何形式的位操作。

5
在工作中,我们对位域的大小和地址有static_asserts,只是为了确保它们不会被填充。我们将位字段用于固件中的硬件寄存器。
迈克尔

4
@Lundin:使用#define-d掩码和偏移量的丑陋之处在于,您的代码被移位和按位AND / OR运算符所困扰。使用位域,编译器会为您解决这一问题。
迈克尔

4
@Michael 使用位域,编译器会为您解决这一问题。好吧,如果您的“照顾”标准是“不可移植的”和“不可预测的”,那就可以了。我的比那更高。
安德鲁·亨利

3
@AndrewHenle Leushenko说,仅从C标准本身的角度来看,是否选择遵循x86-64 ABI取决于实现。
mtraceur

3
@AndrewHenle对,我在这两点上都同意。我的观点是,我认为您与Leushenko的分歧归结为以下事实:您使用“实现定义”仅指既不是C标准严格定义也不是平台ABI严格定义的事物,而他是在使用它来引用到C标准没有严格定义的任何内容。
mtraceur '18

58

我无法理解,我们怎么可能设置一些东西,然后它却没有显示出来。

您是否在问为什么编译会给您一个错误?

是的,理想情况下应该会给您一个错误。如果您使用编译器的警告,它也会这样做。在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;
           ^

为什么要由实现定义而不是错误定义的原因可能与历史用法有更多关系,在这种情况下,要求进行强制转换将意味着破坏旧代码。该标准的作者可能认为警告足以弥补相关人员的懈怠。

为了说明一些规定,我将回应@Lundin的声明:“切勿将位域用于任何目的。” 如果您出于某种充分的理由而对底层的内存布局细节有所了解,那么您一开始便会认为自己需要位字段,那么几乎可以肯定的其他相关要求将与它们的规格不足相对应。

(TL; DR-如果您足够复杂,可以合法地“需要”位域,则它们的定义不够明确,无法为您提供服务。)


15
该标准的作者在设计位域章节的那一天正在放假。所以看门人必须这样做。这是毫无理由的任何关于位域是如何设计的。
伦丁

9
没有一致的技术原理。但这使我得出结论,这是有政治根据的:避免使任何现有代码或实现不正确。但是结果是,您可以依靠的位域很少。
John Bollinger

6
@JohnBollinger肯定存在适当的政治,这对C90造成了很多损害。我曾经与委员会的一位成员交谈过,他解释了很多废话的来源-不允许ISO标准支持某些现有技术。这就是为什么我们坚持使用moronic之类的东西,例如支持1的补码和有符号大小,实现的定义的有符号性char,支持非8位字节等等等。不允许它们使moronic计算机在市场上处于劣势。
伦丁'18

1
@Lundin看到那些认为权衡是错误的人的文章和验尸的集合,以及为什么会很有趣。我不知道对这些“我们上次做过,但没有成功”的研究已经成为通知下一个这样的案例的制度性知识,而不是人们头脑中的故事。
HostileFork说不要信任

1
这仍然列为点号。C2x宪章中C的原始原则之一:“现有代码很重要,现有实现不重要。” ...“没有人支持定义C的示例:假定所有现有实现都必须进行一些更改以符合标准。”
Leushenko '18 / 12/19

23

这是实现定义的行为。我假设您在其上运行的计算机使用二进制补码带符号整数,并且int在这种情况下将其视为带符号整数,以说明为什么不输入if语句的true部分。

struct mystruct { int enabled:1; };

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

实质上是一个带有符号位字段打交道时最大值为2^(bits - 1) - 1这是0在这种情况下。


“一旦签名,有效值为-1和0”。谁说签名?它不是定义的,而是实现定义的行为。如果已签名,则有效值为-+。2的补码无关紧要。
伦丁

5
@Lundin 1位二进制补码只有两个可能的值。如果该位置1,则由于它是符号位,因此为-1。如果未设置,则它为“正”0。我知道这是实现定义的,我只是在使用最常见的植入方法
NathanOliver

1
这里的关键是2的补码或任何其他带符号的形式都不能在单个可用位上起作用。
伦丁'18

1
@JohnBollinger我明白这一点。这就是为什么我要求判例式这是实现定义的原因。至少对于三巨头int,他们在这种情况下都被视为已签名。遗憾的是,位字段没有指定到位。基本上是此功能,请咨询您的编译器如何使用它。
NathanOliver

1
@Lundin,用于表示有符号整数的标准措辞可以很好地处理零值位的情况,至少在三个允许的选项中的两个中。之所以有效,是因为它将(负)位置值分配给符号位,而不是对其进行算法解释。
John Bollinger

10

您可以将其认为是2的补码系统中最左边的位是符号位。因此,设置了最左边的位的任何有符号整数都是负值。

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

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


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
    { /* ... */ }
}

—结束示例]


乍一看,粗体部分似乎可供解释。但是,当从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");
}

使用以上代码,它会发出警告,而不会-Wall -pedantic

警告:“ mystruct :: enabled”太小,无法容纳“枚举BOOL”的所有值 struct mystruct { BOOL enabled:1; };

输出为:

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

如果enum BOOL : int简单enum BOOL,则输出如上述标准提示所示:

已启用(使用时enum BOOL


因此,可以得出结论,也有其他答案很少,该int类型不足以仅在单个位字段中存储值“ 1”。


0

您对我所看到的位域的理解没有错。我看到的是,您首先将mystruct重新定义为struct mystruct {int enabled:1; }然后作为struct mystruct;。您应该编写的代码是:

#include <stdio.h>

struct mystruct { int enabled:1; };
int main()
{
    mystruct s; <-- Get rid of "struct" type declaration
    s.enabled = 1;
    if(s.enabled == 1)
        printf("Is enabled\n"); // --> we think this to be printed
    else
        printf("Is disabled !!\n");
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.