位域中的溢出问题

20

我可以相信C编译器每次访问位字段时都会执行模2 ^ n操作吗?还是有任何编译器/优化,像下面的代码一样不会输出Overflow?

struct {
  uint8_t foo:2;
} G;

G.foo = 3;
G.foo++;

if(G.foo == 0) {
  printf("Overflow\n");
}

提前感谢,Florian


你的意思是什么?你明确要求将数据存储在两个位中。此外,3 + 1 == 0 mod 4。 - Foo Bah
我更关心的是,如果在该结构中声明了其他位域,会对该位产生什么影响。例如:uint8_t bar:2; uint8_t foo:2; uint8_t poo:2; 这会影响bar还是poo? - Dave
1
Charles,它只使用2位进行存储,因此它只能存储0、1、2或3。 - Dave
4个回答

19
是的,只要位域用无符号类型声明,你可以信任C编译器会做正确的事情,就像你使用了uint8_t。从C99标准§6.2.6.1/3可知:

存储在无符号位域和类型为unsigned char的对象中的值应使用纯二进制表示法。40)

从§6.7.2.1/9可知:

位域被解释为由指定位数组成的带符号或无符号整数类型。104)如果将值0或1存储到类型为_Bool的非零宽度位域中,则该位域的值应与存储的值相等。

而从§6.2.5/9中得知(我加了强调):

有符号整数类型的非负值范围是相应无符号整数类型的子范围,且每种类型中同一值的表示方式相同。31) 由无符号操作数计算的计算永远不会溢出,因为不能由结果无符号整数类型表示的结果将对能够由结果类型表示的最大值加一取模。

所以,任何符合标准的编译器都会使G.foo溢出为0,而没有其他不良影响。

正如R..在下面的评论中指出的那样,计算实际上是有符号的计算。这并不改变这里的答案,但在一个整数为32位的架构上使用31位无符号位域时(可能存在未定义的有符号溢出),情况就会发生变化。 - Pascal Cuoq

3

不行。编译器为该字段分配了2个比特,并将3递增得到100b,当放置在两个比特中时,结果为0。


1

是的。我们可以从汇编中得到答案。 这是我在Ubuntu 16.04,64位,gcc中编写的一个示例。

#include <stdio.h>

typedef unsigned int uint32_t;

struct {
  uint32_t foo1:8;
  uint32_t foo2:24;
} G;

int main() {
    G.foo1 = 0x12;
    G.foo2 = 0xffffff; // G is 0xfffff12
    printf("G.foo1=0x%02x, G.foo2=0x%06x, G=0x%08x\n", G.foo1, G.foo2, *(uint32_t *)&G);
    G.foo2++; // G.foo2 overflow
    printf("G.foo1=0x%02x, G.foo2=0x%06x, G=0x%08x\n", G.foo1, G.foo2, *(uint32_t *)&G);
    G.foo1 += (0xff-0x12+1); // // G.foo1 overflow
    printf("G.foo1=0x%02x, G.foo2=0x%06x, G=0x%08x\n", G.foo1, G.foo2, *(uint32_t *)&G);
    return 0;
}

使用gcc -S <.c文件>编译它。你可以得到汇编文件.s。这里我展示了G.foo2++;的汇编代码,并写了一些注释。

movl    G(%rip), %eax
shrl    $8, %eax    #  0xfffff12-->0x00ffffff
addl    $1, %eax    # 0x00ffffff+1=0x01000000
andl    $16777215, %eax # 16777215=0xffffff, so eax still 0x01000000
sall    $8, %eax    # 0x01000000-->0x00000000
movl    %eax, %edx  # edx high-24bit is fool2
movl    G(%rip), %eax   # G.foo2, tmp123
movzbl  %al, %eax   # so eax=0x00000012
orl     %edx, %eax  # eax=0x00000012 | 0x00000000 = 0x00000012
movl    %eax, G(%rip)   # write to G

我们可以看到编译器将使用移位指令来确保您所说的内容。(注意:这里G的内存布局如下:)
----------------------------------
|     foo2-24bit     | foo1-8bit |
----------------------------------

当然,上述的结果是:
G.foo1=0x12, G.foo2=0xffffff, G=0xffffff12
G.foo1=0x12, G.foo2=0x000000, G=0x00000012
G.foo1=0x00, G.foo2=0x000000, G=0x00000000

0

简短回答:是的,您可以相信模 2^n 的结果。

在您的程序中, G.foo++; 实际上等同于 G.foo = (unsigned int)G.foo + 1

无符号整数运算总是产生 2^(无符号整数位数) 的结果。最低位的两个比特位然后存储在 G.foo 中,产生零。


你的等价性是错误的。uint8_t 会被提升为 int,而不是 unsigned intG.foo++; 等同于 G.foo = ((int)G.foo + 1) % 4; - R.. GitHub STOP HELPING ICE
@R.. 你说得对(我总是搞错这些晋升),但是亚当上面的答案中缺少一些东西,那就是+1实际上是在有符号整数之间进行的。 - Pascal Cuoq

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