6-4 递增/递减运算符及其副作用
变量的递增与递减
对变量进行递增Incrementing(加1)和递减decrementing(减1)的操作如此常见,以至于它们各自拥有专属的运算符。
| Operator | Symbol | Form | Operation |
|---|---|---|---|
| Prefix increment (pre-increment) | ++ | ++x | Increment x, then return x |
| Prefix decrement (pre-decrement) | –– | ––x | Decrement x, then return x |
| Postfix increment (post-increment) | ++ | x++ | Copy x, then increment x, then return the copy |
| Postfix decrement (post-decrement) | –– | x–– | Copy x, then decrement x, then return the copy |
请注意,每个运算符都有两种版本——前缀版本(运算符位于操作数之前)和后缀版本(运算符位于操作数之后)。
前缀递增与递减
前缀递增/递减prefix increment/decrement运算符的原理非常简单:首先对操作数进行递增或递减操作,然后将表达式求值为该操作数的值。例如:
#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;
}
这将输出:

后缀递增与递减
后缀递增/递减postfix increment/decrement运算符的实现更为复杂。首先会创建操作数的副本,随后对操作数(而非副本)进行递增或递减操作,最后评估副本(而非原始值)。例如:
#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行代码的工作原理。首先,创建一个与x初始值相同的临时副本(即5)。随后将实际的x从5递增至6。接着将该副本(仍保持值5)返回并赋值给y。最后丢弃临时副本。
最终结果是:y 获得值 5(前置递增前的值)the pre-incremented value,而 x 获得值 6(后置递增后的值the post-incremented value)。
需注意后缀递增版本涉及更多操作步骤,因此性能可能不及前缀递增版本。
更多示例
以下是另一个展示前缀版本与后缀版本差异的示例:
#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;
}
这将产生以下输出:

在第8行,我们执行前缀递增和递减操作。在此行中,x和y的值在传递给std::cout之前被递增/递减,因此我们通过std::cout看到了它们更新后的值。
第10行执行后缀递增和递减操作。此处发送至std::cout的是x和y的副本(其值为递增/递减前的原始值),因此递增递减效果不会在此体现。这些变化要等到下一行重新评估x和y时才会显现。
何时使用前缀运算符与后缀运算符
在许多情况下,前缀运算符和后缀运算符会产生相同的行为:
int main()
{
int x { 0 };
++x; // increments x to 1
x++; // increments x to 2
return 0;
}
在代码既可采用前缀式又可采用后缀式编写的情况下,应优先选择前缀式版本,因其通常性能更优且不易引发意外。
最佳实践
优先采用前缀式版本,因其性能更优且不易引发意外。
仅当使用后缀式版本能显著提升代码简洁性或可读性时,才可选择替代前缀式版本。
副作用
若函数或表达式除返回值外还产生某些可观察到的影响,则称其具有副作用side effect。
常见的副作用包括:改变对象值、执行输入或输出操作、更新图形用户界面(例如启用或禁用按钮)。
大多数情况下,副作用是有用的:
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)。

浙公网安备 33010602011771号