为什么这些构造使用增量前和增量后未定义的行为?
-
09-09-2019 - |
题
#include <stdio.h>
int main(void)
{
int i = 0;
i = i++ + ++i;
printf("%d\n", i); // 3
i = 1;
i = (i++);
printf("%d\n", i); // 2 Should be 1, no ?
volatile int u = 0;
u = u++ + ++u;
printf("%d\n", u); // 1
u = 1;
u = (u++);
printf("%d\n", u); // 2 Should also be one, no ?
register int v = 0;
v = v++ + ++v;
printf("%d\n", v); // 3 (Should be the same as u ?)
int w = 0;
printf("%d %d\n", ++w, w); // shouldn't this print 1 1
int x[2] = { 5, 8 }, y = 0;
x[y] = y ++;
printf("%d %d\n", x[0], x[1]); // shouldn't this print 0 8? or 5 0?
}
解决方案
C具有未定义行为的概念,即,一些语言构造是语法有效但运行代码时,你不能预测的行为。
据我所知,标准并没有明确说的为什么的未定义行为的概念存在。在我看来,这只是因为语言的设计者希望那里是在语义一些回旋余地,而不是即要求所有实现处理整数溢出的方式不尽相同,这将非常有可能实行严重的性能成本,他们刚刚离开的行为未定义因此,如果你写导致整数溢出代码,任何事情都有可能发生。
因此,考虑到这一点,为什么这些“问题”?语言清楚地说,有些事情导致未定义行为。这是没有问题,也没有“应该”参与。如果在所涉及的变量中的一个声明volatile
未定义行为的变化,这并不证明或改变任何东西。这是未定义;你不能推理行为。
您最有趣的外观的示例中,一个与
u = (u++);
时(参见维基百科上关于序列点)未定义行为的一个教科书的例子。
其他提示
只是编译和反汇编的代码行,如果你愿意的话就知道它究竟是如何得到你做了什么。
这是我得到我的机器上,用什么,我想是怎么回事在一起:
$ cat evil.c
void evil(){
int i = 0;
i+= i++ + ++i;
}
$ gcc evil.c -c -o evil.bin
$ gdb evil.bin
(gdb) disassemble evil
Dump of assembler code for function evil:
0x00000000 <+0>: push %ebp
0x00000001 <+1>: mov %esp,%ebp
0x00000003 <+3>: sub $0x10,%esp
0x00000006 <+6>: movl $0x0,-0x4(%ebp) // i = 0 i = 0
0x0000000d <+13>: addl $0x1,-0x4(%ebp) // i++ i = 1
0x00000011 <+17>: mov -0x4(%ebp),%eax // j = i i = 1 j = 1
0x00000014 <+20>: add %eax,%eax // j += j i = 1 j = 2
0x00000016 <+22>: add %eax,-0x4(%ebp) // i += j i = 3
0x00000019 <+25>: addl $0x1,-0x4(%ebp) // i++ i = 4
0x0000001d <+29>: leave
0x0000001e <+30>: ret
End of assembler dump.
(我......假设0x00000014指令是某种编译器优化的?)
我认为C99标准的相关部分6.5表达式,§2
之前和下一序列点的对象应具有其存储的值之间的 由表达式的评估最多一次修改。此外,现有值 应仅被理解为确定要被存储的值。
和6.5.16赋值运算符,§4:
操作数的评价的顺序是不确定的。如果试图修改 赋值运算符或结果到下一个顺序点之后访问它,所述 行为是不确定的。
该行为无法真正解释,因为它同时调用了 未指定的行为 和 未定义的行为, ,所以我们不能对这段代码做出任何一般性的预测,尽管如果你读过 奥尔夫·莫达尔的 工作如 深C 和 未指定和未定义 有时,您可以使用特定的编译器和环境在非常特定的情况下做出很好的猜测,但请不要在生产附近的任何地方这样做。
所以继续 未指定的行为, , 在 c99标准草案 部分6.5
段落 3 说(强调我的):
运算符和操作数的分组由语法指示74),除了稍后指定(对于function-call(),&& &&,||,?:和comma operators),,),, 子表达式的求值顺序和副作用发生的顺序均未指定。
所以当我们有这样一行时:
i = i++ + ++i;
我们不知道是否 i++
或者 ++i
将首先进行评估。这主要是为了给编译器 更好的优化选项.
我们还有 未定义的行为 由于程序正在修改变量(i
, u
, 等..)之间不止一次 序列点. 。来自标准草案部分 6.5
段落 2(强调我的):
在上一个序列和下一个序列之间,一个对象应最多修改其存储值 通过表达式的求值。此外, 先前的值应仅读取以确定要存储的值.
它引用了以下未定义的代码示例:
i = ++i + 1;
a[i++] = i;
在所有这些示例中,代码尝试在同一序列点多次修改对象,这将以 ;
在每一种情况下:
i = i++ + ++i;
^ ^ ^
i = (i++);
^ ^
u = u++ + ++u;
^ ^ ^
u = (u++);
^ ^
v = v++ + ++v;
^ ^ ^
未指定的行为 定义在 c99标准草案 在部分 3.4.4
作为:
使用未指定的价值或该国际标准提供两种或多种可能性的其他行为,并且在任何情况下都不会选择其他要求
和 未定义的行为 定义在节中 3.4.3
作为:
行为,使用了不可存储或错误的程序结构或错误的数据,该国际标准对此没有任何要求
并指出:
可能的未定义行为包括完全忽略具有不可预测结果的情况,到在翻译或程序执行期间以环境特征的记录方式表现(无论是否发出诊断消息),到终止翻译或执行(发出诊断消息)。
这里的大多数答案都引用了 C 标准,强调这些构造的行为是未定义的。要了解 为什么这些构造的行为是未定义的, ,我们先根据C11标准来理解这些术语:
排序: (5.1.2.3)
给定任意两个评估
A
和B
, , 如果A
之前已测序B
, ,然后执行A
应先于执行B
.
未排序:
如果
A
未在之前或之后排序B
, , 然后A
和B
是未排序的。
评估可以是以下两种情况之一:
- 价值计算, ,计算出表达式的结果;和
- 副作用, ,这是对象的修改。
序列点:
表达式求值之间存在序列点
A
和B
意味着每一个 价值计算 和 副作用 有关联A
在每个之前排序 价值计算 和 副作用 有关联B
.
现在来回答这个问题,对于像这样的表达式
int i = 1;
i = i++;
标准说:
6.5 表达式:
如果标量对象的副作用相对于 任何一个 对同一标量对象的不同副作用 或使用同一标量对象的值进行值计算, 行为未定义. [...]
因此,上面的表达式调用 UB,因为同一个对象有两个副作用 i
相对于彼此是无序的。这意味着分配给副作用是否没有排序 i
将在副作用之前或之后完成 ++
.
根据赋值发生在增量之前还是之后,将会产生不同的结果,这就是其中一种情况 未定义的行为.
让我们重命名 i
在作业的左边是 il
并在赋值的右侧(在表达式中 i++
) 是 ir
, ,那么表达式就像
il = ir++ // Note that suffix l and r are used for the sake of clarity.
// Both il and ir represents the same object.
重要的一点 关于 Postfix ++
运算符是:
只是因为
++
出现在变量之后并不意味着增量发生较晚. 。增量可以在编译器喜欢的时候尽早发生 只要编译器确保使用原始值.
这意味着表达 il = ir++
可以评估为
temp = ir; // i = 1
ir = ir + 1; // i = 2 side effect by ++ before assignment
il = temp; // i = 1 result is 1
或者
temp = ir; // i = 1
il = temp; // i = 1 side effect by assignment before ++
ir = ir + 1; // i = 2 result is 2
导致两个不同的结果 1
和 2
这取决于分配和副作用的顺序 ++
因此调用 UB。
回答这个问题的另一种方法是简单地问: 它们是什么意思? 程序员想做什么?
第一个片段询问的是, i = i++ + ++i
, ,在我的书中显然是疯狂的。没有人会在真实的程序中编写它,它的作用并不明显,没有任何人可以想象的算法可以尝试编码来导致这种特定的人为操作序列。由于你我都不清楚它应该做什么,所以在我的书中,如果编译器无法弄清楚它应该做什么也没关系。
第二个片段, i = i++
, ,比较容易理解一些。显然有人试图增加 i,并将结果分配回 i。但在 C 中有几种方法可以做到这一点。将 i 加 1 并将结果赋回 i 的最基本方法在几乎所有编程语言中都是相同的:
i = i + 1
当然,C 有一个方便的快捷方式:
i++
这意味着“i 加 1,并将结果赋回 i”。因此,如果我们构建两者的大杂烩,通过编写
i = i++
我们真正想说的是“将 1 加到 i,并将结果分配回 i,然后将结果分配回 i”。我们很困惑,所以如果编译器也很困惑,我也不会太困扰。
实际上,只有当人们将它们用作 ++ 应该如何工作的人为示例时,才会编写这些疯狂的表达式。当然,了解 ++ 的工作原理也很重要。但使用 ++ 的一条实用规则是:“如果使用 ++ 的表达式的含义不明显,则不要编写它。”
我们曾经在 comp.lang.c 上花费无数时间讨论诸如此类的表达式 为什么 它们是未定义的。我的两个较长的答案,试图真正解释原因,已存档在网络上:
通常这个问题被链接为与代码相关的问题的重复项,例如
printf("%d %d\n", i, i++);
或者
printf("%d %d\n", ++i, i++);
或类似的变体。
虽然这也是 未定义的行为 如前所述,当 printf()
与以下语句进行比较时涉及:
x = i++ + i++;
在以下声明中:
printf("%d %d\n", ++i, i++);
这 评估顺序 中的参数 printf()
是 未指定. 。这意味着,表达式 i++
和 ++i
可以按任何顺序进行评估。 C11标准 对此有一些相关描述:
附件 J,未明确的行为
参数中函数指定者,参数和子表达的顺序在函数调用(6.5.2.2)中评估。
3.4.4、未指定行为
使用未指定的价值或该国际标准提供两种或多种可能性的其他行为,并且在任何情况下都不会对哪些可能选择。
示例未指定行为的一个示例是评估函数参数的顺序。
这 未指定的行为 本身不是问题。考虑这个例子:
printf("%d %d\n", ++x, y++);
这也有 未指定的行为 因为评估的顺序 ++x
和 y++
未指定。但这是完全合法有效的声明。有 不 该语句中未定义的行为。因为修改(++x
和 y++
)已完成 清楚的 对象。
是什么导致了下面的陈述
printf("%d %d\n", ++i, i++);
作为 未定义的行为 事实上,这两个表达式修改了 相同的 目的 i
无需干预 序列点.
另一个细节是 逗号 printf() 调用涉及的是 分隔器, ,不是 逗号运算符.
这是一个重要的区别,因为 逗号运算符 确实介绍了一个 序列点 在其操作数的求值之间,这使得以下内容合法:
int i = 5;
int j;
j = (++i, i++); // No undefined behaviour here because the comma operator
// introduces a sequence point between '++i' and 'i++'
printf("i=%d j=%d\n",i, j); // prints: i=7 j=6
逗号运算符从左到右计算其操作数,并仅生成最后一个操作数的值。所以在 j = (++i, i++);
, ++i
增量 i
到 6
和 i++
产生旧值 i
(6
) 被分配给 j
. 。然后 i
变成 7
由于后增量。
所以如果 逗号 在函数调用中是逗号运算符 then
printf("%d %d\n", ++i, i++);
不会有问题。但它调用了 未定义的行为 因为 逗号 这里有一个 分隔器.
对于那些刚接触的人 未定义的行为 会从阅读中受益 每个 C 程序员都应该了解关于未定义行为的知识 理解 C 中未定义行为的概念和许多其他变体。
这个帖子: 未定义、未指定和实现定义的行为 也相关。
尽管这是不可能的任何编译器和处理器实际上这样做,那将是合法的,C标准下,为使编译器实现的“i ++”与序列:
In a single operation, read `i` and lock it to prevent access until further notice
Compute (1+read_value)
In a single operation, unlock `i` and store the computed value
虽然我不认为任何处理器支持硬件允许这样的事情要高效地完成,人们可以很容易想象的情况下这样的行为将使多线程代码更容易(例如,它会保证如果两个线程尝试执行上面的顺序同时,i
将由二送递增),它不是完全不可想象的,一些未来的处理器可能会提供一个功能类似的东西。
如果编译器是写i++
如上所示(标准所规定的法律),并在整个总体表达(也法律)的评价穿插上述指令,并且如果它没有发生注意到之一碰巧访问i
其它指令,这将是可能的(合法的)的编译器生成的,将死锁指令序列。可以肯定,编译器几乎肯定会发现在相同的变量i
在两地使用的情况下,问题的,但如果一个例程接受两个指针p
和q
参考,并使用(*p)
和(*q)
在上述表达式(而比使用i
两次),编译器将不需要识别或避免如果相同的对象的地址被传递两者p
和q
了不会发生死锁。
C标准说,一个变量应该只在两个序列点之间至多分配一次。分号例如是一个顺序点。点击 所以每个以下形式的语句:点击
i = i++;
i = i++ + ++i;
等违反该规则。该标准还指出,行为是不确定的,而不是不确定的。一些编译器不检测这些和产生一些结果,但这不是每标准。结果
然而,两个不同的变量可以在两个序列点之间递增。结果
while(*src++ = *dst++);
上面的是一种常见的编码实践而复制/分析字符串。
虽然 句法 像这样的表达式 a = a++
或者 a++ + a++
是合法的,则 行为 这些构造是 不明确的 因为一个 将 在C标准中不被遵守。 C99 6.5p2:
- 在上一个和下一个序列点之间,对象的存储值最多应通过表达式的求值修改一次。[72]此外,应只读先前值以确定要存储的值[73]
和 脚注73 进一步澄清
本段呈现未定义的语句表达式,例如
i = ++i + 1; a[i++] = i;
同时允许
i = i + 1; a[i] = i;
以下是 5.1.2.3 中描述的顺序点:
- 在函数调用和实际调用中函数指示符和实际参数的计算之间。(6.5.2.2)。
- 在以下运算符的第一个和第二个操作数的计算之间:逻辑与 && (6.5.13);逻辑或|| (6.5.14);逗号 , (6.5.17)。
- 在条件 ? 的第一个操作数的计算之间:运算符以及第二个和第三个操作数中的任一个被求值 (6.5.15)。
- 完整声明符的结尾:声明者(6.7.6);
- 在完整表达式的计算和下一个要计算的完整表达式之间。以下为完整表达:不属于复合文字一部分的初始值设定项 (6.7.9);表达式语句中的表达式 (6.8.3);选择语句的控制表达式(if 或 switch)(6.8.4);while 或 do 语句的控制表达式 (6.8.5);for 语句的每个(可选)表达式 (6.8.5.3);return 语句中的(可选)表达式 (6.8.6.4)。
- 紧接着库函数返回之前 (7.1.4)。
- 在与每个格式化输入/输出函数转换说明符(7.21.6、7.29.2)关联的操作之后。
- 在每次调用比较函数之前和之后,以及在对比较函数的任何调用和作为参数传递给该调用的对象的任何移动之间(7.22.5)。
措辞相同 C11 中的段落 是:
- 如果标量对象上的副作用相对于同一标量对象上的不同副作用或使用同一标量对象的值的值计算是无序的,则行为是未定义的。如果表达式的子表达式有多个允许的排序,则如果任何排序中出现此类未排序的副作用,则行为未定义。84)
您可以通过使用最新版本的 GCC 来检测程序中的此类错误 -Wall
和 -Werror
, ,然后 GCC 将彻底拒绝编译你的程序。以下是 gcc (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005 的输出:
% gcc plusplus.c -Wall -Werror -pedantic
plusplus.c: In function ‘main’:
plusplus.c:6:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
i = i++ + ++i;
~~^~~~~~~~~~~
plusplus.c:6:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
plusplus.c:10:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
i = (i++);
~~^~~~~~~
plusplus.c:14:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
u = u++ + ++u;
~~^~~~~~~~~~~
plusplus.c:14:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
plusplus.c:18:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
u = (u++);
~~^~~~~~~
plusplus.c:22:6: error: operation on ‘v’ may be undefined [-Werror=sequence-point]
v = v++ + ++v;
~~^~~~~~~~~~~
plusplus.c:22:6: error: operation on ‘v’ may be undefined [-Werror=sequence-point]
cc1: all warnings being treated as errors
重要的是要知道 什么是序列点——以及 什么是 序列点和什么 不是. 。例如 逗号运算符 是一个序列点,所以
j = (i ++, ++ i);
是明确定义的,并且会递增 i
加一,产生旧值,丢弃该值;然后在逗号运算符处解决副作用;然后递增 i
加一,结果值成为表达式的值 - 即这只是一种人为的写作方式 j = (i += 2)
这又是一种“聪明”的写作方式
i += 2;
j = i;
但是,那 ,
在函数参数列表中是 不是 逗号运算符,并且不同参数的计算之间没有序列点;相反,他们的评估彼此之间没有顺序;所以函数调用
int i = 0;
printf("%d %d\n", i++, ++i, i);
有 未定义的行为 因为 评估之间没有序列点 i++
和 ++i
在函数参数中, ,以及值 i
因此,双方都修改了两次 i++
和 ++i
, ,在上一个序列点和下一个序列点之间。
在 https://stackoverflow.com/questions/29505280/incrementing-array-index-在-C 有人问起类似的语句:
int k[] = {0,1,2,3,4,5,6,7,8,9,10};
int i = 0;
int num;
num = k[++i+k[++i]] + k[++i];
printf("%d", num);
它打印7 ...的OP预期它打印6。
在++i
增量不计算的其余部分之前保证所有完整。事实上,不同的编译器将在这里得到不同的结果。在你所提供的示例中,第一2 ++i
执行,则k[]
的值读,则最后++i
然后k[]
。
num = k[i+1]+k[i+2] + k[i+3];
i += 3
现代编译器将优化这很好。事实上,可能比您最初写的代码更好的(假设它工作过你所希望的方式)。
您的问题可能不是,“为什么这些结构用C未定义行为?”。你的问题可能是,“为什么这个代码(使用++
)不给我我所期望的价值?”,有人标记为重复你的问题,并把您带到这里。
此的答案试图回答这个问题:为什么你的代码不能给你你所期望的答案,你怎么能学会识别(避免)的预期将无法正常工作表达式<。 / p>
我假设你现在听到的C'S ++
和--
运营商的基本定义,并且前缀形式++x
从后缀形式x++
的区别。但这些运营商很难去想,所以要确保你明白,或许你写的涉及类似
int x = 5;
printf("%d %d %d\n", x, ++x, x++);
不过,给您惊喜,这个程序做的不的帮助你理解 - 它印一些奇怪的,意外的,莫名的输出,这表明可能++
做完全不同的事情,一点都没有你的想法它没有。
或者,也许你正在寻找一个很难理解的,如
表达int x = 5;
x = x++ + ++x;
printf("%d\n", x);
也许有人给你的代码作为一个谜。此代码还没有任何意义,特别是如果你运行它 - 如果你编译并在两个不同的编译器中运行它,你可能会得到两个不同的答案!那是怎么回事?哪个答案是正确的? (答案是两者都是,或两者都不是。)
就像你现在听到的,所有这些表述都的未定义的,这意味着C语言不作任何他们会做什么样的保证。这是一个奇怪而令人惊讶的结果,因为你可能认为任何方案,你可以写,只要它编译和运行,将生成一个唯一的,定义明确的输出。但在不确定的行为的情况下,这并非如此。
什么使表达式未定义?被涉及++
和--
表达式总是不确定的?当然不是。这些都是有用的运营商,如果你正确地使用它们,他们非常明确的。
有关我们谈论的是什么使他们不确定的是,当有太多的事情一次,当我们不知道为了什么事情会发生在,但是当订单事宜,结果我们得到的。表达
让我们回到我在这个答案中使用的两个例子。当我写
printf("%d %d %d\n", x, ++x, x++);
的问题是,调用printf
之前,该编译器计算x
的第一值,或x++
,或也许++x
?但事实证明的我们不知道的。有没有在C中没有规则,它说,该参数的函数得到评估左到右,或者从右到左,或者以其他的顺序。所以,我们不能说编译器是否会做x
,再++x
,然后x++
,或x++
然后++x
然后x
,或一些其他命令。但为了清楚地重要,因为这取决于订购编译器使用,我们会清楚地得到不同的结果通过printf
打印。
什么这个疯狂表达?
x = x++ + ++x;
这个表达式的问题是,它包含三个不同的尝试修改x的值:(1)x++
部分试图1添加至x,存储在x
新的值,并返回x
的旧值; (2)部分++x
试图添加1至x,存储在x
新的值,并返回x
的新值;和(3)的部分x =
试图分配其他两个回x的总和。其中这三个试图转让土地将“赢”?这三个值中的真正能拿到assigned到x
?再次,也许令人惊讶的,有一个在C中没有规则告诉我们。
您可以想象,优先级或关联或左到右的评价告诉你什么样的顺序的事情发生了,但他们没有。你可能不相信我,但请把我的话,我再说一遍:优先级和结合并不确定C.特别表达式的计算顺序的每一个方面,如果一个表达式中有多个不同的地方,我们尝试了新的值赋给像x
,优先级和结合做的不的告诉我们,这一次的尝试都首先发生,或最后,或任何东西。
因此,所有这样的背景和介绍的方式进行,如果要确保所有的程序都明确定义,你可以写一个表达式,哪些你能不能写?
这些表达式所有细:
y = x++;
z = x++ + y++;
x = x + 1;
x = a[i++];
x = a[i++] + b[j++];
x[i++] = a[j++] + b[k++];
x = *p++;
x = *p++ + *q++;
这些表达方式的所有未定义:
x = x++;
x = x++ + ++x;
y = x + x++;
a[i] = i++;
a[i++] = i;
printf("%d %d %d\n", x, ++x, x++);
和最后一个问题是,你怎么能知道哪些表达方式明确界定,并表述是不确定的?
正如我前面所说的,不确定的表情,其中有太多的那些多去一次,在那里你不能确定什么样的顺序事情发生在,并且订单事项:
- 如果有一个变量,它是越来越修改在两个或多个不同的地方(分配),你怎么知道哪个修改首先发生?
- 如果有多数民众赞成在一处得到修改,在另一个地方有使用其值的变量,你怎么知道它是否使用旧值或新的价值? 醇>
如在表达的#1,例如
x = x++ + ++x;
有三个尝试修改`X
如在表达的#2,例如
y = x + x++;
我们都使用x
的值,并对其进行修改。
所以这就是答案:请确保你写的任何表情,每一个变量被修改最多一次,如果一个变量被修改,你不也尝试使用该变量其他地方的值。 p>
文档中提供了有关此类计算中发生的情况的良好解释 n1188 从 ISO W14 站点.
我解释一下这些想法。
适用于这种情况的 ISO 9899 标准的主要规则是 6.5p2。
在上一个和下一个序列点之间,对象的存储值最多应通过表达式的求值修改一次。此外,应只读先前值以确定要存储的值。
表达式中的序列点如 i=i++
之前 i=
之后 i++
.
在我上面引用的论文中,解释说您可以将程序理解为由小盒子组成,每个盒子包含两个连续序列点之间的指令。序列点在标准附录 C 中定义,在以下情况下 i=i++
有 2 个序列点来界定完整表达式。这样的表达式在语法上等同于条目 expression-statement
语法的 Backus-Naur 形式(标准附录 A 中提供了语法)。
因此盒子内的说明顺序没有明确的顺序。
i=i++
可以解释为
tmp = i
i=i+1
i = tmp
或作为
tmp = i
i = tmp
i=i+1
因为所有这些形式都可以解释代码 i=i++
是有效的,并且因为两者生成不同的答案,所以行为是未定义的。
因此,可以通过组成程序的每个框的开头和结尾看到序列点[这些框是C中的原子单元],并且框内的指令顺序在所有情况下都没有定义。改变这个顺序有时会改变结果。
编辑:
原因是程序正在运行未定义的行为。问题在于求值顺序,因为根据 C++98 标准不需要序列点(根据 C++11 术语,没有操作在另一个操作之前或之后排序)。
但是,如果您坚持使用一种编译器,您会发现该行为是持久的,只要您不添加函数调用或指针,这会使行为更加混乱。
首先是海湾合作委员会:使用 努文明威 15 GCC 7.1 您将获得:
#include<stdio.h> int main(int argc, char ** argv) { int i = 0; i = i++ + ++i; printf("%d\n", i); // 2 i = 1; i = (i++); printf("%d\n", i); //1 volatile int u = 0; u = u++ + ++u; printf("%d\n", u); // 2 u = 1; u = (u++); printf("%d\n", u); //1 register int v = 0; v = v++ + ++v; printf("%d\n", v); //2
}
海湾合作委员会如何运作?它按从左到右的顺序计算右侧 (RHS) 的子表达式,然后将值分配给左侧 (LHS) 。这正是 Java 和 C# 的行为方式和定义其标准的方式。(是的,Java 和 C# 中的等效软件已定义行为)。它按照从左到右的顺序逐一评估 RHS 语句中的每个子表达式;对于每个子表达式:首先评估 ++c(前增量),然后使用值 c 进行操作,然后是后增量 c++)。
在GCC C ++中,操作员的优先级控制评估单个操作员的顺序
GCC 理解的定义行为 C++ 中的等效代码:
#include<stdio.h>
int main(int argc, char ** argv)
{
int i = 0;
//i = i++ + ++i;
int r;
r=i;
i++;
++i;
r+=i;
i=r;
printf("%d\n", i); // 2
i = 1;
//i = (i++);
r=i;
i++;
i=r;
printf("%d\n", i); // 1
volatile int u = 0;
//u = u++ + ++u;
r=u;
u++;
++u;
r+=u;
u=r;
printf("%d\n", u); // 2
u = 1;
//u = (u++);
r=u;
u++;
u=r;
printf("%d\n", u); // 1
register int v = 0;
//v = v++ + ++v;
r=v;
v++;
++v;
r+=v;
v=r;
printf("%d\n", v); //2
}
然后我们去 视觉工作室. 。Visual Studio 2015,您将获得:
#include<stdio.h>
int main(int argc, char ** argv)
{
int i = 0;
i = i++ + ++i;
printf("%d\n", i); // 3
i = 1;
i = (i++);
printf("%d\n", i); // 2
volatile int u = 0;
u = u++ + ++u;
printf("%d\n", u); // 3
u = 1;
u = (u++);
printf("%d\n", u); // 2
register int v = 0;
v = v++ + ++v;
printf("%d\n", v); // 3
}
Visual Studio是如何工作的,它采用另一种方法,它在第一遍中评估所有预增量表达式,然后在第二遍中的操作中使用变量值,在第三遍中从RHS分配到LHS,然后在最后一遍评估所有一次性完成后递增表达式。
因此,Visual C++ 所理解的定义行为 C++ 中的等价物:
#include<stdio.h>
int main(int argc, char ** argv)
{
int r;
int i = 0;
//i = i++ + ++i;
++i;
r = i + i;
i = r;
i++;
printf("%d\n", i); // 3
i = 1;
//i = (i++);
r = i;
i = r;
i++;
printf("%d\n", i); // 2
volatile int u = 0;
//u = u++ + ++u;
++u;
r = u + u;
u = r;
u++;
printf("%d\n", u); // 3
u = 1;
//u = (u++);
r = u;
u = r;
u++;
printf("%d\n", u); // 2
register int v = 0;
//v = v++ + ++v;
++v;
r = v + v;
v = r;
v++;
printf("%d\n", v); // 3
}
正如 Visual Studio 文档所述 评估的优先级和顺序:
当多个运算符一起出现时,它们具有相同的优先级,并根据它们的结合性进行评估。表中的运算符在以后缀运算符开头的部分中进行了描述。