C语言 bool变量的递减量是否用С定义?

pkwftd7m  于 2023-08-03  发布在  其他
关注(0)|答案(3)|浏览(101)

这个问题是关于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

我的问题

  1. bool变量的递减量是按照C标准定义的吗?或者它是未定义的(或者可能是实现定义的)行为?
    1.如果定义了布尔值的递增和递减,为什么gcc在给出-Wall标志时显示关于++和--的警告?
  2. bool变量的连续递减将其值从0翻转到1,再翻转到0,依此类推。这与增量的作用相反(它不会翻转)。它是故意选择和便携式的行为?
vtwuwzda

vtwuwzda1#

  1. bool变量的减量是否根据C标准定义?或者它是未定义的(或者可能是实现定义的)行为?
    它是定义。
    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 aa + 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上执行算术是令人困惑的,并且在风格上令人担忧。

lokaqttq

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作为int1false作为int0,等等。但你明白了。)
这就是我们观察到的行为。

bool a = false;
a++;                    # false⇒0, 0+1=1,  1⇒true
printf("%d\n", a);      #                            true⇒1
a++;                    # true⇒1,  1+1=2,  2⇒true
printf("%d\n", a);      #                            true⇒1
a--;                    # true⇒1,  1-1=0,  0⇒false
printf("%d\n", a);      #                            false⇒0
a--;                    # false⇒0, 0-1=-1, -1⇒true
printf("%d\n", a);      #                            true⇒1

字符串
⇒表示整数提升或隐式转换为bool
它发出警告是因为它很奇怪。加法和减法不是布尔运算。还有更清晰的替代方案(至少对于前缀增量和后缀增量,或者如果您放弃返回值)。从上面,我们推导出一些bool对象b的以下等价:

  • ++b相当于b = true
  • --b等于b = !b

1.它是可移植的,只要你有一个C编译器。@Weather Vane指出--b在MSVC中无条件地生成false,但众所周知,MSVC实际上不是C编译器。

jgovgodb

jgovgodb3#

  1. bool变量的减量是否根据C标准定义?或者它是未定义的(或者可能是实现定义的)行为?
    它是定义。
    在这个答案中,bool_Bool类型。(在<stdbool.h>的宏中,它是这样定义的,但程序可以用不同的方式定义它。)
    a++a--在C 2018 6.5.2.4中进行了规定,其中第2段规定:
    ...作为副作用,操作数对象的值将递增(即,相应类型的值1将添加到该对象)。有关约束、类型和转换以及指针操作效果的信息,请参见加法运算符和复合赋值的讨论...
    第3段说后缀--类似于后缀++。有关转换的信息,请注意对加法运算符的引用。加法运算符在6.5.6中有规定,其中第4段规定:
    如果两个操作数都具有算术类型,则对它们执行通常的算术转换。
    因此,我们有一个boola和一个“适当类型”的值1。“适当类型”没有正式定义,但我们可以假设它是boolint,结果将是相同的。* 通常的算术转换 * 对许多读者来说都很熟悉,但它们在www.example.com中有详细说明6.3.1.8,主要是在第1段中。通常的算术转换会从浮点型别的考量开始,而这些考量在这里并不适用。整型操作数的第一条规则是:
    ...对两个操作数执行整数提升...
    整数提升在www.example.com中指定6.3.1.1,第2段告诉我们boolint操作数被转换为int。然后,通常的算术转换规则继续:
    ...如果两个操作数的类型相同,则不需要进一步转换。
    因此,在将a转换为int并将1转换为int之后,转换停止,并且a++的增量计算为转换为inta加上转换为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值。这更有可能是规则总体设计的结果。

相关问题