HOWTO · C++

C++ 中的前递增 VS 后递增运算符

本文演示了如何在 C++ 中使用前自增 VS 后自增

本页内容

对于 C++ 内置算术类型的变量,++x 和 x++ 都会使 x 增加 1。区别在于表达式产生的值:前置递增产生更新后的值,后置递增产生更新前的值。应根据周围表达式需要哪个值来选择。对于用户定义类型,重载的递增运算符遵循各自的定义;按照惯例,它们通常也提供类似的前置和后置行为。

C++ 中前置递增与后置递增的区别

对于内置算术类型,++x 会先更新 x,再产生表达式的结果。x++ 会先产生旧值,并在该操作中完成递增。两种形式都会使变量改变一次。重载运算符可以有不同的细节,不过按照惯例,它们通常遵循相同的前置与后置区别。

运行以下示例:

#include <iostream>

int main() {
    int value = 4;
    int previous = value++;
    int current = ++value;

    std::cout << previous << ' ' << current << ' ' << value << '\n';
}
4 6 6

previous 接收第一次递增前的值;current 接收第二次递增后的值,这也是 value 的最终值。

在循环体中,递增的位置会改变循环体打印的值。第一个循环中,i++ 是打印表达式:它先打印当前值,再递增 i。第二个循环中,++j 在打印表达式之前执行,因此循环体打印的是更新后的值。

#include <iostream>

int main() {
    int i = 0;
    while (i < 5) {
        std::cout << i++ << '\n';
    }
    std::cout << "final i = " << i << '\n';
}
#include <iostream>

int main() {
    int j = 0;
    while (j < 5) {
        std::cout << ++j << '\n';
    }
    std::cout << "final j = " << j << '\n';
}

第一个循环依次打印 0、1、2、3 和 4,随后显示 final i = 5;第二个循环依次打印 1、2、3、4 和 5,随后显示 final j = 5。两个示例中的计数器都从 0 开始,循环体各执行五次,最终计数器都是 5。这些示例在循环体中递增。相比之下,如果递增操作位于 for 循环的迭代表达式中,它会在循环体执行后运行。当表达式结果被丢弃时,将 ++k 换成 k++ 不会改变迭代序列:for (int k = 0; k < 5; ++k) 及其使用 k++ 的版本都会遍历 0、1、2、3 和 4,结束时 k 都等于 5。输出块中的 final i 和 final j 标签保持英文。

不要在同一表达式中修改并读取同一个变量

不要在同一个未定序表达式中递增某个变量,同时又读取该变量。例如,k++ * k 在 C++ 中具有未定义行为:修改操作与另一次读取彼此之间没有先后顺序,也就是二者未定序。因此无法预测它的结果,也不能把它当作有效的示例输出。

如果要计算旧值与下一个值的乘积,应明确写出有顺序的步骤:初始化 k,将它保存到 old,递增 k,然后计算 old * k。

分开的语句会先保存旧值,再递增 k,然后将保存的值与更新后的值相乘。运行以下示例:

#include <iostream>

int main() {
    int k = 10;
    int old = k;
    ++k;
    int product = old * k;

    std::cout << product << '\n';
}
110

在迭代器和用户自定义类型上,选择有何不同

当递增在同一个表达式中与另一个操作组合时,值的区别最为重要。常见的情形是解引用:*it++ 的含义是 *(it++),即先读取迭代器当前指向的元素,然后让迭代器前进。相反,*++it 先前进,再读取新元素。在遍历中跳过或重复某个元素,往往正是这种前置与后置的混淆,而不是循环条件写错。

#include <iostream>
#include <vector>

int main() {
    std::vector<int> values{10, 20, 30};
    auto it = values.begin();

    std::cout << *it++ << '\n';
    std::cout << *it << '\n';
    std::cout << *++it << '\n';
}
10
20
30

第一行输出 10,并使迭代器指向 20;输出 *it 可以确认新位置,随后 *++it 再次前进并输出 30。

对于用户自定义类型,这两种形式是两个不同的函数。按惯例,前置递增声明为 operator++(),返回更新后对象的引用;后置递增声明为 operator++(int)——这个 int 只是占位标记,并不是调用方传入的值——它先保存旧状态的副本,再通过前置形式完成递增,最后按值返回该副本。

#include <iostream>

class Counter {
public:
    explicit Counter(int value) : value_(value) {}

    Counter& operator++() {
        ++value_;
        return *this;
    }

    Counter operator++(int) {
        Counter old = *this;
        ++(*this);
        return old;
    }

    int value() const { return value_; }

private:
    int value_;
};

int main() {
    Counter counter{5};
    Counter old = counter++;
    Counter& ref = ++counter;

    std::cout << old.value() << ' ' << counter.value() << ' '
              << ( &ref == &counter ) << '\n';
}
5 7 1

这里 old 保留了起始值 5,计数器本身在两次递增后结束于 7,最后的 1 证实前置递增返回的是计数器本身,而不是副本。同样的区别也解释了为什么 ++x 的结果仍然指代 x 本身,而 x++ 的结果是旧值的一个临时副本。

性能问题在这里也变得具体。对于结果被丢弃的内置整数,编译器通常为两种形式生成相同的代码,因此在这种场合偏好前置形式只是可读性惯例,并不是提速。后置递增中的副本只有在复制本身是真实工作时才有成本——例如会复制状态的用户自定义迭代器或计数器,或者做了不止加一的工作的重载运算符。如果某个类型只定义了其中一种形式,另一种就根本不可用;请查看该类型的声明,不要假定两者都存在。

何时使用前置递增或后置递增

当表达式需要旧值时,使用后置递增,例如 int previous = value++;。当需要更新后的值,或者表达式的值会被丢弃时,使用前置递增;后一种情况例如 for 循环的迭代表达式。

当不需要旧迭代器值时,前置递增是常见的约定。用户定义迭代器的重载运算符遵循其具体实现,不过按照惯例通常会对应内置运算符的前置与后置区别。后置递增可能会保留并返回旧迭代器的副本;对于复制有成本的迭代器类型,前置递增可以避免这部分工作。但这不能证明某个具体程序一定会有可测量的速度差异:编译器优化和迭代器实现都会影响生成的工作量。

总结

两种形式都会使变量递增一次。前置递增产生更新后的值,后置递增产生先前的值。选择能清楚表达所需值的形式;对于迭代器,如果不会使用旧迭代器值,通常优先使用前置递增,但不要据此假设一定能提速。

常见问题

前置递增与后置递增的主要区别是什么?

对于内置算术类型,++x 会先更新变量,再产生更新后的值。x++ 会产生先前的值,并完成递增。重载运算符遵循各自的定义,不过按照惯例通常也提供类似的前置和后置行为。

在循环中可以使用任意一种形式吗?

可以。在 for 循环的迭代表达式中,结果通常会被丢弃,因此将 ++i 换成 i++ 不会改变该循环的迭代序列。

在 C++ 中哪种形式更快?

对于所有表达式和类型,并不存在通用的速度排名。对于迭代器或用户定义类型,后置递增可能涉及临时副本;优化可能会消除不必要的工作。应根据所需的值和清晰表达意图来选择,并在作出性能判断前测量实际工作负载。

后置递增何时有用?

当表达式需要递增前的值时使用,例如将该值赋给另一个变量。

前置递增和后置递增在所有语言中的行为都一样吗?

不一样。拼写相似的运算符也可能遵循各自语言的规则。请查阅所用语言的语义;本文的说明针对 C++。