你的问题可能不是,“为什么这些构造在 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
?但事实证明,我们不知道。在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个例子,在以下表达式中:
x = x++ + ++x;
有三次尝试修改 x
。
作为第二个示例,在表达式中:
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
这样的表达式。
(i++)
的值仍为1。 - Drew McGoweni = (i++);
的目的是什么,肯定有更清晰的写法。即使它被明确定义了,这也是正确的。即使在 Java 中定义了i = (i++);
的行为,这仍然是糟糕的代码。只需编写i++;
即可。 - Keith Thompson