当我运行此代码时,输出为 11、10。
为什么会这样?有人可以给我一个解释,希望能启发我吗?
谢谢
#include <iostream>
using namespace std;
void print(int x, int y)
{
cout << x << endl;
cout << y << endl;
}
int main()
{
int x = 10;
print(x, x++);
}
当我运行此代码时,输出为 11、10。
为什么会这样?有人可以给我一个解释,希望能启发我吗?
谢谢
#include <iostream>
using namespace std;
void print(int x, int y)
{
cout << x << endl;
cout << y << endl;
}
int main()
{
int x = 10;
print(x, x++);
}
C++ 标准规定(第1.9.16 节中的注释):
与不同参数表达式相关的值计算和副作用是无序的。
换句话说,在将参数的值传递给函数之前,参数的评估顺序是未定义的和/或编译器相关的。因此,在某些编译器(首先评估左参数)上,代码将输出10, 10
,而在其他编译器(首先评估右参数)上,它将输出11, 10
. 一般来说,你永远不应该依赖未定义的行为。
为了帮助您理解这一点,假设在调用函数之前对每个参数表达式进行评估(并不是说这正是它的实际工作方式,这只是一种简单的思考方式,可以帮助您理解排序):
int arg1 = x; // This line
int arg2 = x++; // And this line can be swapped.
print(arg1, arg2);
C++ 标准说两个参数表达式是无序的。所以,如果我们像这样在单独的行上写出参数表达式,它们的顺序应该不重要,因为标准说它们可以按任何顺序进行计算。一些编译器可能会按上述顺序评估它们,其他编译器可能会交换它们:
int arg2 = x++; // And this line can be swapped.
int arg1 = x; // This line
print(arg1, arg2);
这使得它很明显如何arg2
保持价值10
,同时arg1
保持价值11
。
您应该始终避免在代码中出现这种未定义的行为。
整体而言,声明:
print(x, x++);
导致未定义的行为。一旦程序具有未定义的行为,它就不再是有效的 C++ 程序,并且实际上任何行为都是可能的。因此,为这样的程序寻找推理是没有意义的。
为什么这是未定义的行为?
让我们逐步评估程序,直到我们可以毫无疑问地证明它会导致Undefined Behavior。
函数参数的评估顺序是未指定的[参考 1]。
未指定意味着允许实现根据需要实现此特定功能,并且不需要记录有关它的详细信息。
将上述规则应用于您的函数调用:
print(x, x++);
一个实现可能会将其评估为:
简而言之,您不能依赖实现来遵循任何特定顺序,因为根据 C++ 标准,它不是必需的。
在 C/C++ 中,如果没有中间序列点[Ref 2] ,您不能多次读取或写入变量。如果这样做,则会导致未定义的行为。无论任何一个参数是否在所述函数中首先被评估,它们之间没有序列点,只有在评估所有函数参数后才会存在序列点[Ref 3]。
在这种情况下x
,在没有中间序列点的情况下进行访问,因此会导致未定义的行为。
简而言之,最好编写任何不调用此类未定义行为的代码,因为一旦这样做,您就无法期望此类程序有任何特定行为。
[参考 1] C++03 标准§5.2.2.8
第 8 段:
[...] 函数参数的评估顺序未指定。[...]
[参考 2] C++03 5 表达式 [expr]:
第 4 段:
....
在前一个和下一个序列点之间,一个标量对象的存储值最多只能通过表达式的评估修改一次。此外,只能访问先验值以确定要存储的值。对于完整表达式的子表达式的每个允许排序,都应满足本段的要求;否则行为未定义。
[参考 3] C++03 1.9 程序执行 [intro.execution]:
第 17 段:
调用函数时(无论该函数是否内联),在对所有函数参数(如果有)求值之后都有一个序列点,该序列点发生在函数体中的任何表达式或语句执行之前。
x++ 是一个函数参数,它们可以按未指定的顺序进行评估,这意味着行为未定义且不可移植(或合法)。
我相信这与最后一个参数首先进入的函数调用堆栈有关。所以 x++ 是你的 y 而 x 是 print() 中的本地 x。
迟到的答案。忽略评估顺序的问题,请注意 C++ 标准解释了后递增和后递减如何操作:“后递增和后递减创建对象的副本,递增或递减对象的值并返回副本从增加或减少之前开始。”
https://en.cppreference.com/w/cpp/language/operator_incdec
作为结果差异显着的示例,请考虑 std::list::splice,例如:
mylist.splice(where, mylist, iter++);
This will move the node pointed by iter to just before the node pointed by where. The sequence will be make a copy of iter to be passed to splice, increment iter, then call splice using the copy of iter before it was incremented. After splice returns, iter will point to the next node after the node iter originally pointed to, as opposed to the next node after iter's new location in the list after it was moved.