
前言i与i都让i加一差别在表达式的值前置返回加一之后的值后置返回加一之前的旧值。这个差别看起来很小但在重载运算符、写迭代器、以及阅读*it这类惯用法时它决定了代码是否正确。第一个典型误解是「i一定比i慢」。对内置的int、指针来说如果旧值没被使用编译器通常会把两者优化成同一条自增指令标准也不保证谁更快。真正有差别的是类类型后置版本按约定要返回旧值往往需要先拷贝一个对象再自增再返回副本。第二个误解是「前置和后置的重载签名一样」。为了区分两者C 规定后置版本带一个额外的int形参调用时编译器自动传0这个参数在函数体里不应被使用。少了它两个版本就是同一个签名无法共存。第三个误解是「i和i等价」。对内置类型i无法编译后置结果是右值不能再自增而i合法前置结果是左值两者含义并不相同。对重载了的类类型如果后置返回的是值c可能通过编译但它修改的是一个临时对象结果没有意义。本文从内置类型的行为讲起给出前置/后置的标准重载形式、迭代器里的实际差异以及几个容易踩的坑。一、内置类型值语义的差别// 适用于 C17#include iostreamint main() {int i 5;int a i; // i 先变成 6a 得到 6int b i; // b 得到 6i 再变成 7std::cout i a b \n;}按标准语义输出应当是7 6 6。这里没有编译运行环境结论来自语言规则前置自增的值是自增后的左值后置自增的值是自增前的右值副本。对内置类型前置表达式是左值可以取地址、可以赋值后置表达式是右值不能取地址。这条区别在模板推导和decltype里会体现出来// 适用于 C17int i 0;decltype(i) x i; // int前置是左值decltype(i) y 0; // int后置是右值y的类型是int而不是int因为decltype对右值表达式给出的是值类型。这个细节说明后置自增产生的是一个独立的值而不是对原对象的引用。二、重载两种标准签名对类类型重载自增运算符惯例写法是// 适用于 C17class Counter {public:explicit Counter(int v 0) : v_(v) {}// 前置先加再返回自身引用Counter operator() {v_;return *this;}// 后置先存旧值再加返回旧值Counter operator(int) {Counter old(*this);v_;return old;}int value() const { return v_; }private:int v_;};几个必须遵守的约定前置返回Counter这样(c)能连续调用且不产生额外拷贝。后置返回Counter按值因为要返回自增前的状态不可能返回引用——那个状态在自增后已经不存在了。后置的int参数只是区分标记不命名也可以Counter operator(int)。命名了却不用编译器可能给出未使用参数警告。两个版本通常写成成员函数。非成员重载在语法上可行但要接收类类型参数还会牵扯到隐式转换工程上很少这么写。前置和后置的语义必须一致后置返回的旧值应当等于调用前对象的值前置返回的引用应当指向自增后的对象。写反了不会编译报错但所有依赖it的代码都会出错。三、迭代器为什么循环里推荐前置标准库迭代器的前置自增返回引用后置自增返回一个完整的迭代器副本。对一个std::vectorint::iterator这个副本通常只是一个指针代价很小但对std::map的迭代器、或者节点式容器的迭代器副本可能包含更多状态。// 适用于 C17#include vectorvoid walk(const std::vectorint v) {for (auto it v.begin(); it ! v.end(); it) { // 推荐前置// 使用 *it}for (auto it v.begin(); it ! v.end(); it) { // 可用但多一次副本// 使用 *it}}如果循环体里不使用it的旧值后置自增的那次拷贝就是纯粹的浪费。编译器有时能把它优化掉但不能保证——尤其当迭代器的operator(int)定义在另一个翻译单元、没有内联时。两个常见惯用法需要后置*it先解引用旧位置再把迭代器前移。等价于*(it)因为后置的旧值就是当前元素的位置。v.erase(it)把旧迭代器传给erase同时让it前移。注意对std::vectorerase会使被删元素之后的迭代器失效这种写法在vector上并不安全它是为节点式容器list、map准备的惯用法。C11 起更清晰的写法是it v.erase(it)。反过来*it是先前移再解引用跳过当前元素。(*it)是自增迭代器指向的元素不是迭代器。这三种写法只差括号位置语义完全不同。四、运算符重载的返回值与链式调用前置返回引用使c合法第一次c返回c的引用第二次再对它自增。后置返回值c在某些实现里也能编译第一次返回一个临时Counter第二次对临时对象调用后置自增然后临时对象被销毁。它能通过编译但没有任何可观察的效果应该视为错误。// 适用于 C17Counter c(0);c; // 合法c 变成 2// c; // 能编译后置返回非 const 值但改的是临时对象无意义早期有些教程建议后置返回const Counter用const临时对象阻止c编译。这种做法在现代 C 里不再推荐它会影响移动语义也让返回值无法参与某些表达式。正确做法是不要写c。对内置类型i合法i是左值i不合法i是右值内置类型的自增要求左值。这与类类型的行为不同是因为内置运算符不走重载决议。完整示例一个可遍历的整数区间下面的例子同时实现前置和后置并用范围 for 演示迭代器协议// 适用于 C17#include iostreamclass IntRange {public:class iterator {public:explicit iterator(int v) : v_(v) {}int operator*() const { return v_; }iterator operator() { // 前置v_;return *this;}iterator operator(int) { // 后置iterator old(*this);v_;return old;}bool operator!(const iterator other) const {return v_ ! other.v_;}private:int v_;};explicit IntRange(int n) : n_(n) {}iterator begin() const { return iterator(0); }iterator end() const { return iterator(n_); }private:int n_;};int main() {IntRange range(5);for (int x : range) {std::cout x ;}auto it range.begin();auto old it; // old 指向 0it 指向 1std::cout \n *old *it \n;}范围 for 在标准里被规定为使用前置推进迭代器所以只要实现了begin()、end()、operator!、operator*和前置operator它就能工作。后置版本在这个例子里只用于演示it的旧值语义。常见坑点1. 后置返回引用❌Counter operator(int) { Counter old(*this); v_; return old; }返回局部对象的引用。 ✅ 后置按值返回Counter前置才返回Counter。2. 前置返回副本❌Counter operator() { v_; return *this; }✅Counter operator()避免多余拷贝并支持链式调用。3. 两个版本签名相同❌ 前置和后置都写成Counter operator()。 ✅ 后置必须带int形参Counter operator(int)。4. 认为i对int一定更慢❌ 在普通int循环里为了「性能」把i改成i并声称有提升。 ✅ 对内置类型两者通常等价对迭代器等类类型才优先前置。不要给未测量的性能结论。5. 写i❌c;以为会自增两次。 ✅ 写c;或分两条c; c;。6. 在一个表达式里多次修改同一变量❌int i 0; int j i i;✅ 拆成两条语句同一表达式内对同一对象的多次未排序修改是 UB。7. 对bool使用❌bool b false; b;✅对bool在 C98 已弃用、C17 被移除用b true;。8. 在循环里对重量级迭代器用后置❌for (auto it m.begin(); it ! m.end(); it)循环体不使用旧值。 ✅ 写成it省掉一次迭代器拷贝。总结维度前置i后置i表达式的值自增后的值自增前的旧值值类别内置类型左值右值重载返回类型TT重载签名operator()operator(int)额外开销无通常一次对象拷贝典型用途循环推进、链式自增需要旧值的场合如*it判断该用哪一个只需要问一句这个表达式的旧值还有用吗没用就写前置既符合惯例也避免了类类型上的一次拷贝。真正需要后置的地方不多*it和少数「取出旧值再前移」的算法是主要场景。