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

#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?
}
94976 次浏览

C具有未定义行为的概念,即某些语言结构在语法上是有效的,但您无法预测代码运行时的行为。

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

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

你最有趣的例子是

u = (u++);

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

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

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

和6.5.16赋值运算符,§4:

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

只要编译和反汇编你的代码行,如果你很想知道它到底是如何得到你所得到的。

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

$ cat evil.cvoid evil(){int i = 0;i+= i++ + ++i;}$ gcc evil.c -c -o evil.bin$ gdb evil.bin(gdb) disassemble evilDump of assembler code for function evil:0x00000000 <+0>:   push   %ebp0x00000001 <+1>:   mov    %esp,%ebp0x00000003 <+3>:   sub    $0x10,%esp0x00000006 <+6>:   movl   $0x0,-0x4(%ebp)  // i = 0   i = 00x0000000d <+13>:  addl   $0x1,-0x4(%ebp)  // i++     i = 10x00000011 <+17>:  mov    -0x4(%ebp),%eax  // j = i   i = 1  j = 10x00000014 <+20>:  add    %eax,%eax        // j += j  i = 1  j = 20x00000016 <+22>:  add    %eax,-0x4(%ebp)  // i += j  i = 30x00000019 <+25>:  addl   $0x1,-0x4(%ebp)  // i++     i = 40x0000001d <+29>:  leave0x0000001e <+30>:  retEnd of assembler dump.

(我……假设0x00000014指令是某种编译器优化?)

虽然任何编译器和处理器都不太可能真正这样做,但根据C标准,编译器使用序列实现“i++”是合法的:

In a single operation, read `i` and lock it to prevent access until further noticeCompute (1+read_value)In a single operation, unlock `i` and store the computed value

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

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

这种行为无法真正解释,因为它同时调用了未指定行为未定义行为,所以我们不能对这段代码做出任何一般的预测,尽管如果你读了Olve Maudal's的工作,比如深c未指定和未定义,有时你可以在特定的编译器和环境的非常具体的情况下做出很好的猜测,但请不要在生产环境附近这样做。

所以转到未指定行为,在c99标准草案部分6.53段说(重点矿):

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

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

i = i++ + ++i;

我们不知道是先计算i++还是++i。这主要是为了给编译器更好的优化选择

我们这里也有未定义行为,因为程序在序列点之间多次修改变量(iu等)。来自标准草案第2节第5段(重点矿):

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

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

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标准规定变量在两个序列点之间最多只能分配一次。例如,分号是序列点。
因此,形式的每个语句:

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

我们真正的意思是“将1添加到i,并将结果分配回i,并将结果分配回i”。我们很困惑,所以如果编译器也感到困惑,我不会太困扰。

实际上,这些疯狂的表达方式唯一被写出来的时候,是人们用它们作为人工例子来说明++应该如何工作的时候。当然,理解++是如何工作的很重要。但使用++的一个实用规则是,“如果使用++的表达方式不明显,不要写它。”

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

另见问题3.8C常见问题列表第3的其余问题。

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

已排序:(5.1.2.3)

给定任意两个求值AB,如果A排序在B之前,则A的执行应先于B的执行。

未排序:

如果A未在B之前或之后排序,则AB未排序。

评估可以是两件事之一:

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

序列点:

在表达式AB的求值之间存在序列点意味着与A相关的每个价值计算副作用都在与B相关的每个价值计算副作用之前进行排序。

现在进入问题,对于像这样的表达

int i = 1;i = i++;

标准说:

6.5表情:

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

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

让我们将赋值左边的i重命名为il,赋值右边的ir(在表达式i++中),然后表达式就像

il = ir++     // Note that suffix l and r are used for the sake of clarity.// Both il and ir represents the same object.

重要的一点关于后缀++运算符是:

仅仅因为#0出现在变量之后并不意味着增量发生得晚。增量可以早在编译器喜欢的时候发生只要编译器确保使用原值

这意味着表达式il = ir++可以被评估为

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

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

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

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

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

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

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

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

不会有问题。但它调用未定义行为,因为这里的逗号分离器


对于那些不熟悉未定义行为的人来说,阅读每个C程序员都应该知道的关于未定义行为的知识将有助于理解C中未定义行为的概念和许多其他变体。

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

虽然a = a++a++ + a++等表达式中的语法是合法的,但这些构造中的行为未定义,因为C标准中的没有被遵守。C99 6.5p2

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

随着脚注73进一步澄清

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

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

    同时允许

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

The various sequence points are listed in Annex C of C11 (and C99):

  1. 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).

The wording of the same paragraph in C11 is:

  1. If a side effect on a scalar object is unsequenced relative to either a different side effect on the same scalar object or a value computation using the value of the same scalar object, the behavior is undefined. If there are multiple allowable orderings of the subexpressions of an expression, the behavior is undefined if such an unsequenced side effect occurs in any of the orderings.84)

You can detect such errors in a program by for example using a recent version of GCC with -Wall and -Werror, and then GCC will outright refuse to compile your program. The following is the output of gcc (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005:

% gcc plusplus.c -Wall -Werror -pedanticplusplus.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);

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

原因是程序正在运行未定义的行为。问题在于评估顺序,因为没有根据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}

GCC是如何工作的?它以从左到右的顺序评估右侧(RHS)的子表达式,然后将值分配给左侧(LHS)。这正是Java和C#的行为和定义它们的标准。(是的,Java和C#中的等效软件已经定义了行为)。它以从左到右的顺序在RHS语句中逐个评估每个子表达式;对于每个子表达式:首先评估++c(预增量),然后使用值c进行操作,然后c++)。

根据GCCC++:运营商

在GCCC++中,运算符的优先级控制其中单个运算符被评估

定义行为中的等效代码C++GCC理解的:

#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++VisualC++理解的:

#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++之后。

在我上面引用的论文中解释说,你可以把程序看成是由小盒子组成的,每个盒子包含2个连续序列点之间的指令。序列点在标准的附件C中定义,在i=i++的情况下,有2个序列点分隔一个完整表达式。这样的表达式在语法的巴克斯-诺尔形式中与expression-statement的条目等同(语法在标准的附件A中提供)。

所以盒子里的指令顺序没有明确的顺序。

i=i++

可以解释为

tmp = ii=i+1i = tmp

或作为

tmp = ii = tmpi=i+1

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

因此,序列点可以在组成程序的每个盒子的开始和结束处看到[盒子是C中的原子单元],并且在盒子内,指令的顺序并非在所有情况下都定义。更改顺序有时可以更改结果。

编辑:

解释这种歧义的其他好来源是来自c-faq站点(也发布了作为一本书)的条目,即这里这里这里

你的问题可能不是,“为什么这些构造在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++);

问题是,在实际调用printf之前,编译器会先计算x的值,还是x++,或者++x?但结果是x0。C中没有规则规定函数的参数会从左到右,从右到左,或者以其他顺序计算。所以我们不能说编译器是会先执行x,然后++x,然后x++,还是x++,然后++x,然后x,或者其他顺序。但顺序显然很重要,因为根据编译器使用的顺序,我们显然会打印出不同的数字系列。

这个疯狂的表达呢?

x = x++ + ++x;

这个表达式的问题在于它包含三种不同的修改x值的尝试:(1)x++部分尝试获取x的值,添加1,将新值存储在x中,并返回旧值;(2)++x部分尝试获取x的值,添加1,将新值存储在x中,并返回新值;(3)x =部分尝试将另外两个值的总和赋值回x。这三个尝试赋值中的哪一个会“赢”?这三个值中的哪一个实际上会决定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. 如果有一个变量在两个或多个不同的地方被修改(分配给),你怎么知道哪个修改首先发生?
  2. 如果有一个变量在一个地方被修改,它的值在另一个地方被使用,你怎么知道它使用的是旧值还是新值?

作为#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这样的表达式。