这个问题是关于C的。假设我们有一个这样的代码:
bool a = false;
a++;
printf("%d\n", a);
a--;
printf("%d\n", a);
字符串
在我的x86-64 Linux机器上显示:
1
0
型
这对我来说并不意外。这段代码:
bool a = false;
a++; a++;
printf("%d\n", a);
a--; a--;
printf("%d\n", a);
型
这是一个惊喜,因为它打印:
1
1
型
这在其他一些架构上是一致的(我检查了x86和arm 7)。
C标准规定e或e--应分别视为e+=1或e-=1。如果我们把A替换掉,其中a += 1;和a-;其中a-= 1;输出保持不变。
我看了x86-64的汇编。gcc使用“异或”指令进行递减:
b--; b--;
11e6: 80 75 ff 01 xor BYTE PTR [rbp-0x1],0x1
11ea: 80 75 ff 01 xor BYTE PTR [rbp-0x1],0x1
printf("%d\n", b);
11ee: 0f b6 45 ff movzx eax,BYTE PTR [rbp-0x1]
11f2: 89 c6 mov esi,eax
11f4: 48 8d 05 19 0e 00 00 lea rax,[rip+0xe19] # 2014 <_IO_stdin_used+0x14>
11fb: 48 89 c7 mov rdi,rax
11fe: b8 00 00 00 00 mov eax,0x0
1203: e8 68 fe ff ff call 1070 <printf@plt>
型
而且clang更喜欢用‘add‘(!)和'and'表示减量:
11c9: 04 01 add al,0x1
11cb: 24 01 and al,0x1
11cd: 88 45 fb mov BYTE PTR [rbp-0x5],al
11d0: 8a 45 fb mov al,BYTE PTR [rbp-0x5]
11d3: 24 01 and al,0x1
11d5: 0f b6 f0 movzx esi,al
11d8: 48 8d 3d 36 0e 00 00 lea rdi,[rip+0xe36] # 2015 <_IO_stdin_used+0x15>
11df: b0 00 mov al,0x0
11e1: e8 4a fe ff ff call 1030 <printf@plt>
型
但结果是一样的。如果我理解正确的话,这些只是翻转最低有效位的不同方法。
我所知道的教科书中没有这样的例子,所以我认为这不是广为人知的事实。可能是我自己的无知,但我用C编程有一段时间了,直到现在才知道这个奇怪的[或不是?]行为。
源代码为here。
我的问题:
- bool变量的递减量是按照C标准定义的吗?或者它是未定义的(或者可能是实现定义的)行为?
1.如果定义了布尔值的递增和递减,为什么gcc在给出-Wall标志时显示关于++和--的警告? - bool变量的连续递减将其值从0翻转到1,再翻转到0,依此类推。这与增量的作用相反(它不会翻转)。它是故意选择和便携式的行为?
3条答案
按热度按时间vtwuwzda1#
它是定义。
bool
是无符号整数类型之一,它是整数类型,是实数类型,是算术类型。后缀递减运算符的一个约束是:后缀递增或递减运算符的操作数必须具有原子、限定或非限定的真实的或指针类型,并且必须是可修改的左值。
(C23 6.5.2.4/1)
任何指定可修改的
bool
的表达式都满足这一点,并且附带的语义描述了结果(但请参见下文)。bool
s也不例外。1.如果定义了布尔值的递增和递减,为什么gcc在给出-Wall标志时会显示关于++和--的警告?
因为对布尔值执行算术没有太大意义,而且因为这样的表达式可能无法实现您所期望的功能。虽然
bool
被分类为无符号整数类型,但它的行为与所有其他整数类型不同。1.连续递减bool变量会将其值从0翻转为1,然后再翻转为0,依此类推。这与增量的作用相反(它不翻转)。这是故意选择和便携的行为吗?
该行为与
bool
和C算术的行为规范一致。后缀递增和递减运算符的规范分别将对操作数存储值的影响定义为加1或减1,并且它们遵从加法运算符和复合赋值的规范以获得更多细节。我认为最合理的解释是
a++
对a
的存储值的副作用与表达式a = a + 1
的副作用相同,除了a
本身仅被求值一次。a--
对a
的存储值的副作用与表达式a = a - 1
的副作用相同,除了a
本身仅被求值一次。对于
bool a
,a + 1
的求值通过首先将a
转换为int
,然后将所得int
加1来进行。由于a
的类型为bool
,因此我们可以确信结果可以表示为int
。然后将该结果转换为bool
类型,对此有一个特殊的规则:当任何标量值被转换为
bool
时,如果该值为零(对于算术类型),则结果为false
[...];否则,结果为true
。(C23 6.3.1.2/1)
这将具有将
a + 1
的任一可能结果转换为true
的效果。另一方面,对于后递减,如果
a
最初是false
(0),通过中间的int
值-1,则将a - 1
转换为bool
会产生true
,但如果a
最初是true
(1),则会产生false
。总的来说,那么,规范在这一点上可能比它更清楚,但我确实认为您指定的行为定义得很好,这些特定操作的其他结果都不正确。* 然而 *,我不会仅仅依赖它们,因为在
bool
s上执行算术是令人困惑的,并且在风格上令人担忧。lokaqttq2#
它是定义的(因此是可移植的[1])。
C17 §6.5.2.4 ¶2 [...]作为一个副作用,操作数对象的值被递增(也就是说,适当类型的值1被添加到它)。[...]
C17 §6.5.2.4 ¶23后缀--操作符类似于后缀操作符,除了操作数的值被递减(即从它减去适当类型的值1)。
C17 §6.5.3.1 ¶2 [...]表达式E等价于(E+=1)。[...]
C17 §6.5.3.1 ¶3前缀--运算符类似于前缀++运算符,除了操作数的值被递减。
C17 §6.5.16.2 ¶3形式为E1 op= E2的 * 复合赋值 * 等价于简单赋值表达式E1 = E1 op(E2),除了左值E1只计算一次[...]
(我可以继续说明加法执行整数提升,
true
作为int
是1
,false
作为int
是0
,等等。但你明白了。)这就是我们观察到的行为。
字符串
⇒表示整数提升或隐式转换为
bool
。它发出警告是因为它很奇怪。加法和减法不是布尔运算。还有更清晰的替代方案(至少对于前缀增量和后缀增量,或者如果您放弃返回值)。从上面,我们推导出一些bool对象
b
的以下等价:++b
相当于b = true
。--b
等于b = !b
。1.它是可移植的,只要你有一个C编译器。@Weather Vane指出
--b
在MSVC中无条件地生成false
,但众所周知,MSVC实际上不是C编译器。jgovgodb3#
它是定义。
在这个答案中,
bool
是_Bool
类型。(在<stdbool.h>
的宏中,它是这样定义的,但程序可以用不同的方式定义它。)a++
和a--
在C 2018 6.5.2.4中进行了规定,其中第2段规定:...作为副作用,操作数对象的值将递增(即,相应类型的值1将添加到该对象)。有关约束、类型和转换以及指针操作效果的信息,请参见加法运算符和复合赋值的讨论...
第3段说后缀
--
类似于后缀++
。有关转换的信息,请注意对加法运算符的引用。加法运算符在6.5.6中有规定,其中第4段规定:如果两个操作数都具有算术类型,则对它们执行通常的算术转换。
因此,我们有一个
bool
a
和一个“适当类型”的值1。“适当类型”没有正式定义,但我们可以假设它是bool
或int
,结果将是相同的。* 通常的算术转换 * 对许多读者来说都很熟悉,但它们在www.example.com中有详细说明6.3.1.8,主要是在第1段中。通常的算术转换会从浮点型别的考量开始,而这些考量在这里并不适用。整型操作数的第一条规则是:...对两个操作数执行整数提升...
整数提升在www.example.com中指定6.3.1.1,第2段告诉我们
bool
或int
操作数被转换为int
。然后,通常的算术转换规则继续:...如果两个操作数的类型相同,则不需要进一步转换。
因此,在将
a
转换为int
并将1转换为int
之后,转换停止,并且a++
的增量计算为转换为int
的a
加上转换为int
的1,因此根据a
是从0还是从1开始,这将产生1或2。然后,正如6.5.2.4上面的www.example.com 2所述,我们来讨论复合赋值。这意味着
a++;
等价于a += 1;
. C 2018 6.5.16.2 3表示,这相当于a = a + 1;
。我们已经计算出了a + 1
,所以a
的赋值仍然存在。这一点在www.example.com中有具体规定6.5.16.1,其中第2款规定:...右操作数的值转换为赋值表达式的类型,并替换左操作数指定的对象中存储的值。
因此,加法的结果1或2被转换为
bool
,并存储在a
中。6.3.1.2告诉我们有关转换为bool
的信息:当任何标量值被转换为
_Bool
时,如果该值等于0,则结果为0;否则,结果为1。因此,将1或2转换为
bool
得到1。因此,a++;
是完全定义的,并在a
中存储1。1.如果定义了布尔值的递增和递减,为什么gcc在给出-Wall标志时会显示关于++和--的警告?
C标准允许实现发出额外的诊断,并且诊断的一个常见类别是完全由C标准定义但通常很少被程序员使用的代码,因此它的使用可能指示打字错误或其他错误。由于
a++
总是将bool
设置为1,因此a = 1
将是更清晰和更常见的代码,因此a++
在某种程度上可能是一个错误,而不是有意编写的代码,因此值得进行诊断,尤其是在请求-Wall
的情况下。同样,
a--;
也是不寻常的。如果您的意图是翻转bool
,则a = !a;
更为常见和熟悉。1.连续递减bool变量会将其值从0翻转为1,然后再翻转为0,依此类推。这与增量的作用相反(它不翻转)。这是故意选择和便携的行为吗?
它是故意的,在这个意义上,C的规则已经被委员会反复和仔细地考虑了几十年,行为产生于上述讨论的规则,注意:
a--
将bool
转换为int
。然后我们从bool
的0或1开始,减去1,得到int
的值-1或0。然后将int
转换为bool
,得到1或0,并将其存储在a
中。由于这是完全指定的,并且严格符合C代码,因此它可以在符合C标准的编译器之间移植。(我并不Assert任何Microsoft产品都与C标准兼容。)
但是,我怀疑这些规则的设计意图是使
a--;
翻转bool
值。这更有可能是规则总体设计的结果。