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;
}

这将输出:

image

后缀递增与递减

后缀递增/递减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;
}

这将输出:

image

让我们更详细地分析第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;
}

这将产生以下输出:

image

在第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;
}

image

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)。

posted @ 2026-02-19 09:36  游翔  阅读(0)  评论(0)    收藏  举报