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