#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?
}

当前回答

这种行为实际上无法解释,因为它同时调用了未指定的行为和未定义的行为,所以我们不能对这段代码做出任何一般的预测,尽管如果你阅读了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节中定义为:

在使用不可移植的或错误的程序构造或错误的数据时, 本标准对此没有任何要求

并指出:

可能的未定义行为包括:完全忽略带有不可预测结果的情况,在翻译或程序执行期间以环境特征的文档化方式(有或没有发出诊断消息)行为,以及终止翻译或执行(发出诊断消息)。

其他回答

这种行为实际上无法解释,因为它同时调用了未指定的行为和未定义的行为,所以我们不能对这段代码做出任何一般的预测,尽管如果你阅读了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节中定义为:

在使用不可移植的或错误的程序构造或错误的数据时, 本标准对此没有任何要求

并指出:

可能的未定义行为包括:完全忽略带有不可预测结果的情况,在翻译或程序执行期间以环境特征的文档化方式(有或没有发出诊断消息)行为,以及终止翻译或执行(发出诊断消息)。

C有未定义行为的概念,也就是说,一些语言结构在语法上是有效的,但你不能预测代码运行时的行为。

据我所知,该标准并没有明确说明为什么存在未定义行为的概念。在我看来,这只是因为语言设计者想在语义上有一些余地,而不是要求所有实现以完全相同的方式处理整数溢出,这很可能会带来严重的性能损失,他们只是让行为未定义,所以如果你编写的代码导致整数溢出,任何事情都可能发生。

那么,考虑到这一点,为什么这些是“问题”呢?语言清楚地表明,某些事情会导致未定义的行为。没有什么问题,没有什么“应该”牵涉其中。如果当其中一个涉及的变量被声明为volatile时,未定义的行为发生了变化,这并不能证明或改变任何东西。它是没有定义的;你不能对这种行为进行推理。

你最有趣的例子就是

u = (u++);

是教科书上未定义行为的例子(见维基百科关于序列点的条目)。

虽然像a = a++或a++ + a++这样的表达式的语法是合法的,但这些结构的行为是未定义的,因为在C标准中不遵守shall。C99 6.5 p2:

在前一个序列点和下一个序列点之间,通过表达式求值,对象的存储值最多修改一次。[72]此外,前面的值只能被读取,以确定要存储的值[73]

脚注73进一步澄清

本段给出了未定义的语句表达式,如 I = ++ I + 1; A [i++] = i; 同时允许 I = I + 1; A [i] = i;

各序列点列于C11(和C99)的附件C:

The following are the sequence points described in 5.1.2.3: Between the evaluations of the function designator and actual arguments in a function call and the actual call. (6.5.2.2). Between the evaluations of the first and second operands of the following operators: logical AND && (6.5.13); logical OR || (6.5.14); comma , (6.5.17). Between the evaluations of the first operand of the conditional ? : operator and whichever of the second and third operands is evaluated (6.5.15). The end of a full declarator: declarators (6.7.6); Between the evaluation of a full expression and the next full expression to be evaluated. The following are full expressions: an initializer that is not part of a compound literal (6.7.9); the expression in an expression statement (6.8.3); the controlling expression of a selection statement (if or switch) (6.8.4); the controlling expression of a while or do statement (6.8.5); each of the (optional) expressions of a for statement (6.8.5.3); the (optional) expression in a return statement (6.8.6.4). Immediately before a library function returns (7.1.4). After the actions associated with each formatted input/output function conversion specifier (7.21.6, 7.29.2). Immediately before and immediately after each call to a comparison function, and also between any call to a comparison function and any movement of the objects passed as arguments to that call (7.22.5).

C11同一段的措词是:

如果标量对象上的副作用相对于同一标量对象上的不同副作用或使用同一标量对象的值进行的值计算没有排序,则行为未定义。如果一个表达式的子表达式有多个允许的顺序,那么如果这种未排序的副作用出现在任意一个顺序中,则该行为是未定义的。


您可以在程序中检测此类错误,例如使用带有-Wall和-Werror的最新版本的GCC,然后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加1,得到旧值,丢弃旧值;然后在逗号运算符,解决副作用;然后将I加1,结果值就变成了表达式的值——也就是说,这只是一种写j = (I += 2)的人为方法,这也是一种“聪明”的写法

i += 2;
j = i;

然而,函数参数列表中的,不是逗号操作符,并且在不同参数的计算之间没有序列点;相反,他们对彼此的评价是没有顺序的;函数调用

int i = 0;
printf("%d %d\n", i++, ++i, i);

具有未定义的行为,因为在函数参数中i++和++i的计算之间没有序列点,因此i的值在前一个序列点和下一个序列点之间被i++和++i修改了两次。

原因是程序正在运行未定义的行为。问题在于求值顺序,因为根据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 = i++;
i = i++ + ++i;

等等都违反了这个规则。该标准还说行为是未定义的,而不是未指定的。一些编译器确实检测这些并产生一些结果,但这不是每个标准。

但是,两个不同的变量可以在两个序列点之间递增。

while(*src++ = *dst++);

以上是在复制/分析字符串时常见的编码实践。