C++ 递增递减运算符完全指南:前缀、后缀用法与副作用详解

递增和递减变量

递增(给变量加 1)和递减(给变量减 1)操作非常常见,以至于它们有自己专用的运算符。

运算符符号形式操作
前缀递增 (pre-increment)++++x先递增 x,然后返回 x
前缀递减 (pre-decrement)––––x先递减 x,然后返回 x
后缀递增 (post-increment)++x++复制 x,然后递增 x,最后返回该副本
后缀递减 (post-decrement)––x––复制 x,然后递减 x,最后返回该副本

注意,每个运算符都有两个版本——前缀版本(运算符在操作数之前)和后缀版本(运算符在操作数之后)。

前缀递增和递减

前缀递增/递减运算符非常直接。首先,操作数被递增或递减,然后表达式求值为操作数的值。例如:

#include <iostream>

int main()
{
    int x { 5 };
    int y { ++x }; // x 递增为 6,x 被求值为值 6,然后 6 被赋值给 y

    std::cout << x << ' ' << y << '\n';
    return 0;
}

这将打印:

6 6

后缀递增和递减

后缀递增/递减运算符则更复杂一些。首先,创建操作数的一个副本。然后操作数(不是副本)被递增或递减。最后,副本(不是原始值)被求值。例如:

#include <iostream>

int main()
{
    int x { 5 };
    int y { x++ }; // x 递增为 6,原始 x 的副本被求值为值 5,然后 5 被赋值给 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'; // 前缀版本
    std::cout << x << ' ' << y << '\n';
    std::cout << x++ << ' ' << y-- << '\n'; // 后缀版本
    std::cout << x << ' ' << y << '\n';

    return 0;
}

这将产生输出:

5 5
6 4
6 4
6 4
7 3

在第 8 行(std::cout << ++x << ' ' << --y << '\n';),我们执行前缀递增和递减。在这一行,xy 在它们的值被发送到 std::cout 之前就被递增/递减了,因此我们看到 std::cout 反映了它们更新后的值。

在第 10 行(std::cout << x++ << ' ' << y-- << '\n';),我们执行后缀递增和递减。在这一行,xy 的副本(带有递增前和递减前的值)被发送到 std::cout,因此我们在这里没有看到递增和递减的效果。这些更改直到下一行(当 xy 再次被求值时)才显现出来。

何时使用前缀与后缀

在许多情况下,前缀和后缀运算符会产生相同的行为:

int main()
{
    int x { 0 };
    ++x; // 将 x 递增为 1 (前缀)
    x++; // 将 x 递增为 2 (后缀)

    return 0;
}

在代码可以编写为使用前缀或后缀的情况下,优先使用前缀版本,因为它们通常性能更好,并且不太可能引起意外。

最佳实践 (Best practice)

  • 优先使用前缀版本,因为它们性能更好且不太可能引起意外。
  • 仅当使用后缀版本能产生比使用前缀版本等效代码显著更简洁或更易理解的代码时,才使用后缀版本。

副作用 (Side effects)

如果一个函数或表达式除了产生返回值之外还具有某种可观察的效果,则称其具有副作用

副作用的常见例子包括:更改对象的值、执行输入或输出、更新图形用户界面(例如启用或禁用按钮)。

大多数时候,副作用是有用的:

x = 5; // 赋值运算符具有改变 x 值的副作用
++x; // operator++ 具有递增 x 的副作用
std::cout << x; // operator<< 具有修改控制台状态的副作用

上面例子中的赋值运算符具有永久改变 x 值的副作用。即使在语句执行完毕后,x 的值仍然是 5。类似地,operator++ 会改变 x 的值,即使在语句求值完成后。输出 x 也具有修改控制台状态的副作用,因为你现在可以在控制台上看到 x 的值被打印出来。

关键见解 (Key insight)

赋值运算符、前缀运算符和后缀运算符具有永久改变对象值的副作用。 其他运算符(如算术运算符)返回一个值,并且不修改它们的操作数

副作用可能导致求值顺序问题

在某些情况下,副作用可能导致求值顺序问题。例如:

#include <iostream>

int add(int x, int y)
{
    return x + y;
}

int main()
{
    int x { 5 };
    int value{ add(x, ++x) }; // 未定义行为:这是 5 + 6,还是 6 + 6?
    // 这取决于你的编译器以何种顺序求值函数参数

    std::cout << value << '\n'; // value 可能是 11 或 12,取决于上面一行的求值方式!

    return 0;
}

C++ 标准没有定义函数参数的求值顺序。如果左参数先求值,这就变成了调用 add(5, 6),结果等于 11。如果右参数先求值,这就变成了调用 add(6, 6),结果等于 12!注意,这之所以是个问题,只是因为函数 add() 的一个参数具有副作用。

顺便提一下… (As an aside…)

C++ 标准有意不定义这些内容,以便编译器可以为给定的体系结构做最自然(因而性能最佳)的处理。

副作用的时序 (The sequencing of side effects)

在许多情况下,C++ 也没有规定运算符的副作用必须在何时生效。这可能导致在同一个语句中多次使用一个应用了副作用的对象时,产生未定义行为。

例如,表达式 x + ++x未指定行为 (unspecified behavior)。当 x 初始化为 1 时,Visual Studio 和 GCC 将其求值为 2 + 2,而 Clang 将其求值为 1 + 2!这是由于编译器在何时应用递增 x 的副作用存在差异。

即使 C++ 标准明确规定了应该如何求值,历史上这一直是存在许多编译器错误的领域。通过确保任何应用了副作用的变量在同一个语句中最多只使用一次,通常可以避免所有这些问题。

警告 (Warning)

C++ 没有定义函数参数或运算符操作数的求值顺序。

警告 (Warning)

不要在一个给定的语句中多次使用一个应用了副作用的变量。如果这样做,结果可能是未定义行为 (undefined behavior)

一个例外是简单的赋值表达式,如 x = x + y(这本质上等同于 x += y)。


关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"