为什么这些构造使用前后增量未定义的行为?

#include  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 %d\n", w++, ++w, w); // shouldn't this print 0 2 2 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发生更改,则不会certificate或更改任何内容。 它是未定义的 ; 你无法推理这种行为。

你看起来最有趣的例子

 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 = ii = 1 j = 1 0x00000014 <+20>: add %eax,%eax // j += ji = 1 j = 2 0x00000016 <+22>: add %eax,-0x4(%ebp) // i += ji = 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:

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

行为无法真正解释,因为它会调用未指定的行为和未定义的行为 ,因此我们无法对此代码进行任何一般性预测,但如果您阅读Olve Maudal的工作,例如Deep C和Unspecified and Undefined,有时您可以做好在特定情况下使用特定的编译器和环境猜测,但请不要在生产附近的任何地方这样做。

因此转向未指明的行为 ,在草案c99标准第6.5节第3段中说( 强调我的 ):

运算符和操作数的分组由语法表示.74)除了稍后指定的(对于函数调用(),&&,||,?:和逗号运算符), 子表达式的评估顺序和顺序发生哪些副作用都是未指明的。

所以,当我们有这样一条线:

 i = i++ + ++i; 

我们不知道是否会首先评估i++++i 。 这主要是为编译器提供更好的优化选项 。

我们这里也有未定义的行为 ,因为程序在序列点之间不止一次地修改变量( iu等)。 草案标准第6.5节第2段( 强调我的 ):

在前一个和下一个序列点之间,对象的存储值最多只能通过表达式的计算修改一次 。 此外, 先前的值应该是只读的,以确定要存储的值

它引用以下代码示例为未定义:

 i = ++i + 1; a[i++] = i; 

在所有这些示例中,代码试图在同一序列点中多次修改对象,这将以; 在每一个案例中:

 i = i++ + ++i; ^ ^ ^ i = (i++); ^ ^ u = u++ + ++u; ^ ^ ^ u = (u++); ^ ^ v = v++ + ++v; ^ ^ ^ 

3.4.4节中的c99标准草案中定义了未指定的行为

使用未指明的值,或本国际标准提供两种或更多种可能性的其他行为,并且在任何情况下都不会对其进行任何进一步的要求

未定义的行为3.4.3节中定义为:

行为,在使用不可移植或错误的程序结构或错误数据时,本国际标准不对此要求

并注意到:

可能的未定义行为包括完全忽略不完整结果的情况,在翻译或程序执行期间以环境特征(有或没有发出诊断消息)的特定文档执行,终止翻译或执行(发布时)一条诊断信息)。

这里引用的大多数答案来自C标准,强调这些结构的行为是不确定的。 要理解为什么这些结构的行为未定义 ,让我们首先根据C11标准理解这些术语:

顺序: (5.1.2.3)

给定任何两个评估AB ,如果AB之前被排序,那么A的执行应该在B的执行之前。

未测序:

如果AB之前或之后没有排序,则AB未被排序。

评估可以是两件事之一:

  • 值计算计算出表达式的结果; 和
  • 副作用 ,是对象的修改。

序列点:

在表达式AB的评估之间存在序列点意味着AB相关联的每个值计算副作用之前对A相关联的每个值计算副作用进行排序。

现在提出问题,对于像这样的表达式

 int i = 1; i = i++; 

标准说:

6.5表达式:

如果对标量对象的副作用相对于 对同一标量对象的不同副作用或使用相同标量对象的值进行的值计算未进行排序, 则行为未定义 。 […]

因此,上面的表达式调用UB,因为对同一对象i两个副作用相对于彼此是无序的。 这意味着没有对通过分配给i的副作用是否在副作用之前或之后通过++进行排序。
根据赋值是在增量之前还是之后发生,将产生不同的结果,这是未定义行为的情况之一。

让我们在赋值的左边重命名为i在赋值的右边(在表达式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 

得到两个不同的结果12 ,它取决于通过赋值和++副作用的顺序,因此调用UB。

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

第一个片段问i = i++ + ++ii = i++ + ++i ,在我的书中非常疯狂。 没有人会在真实的程序中编写它,它的作用并不明显,没有任何可想象的算法,有人可能会尝试编码,这会产生这种特殊的操作序列。 因为对你和我来说,它应该做的事情并不明显,如果编译器无法弄清楚它应该做什么,那么在我的书中这很好。

第二个片段, i = i++ ,更容易理解。 有人显然试图增加i,并将结果分配给i。 但是有几种方法可以在C中执行此操作。向i添加1并将结果返回给i的最基本方法在几乎所有编程语言中都是相同的:

 i = i + 1 

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

 i++ 

这意味着,“向i添加1,并将结果返回给i”。 因此,如果我们通过写作构建两者的大杂烩

 i = i++ 

我们真正说的是“向i添加1,并将结果返回给i,并将结果返回给i”。 我们很困惑,所以如果编译器也感到困惑,它也不会让我感到困扰。

实际上,这些疯狂的表达式写作的唯一时间是人们将它们用作++应该如何工作的人为例子。 当然,了解++的工作原理非常重要。 但是使用++的一个实际规则是,“如果使用++意味着什么表达不明显,就不要写它。”

我们曾经花了不少时间在comp.lang.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 

虽然我不认为任何处理器支持硬件以允许有效地完成这样的事情,但是可以很容易地想象这样的行为会使multithreading代码更容易的情况(例如,如果两个线程试图执行上述操作,它将保证同时顺序, i会增加2)并且一些未来的处理器可能提供类似的function并不是完全不可思议的。

如果编译器如上所述编写i++ (在标准下是合法的)并且在整个表达式的评估过程中散布上述指令(也是合法的),并且如果没有注意到其他指令之一碰巧访问i ,编译器生成一系列会死锁的指令是可能的(也是合法的)。 可以肯定的是,编译器几乎肯定会在两个地方使用相同变量i的情况下检测到问题,但是如果例程接受对两个指针pq引用,并使用(*p)(*q)在上面的表达式中(而不是使用i两次),编译器不需要识别或避免在pq都传递相同对象的地址时会发生的死锁。

通常这个问题被链接为与代码相关的问题的副本

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

这也有未指定的行为,因为++xy++的评估顺序未指定。 但这是完全合法和有效的声明。 此语句中没有未定义的行为。 因为修改( ++xy++ )是针对不同的对象完成的。

是什么呈现以下声明

 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=%dj=%d\n",i, j); // prints: i=7 j=6 

逗号运算符从左到右计算其操作数,并仅生成最后一个操作数的值。 所以在j = (++i, i++);++i递增到6并且i++产生分配给ji6 )的旧值。 然后由于后增量i变成了7

因此,如果函数调用中的逗号是逗号运算符,那么

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

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


对于那些对未定义行为不熟悉的人,可以从阅读每个C程序员应该知道的 未定义行为中获益,以理解C中未定义行为的概念和许多其他变体。

这篇文章: 未定义,未指定和实现定义的行为也是相关的。

C标准规定变量最多只能在两个序列点之间分配一次。 例如,分号是序列点。
所以表格的每一个陈述:

 i = i++; i = i++ + ++i; 

等违反了这条规则。 该标准还表示行为未定义且未指定。 有些编译器会检测到这些并产生一些结果,但这不符合标准。

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

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

以上是复制/分析字符串时的常见编码习惯。

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

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

脚注73进一步澄清了这一点

  1. 此段落呈现未定义的语句表达式,如

     i = ++i + 1; a[i++] = i; 

    同时允许

     i = i + 1; a[i] = i; 

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

  1. 以下是5.1.2.3中描述的序列点:

    • 在函数调用和实际调用中的函数指示符和实际参数的评估之间。 (6.5.2.2)。
    • 在以下运算符的第一个和第二个操作数的计算之间:逻辑AND &&(6.5.13); 逻辑OR || (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中同一段的措辞是:

  1. 如果对标量对象的副作用相对于对同一标量对象的不同副作用或使用相同标量对象的值进行的值计算未进行排序,则行为未定义。 如果表达式的子表达式有多个允许的排序,则如果在任何排序中发生这种未测序的副作用,则行为是不确定的.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递增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修改两次。

在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 

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

ISO W14站点的文档n1188中提供了关于此类计算中发生的情况的良好解释。

我解释了这些想法。

在这种情况下适用的标准ISO 9899的主要规则是6.5p2。

在前一个和下一个序列点之间,对象的存储值最多只能通过表达式的计算修改一次。 此外,先前的值应该是只读的,以确定要存储的值。

i=i++这样的表达式中的序列点在i=i++之前和i++

在我上面引用的论文中,解释了你可以把程序想象成由小盒子组成,每个盒子包含2个连续序列点之间的指令。 序列点在标准的附录C中定义,在i=i++的情况下,存在2个序列点,其界定完整表达。 这样的表达式在语法上等同于语法的Backus-Naurforms的expression-statement的条目(语法的附录A中提供了语法)。

因此,框内的指令顺序没有明确的顺序。

 i=i++ 

可以解释为

 tmp = i i=i+1 i = tmp 

或者作为

 tmp = i i = tmp i=i+1 

因为解释代码i=i++所有这些forms都是有效的,并且因为两者都产生不同的答案,所以行为是未定义的。

因此,组成程序的每个框的开头和结尾都可以看到序列点[框中是C中的primefaces单元],并且在框内,指令的顺序在所有情况下都没有定义。 更改该订单有时可以更改结果。

编辑:

解释这种含糊不清的其他好的来源是来自c-faq网站的条目(也作为一本书出版 ),即此处以及此处和此处 。

原因是程序正在运行未定义的行为。 问题在于评估顺序,因为根据C ++ 98标准没有所需的序列点(根据C ++ 11术语,没有操作在其他操作之前或之后排序)。

However if you stick to one compiler, you will find the behavior persistent, as long as you don’t add function calls or pointers, which would make the behavior more messy.

  • So first the GCC: Using Nuwen MinGW 15 GCC 7.1 you will get:

     #include 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++).

according to GCC C++: Operators

In GCC C++, the precedence of the operators controls the order in which the individual operators are evaluated

the equivalent code in defined behavior C++ as GCC understands:

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

Then we go to Visual Studio . Visual Studio 2015, you get:

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

How does visual studio work, it takes another approach, it evaluates all pre-increments expressions in first pass, then uses variables values in the operations in second pass, assign from RHS to LHS in third pass, then at last pass it evaluates all the post-increment expressions in one pass.

So the equivalent in defined behavior C++ as Visual C++ understands:

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

as Visual Studio documentation states at Precedence and Order of Evaluation :

Where several operators appear together, they have equal precedence and are evaluated according to their associativity. The operators in the table are described in the sections beginning with Postfix Operators.

Your question was probably not, “Why are these constructs undefined behavior in C?”. Your question was probably, “Why did this code (using ++ ) not give me the value I expected?”, and someone marked your question as a duplicate, and sent you here.

This answer tries to answer that question: why did your code not give you the answer you expected, and how can you learn to recognize (and avoid) expressions that will not work as expected.

I assume you’ve heard the basic definition of C’s ++ and -- operators by now, and how the prefix form ++x differs from the postfix form x++ . But these operators are hard to think about, so to make sure you understood, perhaps you wrote a tiny little test program involving something like

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

But, to your surprise, this program did not help you understand — it printed some strange, unexpected, inexplicable output, suggesting that maybe ++ does something completely different, not at all what you thought it did.

Or, perhaps you’re looking at a hard-to-understand expression like

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

Perhaps someone gave you that code as a puzzle. This code also makes no sense, especially if you run it — and if you compile and run it under two different compilers, you’re likely to get two different answers! 那是怎么回事? Which answer is correct? (And the answer is that both of them are, or neither of them are.)

As you’ve heard by now, all of these expressions are undefined , which means that the C language makes no guarantee about what they’ll do. This is a strange and surprising result, because you probably thought that any program you could write, as long as it compiled and ran, would generate a unique, well-defined output. But in the case of undefined behavior, that’s not so.

What makes an expression undefined? Are expressions involving ++ and -- always undefined? Of course not: these are useful operators, and if you use them properly, they’re perfectly well-defined.

For the expressions we’re talking about what makes them undefined is when there’s too much going on at once, when we’re not sure what order things will happen in, but when the order matters to the result we get.

Let’s go back to the two examples I’ve used in this answer. When I wrote

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

the question is, before 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 different results printed by printf .

What about this crazy expression?

 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 add 1 to x, store the new value in x , and return the old value of x ; (2) the ++x part tries to add 1 to x, store the new value in x , and return the new value of x ; 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 get assigned to 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.


So with all that background and introduction out of the way, if you want to make sure that all your programs are well-defined, which expressions can you write, and which ones can you not write?

These expressions are all fine:

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

These expressions are all undefined:

 x = x++; x = x++ + ++x; y = x + x++; a[i] = i++; a[i++] = i; printf("%d %d %d\n", x, ++x, x++); 

And the last question is, how can you tell which expressions are well-defined, and which expressions are undefined?

As I said earlier, the undefined expressions are the ones where there’s too much going at once, where you can’t be sure what order things happen in, and where the order matters:

  1. If there’s one variable that’s getting modified (assigned to) in two or more different places, how do you know which modification happens first?
  2. If there’s a variable that’s getting modified in one place, and having its value used in another place, how do you know whether it uses the old value or the new value?

As an example of #1, in the expression

 x = x++ + ++x; 

there are three attempts to modify `x.

As an example of #2, in the expression

 y = x + x++; 

we both use the value of x , and modify it.

So that’s the answer: make sure that in any expression you write, each variable is modified at most once, and if a variable is modified, you don’t also attempt to use the value of that variable somewhere else.