#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标准规定,一个变量最多只能在两个序列点之间赋值一次。例如,分号是一个序列点。 所以每个形式的表述
i = i++;
i = i++ + ++i;
等等都违反了这个规则。该标准还说行为是未定义的,而不是未指定的。一些编译器确实检测这些并产生一些结果,但这不是每个标准。
但是,两个不同的变量可以在两个序列点之间递增。
while(*src++ = *dst++);
以上是在复制/分析字符串时常见的编码实践。
其他回答
在https://stackoverflow.com/questions/29505280/incrementing-array-index-in-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++ 98标准不需要序列点(根据c++ 11术语,没有任何操作在另一个操作之前或之后排序)。
然而,如果你坚持使用一个编译器,你会发现这种行为是持久的,只要你不添加函数调用或指针,这将使行为更加混乱。
使用Nuwen MinGW 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
}
How does GCC work? it evaluates sub expressions at a left to right order for the right hand side (RHS) , then assigns the value to the left hand side (LHS) . This is exactly how Java and C# behave and define their standards. (Yes, the equivalent software in Java and C# has defined behaviors). It evaluate each sub expression one by one in the RHS Statement in a left to right order; for each sub expression: the ++c (pre-increment) is evaluated first then the value c is used for the operation, then the post increment c++).
根据GCC 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。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,然后在最后一遍计算所有的后增量表达式。
因此,在定义行为的c++中,就像Visual 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文档在优先级和计算顺序中所述:
当几个运算符同时出现时,它们具有相同的优先级,并根据它们的结合性进行计算。表中的操作符在以后缀操作符开头的部分中描述。
虽然任何编译器和处理器实际上都不太可能这样做,但在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将增加2),而且未来的某些处理器可能会提供类似的功能,这并不是完全不可想象的。
If the compiler were to write i++ as indicated above (legal under the standard) and were to intersperse the above instructions throughout the evaluation of the overall expression (also legal), and if it didn't happen to notice that one of the other instructions happened to access i, it would be possible (and legal) for the compiler to generate a sequence of instructions that would deadlock. To be sure, a compiler would almost certainly detect the problem in the case where the same variable i is used in both places, but if a routine accepts references to two pointers p and q, and uses (*p) and (*q) in the above expression (rather than using i twice) the compiler would not be required to recognize or avoid the deadlock that would occur if the same object's address were passed for both p and q.
ISO W14站点的文档n1188提供了关于这种计算的一个很好的解释。
我解释这些想法。
适用于这种情况的ISO 9899标准的主要规则是6.5p2。
在前一个序列点和下一个序列点之间,通过表达式求值,对象的存储值最多修改一次。此外,前面的值只能被读取,以确定要存储的值。
像i=i++这样的表达式中的序列点在i=i之前和i++之后。
In the paper that I quoted above it is explained that you can figure out the program as being formed by small boxes, each box containing the instructions between 2 consecutive sequence points. The sequence points are defined in annex C of the standard, in the case of i=i++ there are 2 sequence points that delimit a full-expression. Such an expression is syntactically equivalent with an entry of expression-statement in the Backus-Naur form of the grammar (a grammar is provided in annex A of the Standard).
所以盒子里的指令顺序没有明确的顺序。
i=i++
可以解释为
tmp = i
i=i+1
i = tmp
或者是
tmp = i
i = tmp
i=i+1
因为解释代码i=i++的所有这些形式都是有效的,并且都生成不同的答案,所以行为是未定义的。
因此,序列点可以从组成程序的每个方框的开始和结束处看到(方框是C语言中的原子单位),并且在方框中,指令的顺序并不在所有情况下都是定义的。改变顺序有时会改变结果。
编辑:
其他解释这种歧义的很好的来源是c-faq网站(也出版了一本书)的条目,即这里、这里和这里。
这种行为实际上无法解释,因为它同时调用了未指定的行为和未定义的行为,所以我们不能对这段代码做出任何一般的预测,尽管如果你阅读了Olve Maudal的著作,比如《Deep C》和《未指定的和未定义的》,有时你可以用特定的编译器和环境在非常特定的情况下做出很好的猜测,但请不要在生产环境附近这样做。
所以我们继续讨论未指明的行为,在c99标准草案第6.5节第3段说(我的重点):
操作符和操作数的分组由语法表示。除非指定 稍后(对于函数call()、&&、||、?:和逗号操作符),子表达式的求值顺序和副作用发生的顺序都未指定。
所以当我们有一条这样的直线
i = i++ + ++i;
我们不知道是i++还是++i先被求值。这主要是为了给编译器提供更好的优化选项。
这里也有未定义的行为,因为程序在序列点之间不止一次地修改变量(i, u等)。标准草案第6.5节第2段(重点):
在前一个序列点和下一个序列点之间,一个对象应该有它的存储值 由表达式求值最多修改一次。此外,先验值 应该是只读的,以确定要存储的值。
它引用了以下未定义的代码示例:
i = ++i + 1;
a[i++] = i;
在所有这些示例中,代码都试图在同一序列point中多次修改一个对象,该序列point将以;在以下每一种情况下:
i = i++ + ++i;
^ ^ ^
i = (i++);
^ ^
u = u++ + ++u;
^ ^ ^
u = (u++);
^ ^
v = v++ + ++v;
^ ^ ^
在c99标准草案第3.4.4节中,未指定的行为定义为:
使用未指定的值,或本标准规定的其他行为 两种或两种以上的可能性,并且对选择哪一种没有进一步的要求 实例
而未定义的行为在3.4.3节中定义为:
在使用不可移植的或错误的程序构造或错误的数据时, 本标准对此没有任何要求
并指出:
可能的未定义行为包括:完全忽略带有不可预测结果的情况,在翻译或程序执行期间以环境特征的文档化方式(有或没有发出诊断消息)行为,以及终止翻译或执行(发出诊断消息)。