#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++);

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


我认为C99标准的相关部分是6.5表达式,§2

在前一个序列点和下一个序列点之间,一个对象应该有它的存储值 由表达式求值最多修改一次。此外,先验值 应该是只读的,以确定要存储的值。

和6.5.16赋值操作符,§4:

操作数的求值顺序未指定。如果试图修改 赋值运算符的结果或在下一个序列点之后访问它的结果 行为是未定义的。


只要编译和反汇编你的代码行,如果你如此倾向于知道它是如何得到你所得到的。

这是我从我的机器上得到的,以及我认为正在发生的事情:

$ 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指令是某种编译器优化?)


虽然任何编译器和处理器实际上都不太可能这样做,但在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.


这种行为实际上无法解释,因为它同时调用了未指定的行为和未定义的行为,所以我们不能对这段代码做出任何一般的预测,尽管如果你阅读了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标准规定,一个变量最多只能在两个序列点之间赋值一次。例如,分号是一个序列点。 所以每个形式的表述

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

现代编译器可以很好地优化这一点。事实上,可能比您最初编写的代码更好(假设它按您希望的方式工作)。


回答这个问题的另一种方法是,不要陷入序列点和未定义行为的神秘细节中,而是简单地问,它们应该是什么意思?程序员试图做什么?

第一个片段,i = i++ + ++i,在我的书中很明显是疯狂的。没有人会把它写进真正的程序中,它的功能并不明显,没有人会尝试编写的算法会导致这种特殊的人为操作序列。因为你我都不清楚它应该做什么,所以在我的书中,如果编译器不能弄清楚它应该做什么也没关系。

第二个片段i = i++比较容易理解。很明显,有人试图增加i,并将结果赋值回给i。但是在c中有几种方法可以做到这一点。将1加到i,并将结果赋值回给i,这在几乎任何编程语言中都是相同的:

i = i + 1

当然,C有一个方便的快捷方式:

i++

这意味着,“将1加到i,并将结果赋给i”。所以如果我们通过写作来构造一个两者的大杂烩

i = i++

我们真正说的是“给i加1,然后把结果赋给i,再把结果赋给i”我们感到困惑,所以如果编译器也感到困惑,也不会太困扰我。

实际上,只有当人们将这些疯狂的表达式用作c++应该如何工作的人为示例时,才会写出这些疯狂的表达式。当然,理解++的工作原理也很重要。但使用++的一个实际规则是,“如果使用++的表达式的含义不明显,就不要写它。”

我们曾经在comp.lang.c上花了无数个小时讨论这样的表达式以及为什么它们是未定义的。我的两个较长的回答,试图真正解释为什么,被存档在网络上:

为什么标准没有定义它们的作用? 运算符的优先级不是决定求值的顺序吗?

请参见问题3.8和C常见问题列表第三部分的其他问题。


这里的大多数答案都引用自C标准,强调这些构造的行为是未定义的。为了理解为什么这些结构的行为是未定义的,让我们先从C11标准的角度来理解这些术语:

测序(5.1.2.3):

给定任意两个评估A和B,如果A排在B之前,则A的执行应先于B的执行。

Unsequenced:

如果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  

or

temp = ir;      // i = 1
il = temp;      // i = 1   side effect by assignment before ++
ir = ir + 1;    // i = 2   result is 2  

导致两个不同的结果1和2,这取决于通过赋值和++的副作用的顺序,因此调用UB。


这个问题通常是与代码相关的问题的重复链接,比如

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

or

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。

如果函数调用中的逗号是逗号操作符,那么

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

不会有问题的。但是它调用了未定义的行为,因为这里的逗号是分隔符。


对于那些不熟悉未定义行为的人来说,阅读《每一个C程序员都应该知道关于未定义行为的事情》可以帮助他们理解C语言中未定义行为的概念和许多其他变体。

本文:未定义、未指定和实现定义的行为也是相关的。


虽然像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文档在优先级和计算顺序中所述:

当几个运算符同时出现时,它们具有相同的优先级,并根据它们的结合性进行计算。表中的操作符在以后缀操作符开头的部分中描述。


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网站(也出版了一本书)的条目,即这里、这里和这里。


你的问题可能不是“为什么这些构念在C语言中是未定义的行为?”你的问题可能是,“为什么这段代码(使用++)没有给我预期的值?”,有人将你的问题标记为重复,并将你发送到这里。

这个答案试图回答这个问题:为什么您的代码没有给您预期的答案,以及您如何学会识别(和避免)不能按预期工作的表达式。

我假设您已经听说了C的c++和——操作符的基本定义,以及前缀形式++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++);

the question is, before actually calling printf, does the compiler compute the value of x first, or x++, or maybe ++x? But it turns out we don't know. There's no rule in C which says that the arguments to a function get evaluated left-to-right, or right-to-left, or in some other order. So we can't say whether the compiler will do x first, then ++x, then x++, or x++ then ++x then x, or some other order. But the order clearly matters, because depending on which order the compiler uses, we'll clearly get a different series of numbers printed out.

那么这个疯狂的表达呢?

x = x++ + ++x;

The problem with this expression is that it contains three different attempts to modify the value of x: (1) the x++ part tries to take x's value, add 1, store the new value in x, and return the old value; (2) the ++x part tries to take x's value, add 1, store the new value in x, and return the new value; and (3) the x = part tries to assign the sum of the other two back to x. Which of those three attempted assignments will "win"? Which of the three values will actually determine the final value of x? Again, and perhaps surprisingly, there's no rule in C to tell us.

You might imagine that precedence or associativity or left-to-right evaluation tells you what order things happen in, but they do not. You may not believe me, but please take my word for it, and I'll say it again: precedence and associativity do not determine every aspect of the evaluation order of an expression in C. In particular, if within one expression there are multiple different spots where we try to assign a new value to something like x, precedence and associativity do not tell us which of those attempts happens first, or last, or anything.


有了这些背景知识和介绍,如果你想确保你所有的程序都是定义良好的,哪些表达式可以写,哪些表达式不能写?

这些表达都不错:

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的值,并修改它。

所以这就是答案:确保在您编写的任何表达式中,每个变量最多被修改一次,如果一个变量被修改,您也不要试图在其他地方使用该变量的值。


还有一件事。您可能想知道如何“修复”我在回答这个问题时提出的未定义表达式。

在printf("%d %d %d\n", x, ++x, x++);的情况下,这很简单-只需将它写成三个独立的printf调用:

printf("%d ", x);
printf("%d ", ++x);
printf("%d\n", x++);

现在行为已经完全定义好了,您将得到合理的结果。

另一方面,在x = x++ + ++x的情况下,没有办法修复它。没有办法编写它来保证它的行为符合您的期望——但这没关系,因为无论如何在实际程序中都不会编写像x = x++ + ++x这样的表达式。