微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

赋值为 1 的 C 位域显示 -1

如何解决赋值为 1 的 C 位域显示 -1

我玩过位域并坚持了一些奇怪的事情:

#include <stdio.h>

struct lol {
     int a;
     int b:1,c:1,d:1,e:1;
     char f;
};

int main(void) {
     struct lol l = {0};
     l.a = 123;
     l.c = 1; // -1 ???
     l.f = 'A';

     printf("%d %d %d %d %d %c\n",l.a,l.b,l.c,l.d,l.e,l.f);

     return 0;
}

输出为:

123 0 -1 0 0 A

不知何故,l.c 的值是 -1。什么原因?
对不起,如果明显。

解决方法

如果您不想要符号扩展,请使用 unsigned 位域。

您得到的是您的 1 位被解释为 two's complement 表示中的符号位。在二进制补码中,符号位是最高位,它被解释为 -(2^(width_of_the_number-1)),在您的情况下为 -(2^(1-1)) == -(2^0) == -1。通常所有其他位都会抵消这一点(因为它们被解释为正数)但 1 位数字没有也不能有其他位,所以你只得到 -1

以二进制补码中的 0b10000000 as a as int8_t 为例。 (为了记录,0b10000000 == 0x80 && 0x80 == (1<<7))。它是最高位,因此它被解释为 -(2^7) (==-128) 并且没有正位来抵消它,所以你得到 printf("%d\n",(int8_t)0x80); /*-128*/

现在,如果您将所有位都设置为开启,则会得到 -1,因为 -128 + (128-1) == -1。这(== -1 上的所有位)适用于任何解释为二进制补码的宽度 - 即使对于宽度 1,您得到 -1 + (1-1) == -1`。

当这样的有符号整数扩展到更宽的宽度时,它会经历所谓的符号扩展

符号扩展意味着最高位被复制到所有新添加的高位中。

如果最高位为 0,那么很容易看到符号扩展不会改变值(例如 0x01 扩展为 0x00000001)。

当最高位为 1 时,如 (int8_t)0xff 中(所有 8 位均为 1),然后符号扩展将符号位复制到所有新位中:((int32_t)(int8_t)0xff == (int32_t)0xffffffff)((int32_t)(int8_t)0x80 == (int32_t)0xffffff80) 可能是一个更好的例子,因为它更清楚地表明在高端添加了 1 位(尝试使用 _Static_assert 中的任何一个)。

只要您假设二进制补码,这也不会改变值,因为如果您从以下位置开始:

-(2^n) (value of sign bit) + X (all the positive bits) //^ means exponentiation here

并在最高位置再增加一位,然后你得到:

-(2^(n+1)) + 2^(n) +  X

这是

2*-(2^(n)) + 2^(n) +  X == -(2^n) + X //same as original
//inductively,you can add any number of 1 bits

符号扩展通常发生在您使用强制转换或隐式转换将本机有符号整数宽度扩展为本机更宽的宽度(有符号或无符号)时。 对于原生宽度,平台通常会有相应的说明。

示例:

int32_t signExtend8(int8_t X) { return X; }

示例在 x86_64 上的反汇编:

signExtend8:
        movsx   eax,dil //the sx stands for Sign-eXtending
        ret

如果你想让它适用于非标准宽度,你通常可以利用这样一个事实,即有符号右移通常会在移位范围旁边复制符号位(它实际上是实现定义了什么有符号右移做) 因此,您可以无符号左移到符号位,然后返回以人为地为非本机宽度(例如 2)进行符号扩展:

#include <stdint.h>
#define MC_signExtendIn32(X,Width) ((int32_t)((uint32_t)(X)<<(32-(Width)))>>(32-(Width)))
_Static_assert( MC_signExtendIn32(3,2 /*width 2*/)==-1,"");
int32_t signExtend2(int8_t X) { return MC_signExtendIn32(X,2); }

反汇编 (x86_64):

signExtend2:
        mov     eax,edi
        sal     eax,30
        sar     eax,30
        ret

签名位域本质上使编译器为您生成(隐藏)宏:

struct bits2 { int bits2:2; };
int32_t signExtend2_via_bitfield(struct bits2 X) { return X.bits2; }

在 clang 上反汇编 (x86_64):

signExtend2_via_bitfield:               # @signExtend2_via_bitfield
        mov     eax,edi
        shl     eax,30
        ret

godbolt 上的示例代码:https://godbolt.org/z/qxd5o8 .

,

位域的标准化非常差,通常不能保证其行为可预测。该标准只是含糊地指出 (6.7.2.1/10):

位域被解释为具有有符号或无符号整数类型,包括 指定的位数。125)

信息说明 125) 说:

125) 如上面 6.7.2 中所述,如果使用的实际类型说明符是 int 或定义为 int 的 typedef-name, 那么位域是有符号还是无符号由实现定义。

所以我们无法知道 int b:1 给出的是有符号类型还是无符号类型,这取决于编译器。您的编译器显然认为具有符号位是一个好主意。因此,它将您的 1 位视为转换为二进制补码的 1 位数字,其中二进制 1 是十进制 -1,零是零。


此外,我们无法知道您代码中的 b 在内存中的最终位置,它可能位于任何地方,并且还取决于字节序。我们所知道的是,在这里使用位字段绝对不会节省任何内存,因为无论如何都会为 int 分配至少 16 位。

一般的好建议:

  • 切勿出于任何目的使用位域。
  • 改用按位运算符 << >> | & ^ ~ 和命名位掩码,以获得 100% 可移植且定义良好的代码。
  • 在处理原始二进制文件时使用 stdint.h 类型或至少 unsigned 类型。
,

您正在使用有符号整数,并且由于二进制中 1 的表示的第一个位设置为 1,因此在带有负符号存在的有符号表示中,因此您得到-1。正如其他评论所建议的那样,使用 unsigned 关键字来消除表示负整数的可能性。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。