ale*_*why 4 c boolean increment undefined-behavior decrement
这个问题是关于\xd0\xa1的。假设我们有这样的代码:
\nbool a = false;\na++;\nprintf("%d\\n", a);\na--;\nprintf("%d\\n", a);\nRun Code Online (Sandbox Code Playgroud)\n在我的 x86-64 linux 机器上显示:
\n1\n0\nRun Code Online (Sandbox Code Playgroud)\n这对我来说并不意外。这段代码:
\nbool a = false;\na++; a++;\nprintf("%d\\n", a);\na--; a--;\nprintf("%d\\n", a);\nRun Code Online (Sandbox Code Playgroud)\n有点令人惊讶,因为它打印了:
\n1\n1\nRun Code Online (Sandbox Code Playgroud)\n这在其他一些架构上是一致的(我检查了x86和arm7)。
\nC 标准规定 e++ 或 e-- 应分别视为 e+=1 或 e-=1。事实上,如果我们替换 a++; 其中a += 1;和一个——;其中a-= 1;输出保持不变。
\n我查看了 x86-64 的程序集。gcc 使用“xor”指令进行减量:
\n b--; b--;\n 11e6: 80 75 ff 01 xor BYTE PTR [rbp-0x1],0x1\n 11ea: 80 75 ff 01 xor BYTE PTR [rbp-0x1],0x1\n printf("%d\\n", b);\n 11ee: 0f b6 45 ff movzx eax,BYTE PTR [rbp-0x1]\n 11f2: 89 c6 mov esi,eax\n 11f4: 48 8d 05 19 0e 00 00 lea rax,[rip+0xe19] # 2014 <_IO_stdin_used+0x14>\n 11fb: 48 89 c7 mov rdi,rax\n 11fe: b8 00 00 00 00 mov eax,0x0\n 1203: e8 68 fe ff ff call 1070 <printf@plt>\nRun Code Online (Sandbox Code Playgroud)\n而 clang 更喜欢使用“add”(!)和“and”来减少:
\n 11c9: 04 01 add al,0x1\n 11cb: 24 01 and al,0x1\n 11cd: 88 45 fb mov BYTE PTR [rbp-0x5],al\n 11d0: 8a 45 fb mov al,BYTE PTR [rbp-0x5]\n 11d3: 24 01 and al,0x1\n 11d5: 0f b6 f0 movzx esi,al\n 11d8: 48 8d 3d 36 0e 00 00 lea rdi,[rip+0xe36] # 2015 <_IO_stdin_used+0x15>\n 11df: b0 00 mov al,0x0\n 11e1: e8 4a fe ff ff call 1030 <printf@plt>\nRun Code Online (Sandbox Code Playgroud)\n但结果是一样的。如果我理解正确的话,这些只是翻转最低有效位的不同方法。
\n据我所知,没有一本教科书显示过这样的例子,所以我认为这不是广为人知的事实。也许这是我自己的无知,但我已经用 C 编程了一段时间,直到现在才知道这种奇怪的行为。
\n完整源码在这里。
\n我的问题:
\nbool变量的减量是根据C标准定义的吗?或者它是未定义的(或者可能是实现定义的)行为?
\n如果定义了布尔值的递增和递减,为什么 gcc 在给定 -Wall 标志时显示有关 a++ 和 a-- 的警告?
\n\xd0\xa1 bool 变量的连续递减会将其值从 0 翻转到 1,然后再次翻转到 0,依此类推。这与增量的作用相反(它不翻转)。这是故意选择的、可移植的行为吗?
\n它是已定义的(因此是可移植的[1])。
\n\n\nC17 \xc2\xa76.5.2.4 \xc2\xb62 [...] 作为副作用,操作数对象的值会递增(即,将适当类型的值 1 添加到其中)。[...]
\n
\n\nC17 \xc2\xa76.5.2.4 \xc2\xb623 postfix-- 运算符类似于 postfix++ 运算符,只不过操作数的值递减(即从中减去相应类型的值 1) 。
\n
\n\nC17 \xc2\xa76.5.3.1 \xc2\xb62 [...] 表达式++E 等价于(E+=1)。[...]
\n
\n\nC17 \xc2\xa76.5.3.1 \xc2\xb63 前缀--运算符类似于前缀++运算符,只不过操作数的值递减。
\n
\n\nC17 \xc2\xa76.5.16.2 \xc2\xb63 E1 op = E2形式的复合赋值等效于简单赋值表达式 E1 = E1 op (E2),只不过左值 E1 仅计算一次 [.. .]
\n
(我可以继续证明加法执行整数提升,即true作为intis 1,作为falsean intis0等等。但你明白了。)
这就是我们观察到的行为。
\nbool a = false;\na++; # false\xe2\x87\x920, 0+1=1, 1\xe2\x87\x92true\nprintf("%d\\n", a); # true\xe2\x87\x921\na++; # true\xe2\x87\x921, 1+1=2, 2\xe2\x87\x92true\nprintf("%d\\n", a); # true\xe2\x87\x921\na--; # true\xe2\x87\x921, 1-1=0, 0\xe2\x87\x92false\nprintf("%d\\n", a); # false\xe2\x87\x920\na--; # false\xe2\x87\x920, 0-1=-1, -1\xe2\x87\x92true\nprintf("%d\\n", a); # true\xe2\x87\x921\nRun Code Online (Sandbox Code Playgroud)\n“\xe2\x87\x92”表示整数提升或隐式转换为bool.
它发出警告是因为这很奇怪。加法和减法不是布尔运算。并且还有更清晰的替代方案(至少对于前缀增量和后缀增量,或者如果您丢弃返回的值)。从上面的内容中,我们得出一些 bool 对象的以下等价物b:
++b相当于b = true.--b相当于b = !b.--b生成false,但众所周知,MSVC 实际上并不是 C 编译器。\n\n\n
\n- bool变量的减量是根据C标准定义的吗?或者它是未定义的(或者可能是实现定义的)行为?
\n
它被定义了。
\n就本答案而言,bool是 类型_Bool。(它是在 中的宏中定义的<stdbool.h>,但程序可以以不同的方式定义它。)
a++并a--在 C 2018 6.5.2.4 中进行了规定,其中第 2 段表示:
\n\n\xe2\x80\xa6 作为副作用,操作数对象的值会递增(即,将适当类型的值 1 添加到其中)。有关约束、类型和转换以及操作对指针\xe2\x80\xa6 的影响的信息,请参阅加法运算符和复合赋值的讨论
\n
第 3 段说 postfix--类似于 postfix ++。请注意对加法运算符的引用以获取有关转换的信息。加法运算符在 6.5.6 中指定,其中第 4 段表示:
\n\n如果两个操作数都具有算术类型,则对它们执行通常的算术转换。
\n
所以我们有 abool a和 \xe2\x80\x9c 的值 1 适当的类型。\xe2\x80\x9d \xe2\x80\x9c适当的类型\xe2\x80\x9d 没有正式定义,但我们可以假设它是bool或int,无论哪种方式,结果都是相同的。通常的算术转换对于许多读者来说都很熟悉,但它们在 6.3.1.8 中指定,主要在第 1 段中。通常的算术转换首先考虑浮点类型,但此处不适用。整数操作数的第一条规则是:
\n\n\xe2\x80\xa6 对两个操作数执行整数提升\xe2\x80\xa6
\n
整数提升在 6.3.1.1 中指定,第 2 段告诉我们 a boolorint操作数被转换为int。然后继续通常的算术转换规则:
\n\n\xe2\x80\xa6 如果两个操作数具有相同的类型,则不需要进一步转换。
\n
因此,在转换a为int和 1 后int,转换停止,并且 的增量a++计算为a转换为int加 1 转换为int,因此根据是a从 0 还是 1 开始,这会产生 1 或 2。
然后,正如上面 6.5.2.4 2 所说,我们关注复合赋值的讨论。这里的含义是a++;相当于a += 1;。C 2018 6.5.16.2 3 表示这相当于a = a + 1;. 我们已经弄清楚了a + 1,所以任务仍然a存在。这在 6.5.16.1 中有规定,其中第 2 段说:
\n\n\xe2\x80\xa6 右操作数的值被转换为赋值表达式的类型,并替换左操作数指定的对象中存储的值。
\n
因此,加法结果 1 或 2 被转换bool并存储在 中a。6.3.1.2 告诉我们有关转换为bool:
\n\n当任意标量值转换为 时
\n_Bool,如果该值比较等于 0,则结果为 0;否则,结果为 1。
因此,将 1 或 2 转换为bool1。因此,a++;被完全定义并将 1 存储在 中a。
\n\n\n
\n- 如果定义了布尔值的递增和递减,为什么 gcc 在给定 -Wall 标志时显示有关 a++ 和 a-- 的警告?
\n
C 标准允许实现发出额外的诊断信息,常见的诊断类别是由 C 标准完全定义的代码,但程序员很少正常使用,因此它的使用可能表明拼写错误或其他错误。由于a++总是将 a 设置bool为 1,a = 1代码会更清晰、更常见,因此a++在某种程度上可能是错误而不是有意的代码,因此值得进行诊断,特别是在-Wall需要时。
同样,a--;也是不寻常的。如果意图是翻转 a bool,a = !a;则更常见且更熟悉。
\n\n\n
\n- \xd0\xa1 bool 变量的连续递减会将其值从 0 翻转到 1,然后再次翻转到 0,依此类推。这与增量的作用相反(它不翻转)。这是故意选择的、可移植的行为吗?
\n
从某种意义上说,C 的规则是经过深思熟虑的,委员会在过去几十年里已经反复仔细地考虑了这一点,并且这种行为是由上面讨论的规则产生的,并指出:
\na--将 转换bool为int. 然后我们从 0 或 1 开始,bool减去 1,得到一个int值 \xe2\x88\x921 或 0。然后int将其转换为bool,产生 1 或 0,并将其存储在 中a。
由于这是完全指定的并且严格符合 C 代码,因此它可以跨符合 C 标准的编译器移植。(我不断言任何 Microsoft 产品与 C 标准兼容。)
\n然而,我怀疑这些规则的设计目的是导致值a--;翻转bool。这更有可能是规则整体设计的结果。