我可以相信 C 编译器每次访问位字段时都会模 2^n 吗?或者是否有任何编译器/优化,其中像下面这样的代码不会打印出溢出?
struct {
uint8_t foo:2;
} G;
G.foo = 3;
G.foo++;
if(G.foo == 0) {
printf("Overflow\n");
}
提前致谢, 弗洛里安
我可以相信 C 编译器每次访问位字段时都会模 2^n 吗?或者是否有任何编译器/优化,其中像下面这样的代码不会打印出溢出?
struct {
uint8_t foo:2;
} G;
G.foo = 3;
G.foo++;
if(G.foo == 0) {
printf("Overflow\n");
}
提前致谢, 弗洛里安
是的,你可以相信 C 编译器在这里做正确的事情,只要位字段声明为无符号类型,你有uint8_t
. 根据 C99 标准 §6.2.6.1/3:
存储在无符号位域和无符号字符类型对象中的值应使用纯二进制表示法表示。40)
从§6.7.2.1/9:
位域被解释为由指定位数组成的有符号或无符号整数类型。104)如果值 0 或 1 存储到类型为 的非零宽度位域中
_Bool
,则该位域的值应与存储的值进行比较。
从§6.2.5/9(强调我的):
有符号整数类型的非负值范围是对应无符号整数类型的子范围,相同值在每种类型中的表示是相同的。31) 涉及无符号操作数的计算永远不会溢出,因为不能由结果无符号整数类型表示的结果会以比结果类型可以表示的最大值大一的数字为模减少。
所以是的,您可以确定任何符合标准的编译器都会G.foo
溢出到 0 而没有任何其他不需要的副作用。
不会。编译器为该字段分配 2 位,递增 3 会导致 100b,而将其放在两位中会导致 0。
是的。我们可以从汇编中得到答案。这是我在 Ubuntu 16.04、64bit、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 file>
。你可以得到汇编文件.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
简短的回答:是的,您可以相信模 2^n 会发生。
在你的程序中,
G.foo++;
其实等价于G.foo = (unsigned int)G.foo + 1
.
Unsigned int 算术总是产生 2^(size of unsigned int in bits) 结果。然后将最小权重的两位存储在 中G.foo
,产生零。