递增和递减变量
对变量进行递增(加 1)和递减(减 1)都是非常常见的操作,因此它们有自己的运算符。
运算符 | 符号 | 形式 | 操作 |
---|---|---|---|
前缀增量(前置递增) | ++ | ++x | 递增 x,然后返回 x |
前缀减量(前置递减) | –– | ––x | 递减 x,然后返回 x |
后缀增量(后置递增) | ++ | x++ | 复制 x,然后递增 x,然后返回副本 |
后缀减量(后置递减) | –– | x–– | 复制 x,然后递减 x,然后返回副本 |
请注意,每个运算符都有两个版本——前缀版本(运算符在操作数之前)和后缀版本(运算符在操作数之后)。
前缀递增和递减
前缀递增/递减运算符非常直观。首先,操作数被递增或递减,然后表达式计算为操作数的值。例如:
#include <iostream>
int main()
{
int x { 5 };
int y { ++x }; // x is incremented to 6, x is evaluated to the value 6, and 6 is assigned to y
std::cout << x << ' ' << y << '\n';
return 0;
}
这会打印
6 6
后缀递增和递减
后缀递增/递减运算符更复杂。首先,创建操作数的一个副本。然后操作数(不是副本)被递增或递减。最后,副本(不是原始值)被求值。例如:
#include <iostream>
int main()
{
int x { 5 };
int y { x++ }; // x is incremented to 6, copy of original x is evaluated to the value 5, and 5 is assigned to y
std::cout << x << ' ' << y << '\n';
return 0;
}
这会打印
6 5
我们来更详细地研究一下第 6 行是如何工作的。首先,创建了一个临时副本 x,它的初始值与 x 相同(5)。然后实际的 x 从 5 递增到 6。然后 x 的副本(仍然有值 5)被返回并赋值给 y。然后临时副本被丢弃。
因此,y 最终值为 5(递增前的值),而 x 最终值为 6(递增后的值)。
请注意,后缀版本需要更多的步骤,因此可能不如前缀版本高效。
更多例子
这是另一个示例,展示了前缀版本和后缀版本之间的区别
#include <iostream>
int main()
{
int x { 5 };
int y { 5 };
std::cout << x << ' ' << y << '\n';
std::cout << ++x << ' ' << --y << '\n'; // prefix
std::cout << x << ' ' << y << '\n';
std::cout << x++ << ' ' << y-- << '\n'; // postfix
std::cout << x << ' ' << y << '\n';
return 0;
}
这会产生输出
5 5 6 4 6 4 6 4 7 3
在第 8 行,我们进行前缀递增和递减。在此行中,x 和 y 在它们的值发送到 std::cout 之前被递增/递减,因此我们看到它们更新后的值由 std::cout 反映出来。
在第 10 行,我们进行后缀递增和递减。在此行中,x 和 y 的副本(带有递增前和递减前的值)被发送到 std::cout,因此我们在此处看不到递增和递减的反映。这些更改直到下一行,当 x 和 y 再次求值时才会显示出来。
何时使用前缀 vs 后缀
在许多情况下,前缀和后缀运算符产生相同的行为
int main()
{
int x { 0 };
++x; // increments x to 1
x++; // increments x to 2
return 0;
}
在代码可以使用前缀或后缀的情况下,优先使用前缀版本,因为它们通常更高效,并且更不容易引起意外。
最佳实践
优先使用前缀版本,因为它们更高效,并且更不容易引起意外。
当使用后缀版本比使用前缀版本编写的等效代码更简洁或更易懂时,才使用后缀版本。
副作用
如果函数或表达式除了产生返回值之外还有一些可观察到的效果,则称其具有**副作用**。
副作用的常见示例包括更改对象的值、执行输入或输出,或更新图形用户界面(例如启用或禁用按钮)。
大多数时候,副作用是有用的
x = 5; // the assignment operator has side effect of changing value of x
++x; // operator++ has side effect of incrementing x
std::cout << x; // operator<< has side effect of modifying the state of the console
上面示例中的赋值运算符具有永久更改 x 值的副作用。即使语句执行完毕,x 仍将具有值 5。同样,对于运算符 ++,即使语句求值完毕,x 的值也会被更改。输出 x 也具有修改控制台状态的副作用,因为您现在可以在控制台上看到 x 的值。
关键见解
赋值运算符、前缀运算符和后缀运算符具有永久更改对象值的副作用。
其他运算符(例如算术运算符)返回一个值,并且不修改其操作数。
副作用可能导致求值顺序问题
在某些情况下,副作用可能导致求值顺序问题。例如:
#include <iostream>
int add(int x, int y)
{
return x + y;
}
int main()
{
int x { 5 };
int value{ add(x, ++x) }; // undefined behavior: is this 5 + 6, or 6 + 6?
// It depends on what order your compiler evaluates the function arguments in
std::cout << value << '\n'; // value could be 11 or 12, depending on how the above line evaluates!
return 0;
}
C++ 标准没有定义函数参数的求值顺序。如果先求值左参数,则这变为调用 add(5, 6),结果为 11。如果先求值右参数,则这变为调用 add(6, 6),结果为 12!请注意,这只是一个问题,因为函数 add() 的一个参数具有副作用。
题外话…
C++ 标准故意不定义这些内容,以便编译器可以根据给定架构做最自然(因此最有效)的事情。
副作用的排序
在许多情况下,C++ 也不指定何时必须应用运算符的副作用。这可能导致在一个语句中多次使用应用了副作用的对象时出现未定义行为。
例如,表达式 x + ++x
是未指定行为。当 x
初始化为 1
时,Visual Studio 和 GCC 将其评估为 2 + 2
,而 Clang 将其评估为 1 + 2
!这是由于编译器应用递增 x
的副作用的时间不同。
即使 C++ 标准明确规定了如何求值,但从历史上看,这仍然是许多编译器错误出现的地方。这些问题通常可以通过确保任何已应用副作用的变量在一个给定语句中只使用一次来**完全**避免。
警告
C++ 不定义函数参数或运算符操作数的求值顺序。
警告
在给定语句中,不要多次使用已应用副作用的变量。如果这样做,结果可能未定义。
一个例外是简单的赋值表达式,例如 x = x + y
(这基本上等同于 x += y
)。