8. 重载自增自减运算符:前置与后置的语法区分
自增(++)和自减(--)运算符,在 C++ 里算是比较特殊的一对。为什么?因为它们有前置和后置两种写法,而且语义完全不同。你想想看,++i 和 i++,一个先加后用,一个先用后加。放到自定义类型里,编译器怎么知道你要重载的是哪一个?
嗯,这里就是关键了。C++ 设计者想了个巧妙的办法:通过一个额外的 int 参数来区分。前置版本没有参数,后置版本带一个 int 参数(这个参数在函数体内用不到,纯粹是为了区分)。
核心规则:
- 前置 ++/--:
T& operator++();或T& operator--(); - 后置 ++/--:
T operator++(int);或T operator--(int);
8.1 前置版本:返回引用,效率更高
前置自增的逻辑很简单:先修改对象本身,然后返回修改后的引用。我在项目中处理自定义迭代器时,前置版本用得最多,因为它没有临时对象的拷贝。
class Counter {
private:
int value_;
public:
Counter(int v = 0) : value_(v) {}
// 前置 ++
Counter& operator++() {
++value_;
return *this; // 返回自身引用
}
// 前置 --
Counter& operator--() {
--value_;
return *this;
}
int get() const { return value_; }
};
调用的时候,++c 直接修改 c 的值,然后返回 c 本身。没有拷贝,没有临时对象。说白了,这就是你想要的“原地操作”。
8.2 后置版本:返回副本,注意性能
后置版本就有点“绕”了。它需要先保存当前状态,再修改对象,最后返回保存的那个副本。我刚开始学的时候总觉得奇怪:为什么后置要返回一个副本而不是引用?
原因很简单:后置的语义是“先用后改”。你拿到的是修改前的值,而对象本身已经变了。所以必须拷贝一份旧值返回。
class Counter {
public:
// 后置 ++
Counter operator++(int) {
Counter temp = *this; // 保存当前状态
++value_; // 修改自身
return temp; // 返回旧值
}
// 后置 --
Counter operator--(int) {
Counter temp = *this;
--value_;
return temp;
}
};
我曾经踩过的坑:后置版本一定会产生临时对象。如果你在循环里用
for (auto it = v.begin(); it != v.end(); it++),每次迭代都会多一次拷贝。对于迭代器或复杂对象,性能损耗是实打实的。所以我个人习惯:能前置就别后置。
8.3 前置 vs 后置:一张图看懂
8.4 完整示例:一个可自增的计数器
把前置和后置放在一起对比,你就知道区别在哪了。
#include <iostream>
class Counter {
int val_;
public:
Counter(int v = 0) : val_(v) {}
// 前置
Counter& operator++() {
++val_;
return *this;
}
// 后置
Counter operator++(int) {
Counter old = *this;
++val_;
return old;
}
friend std::ostream& operator<<(std::ostream& os, const Counter& c) {
os << c.val_;
return os;
}
};
int main() {
Counter c(10);
std::cout << "初始值: " << c << "\n";
// 前置
std::cout << "++c: " << ++c << "\n"; // 输出 11
std::cout << "当前值: " << c << "\n"; // 输出 11
// 后置
std::cout << "c++: " << c++ << "\n"; // 输出 11(旧值)
std::cout << "当前值: " << c << "\n"; // 输出 12
return 0;
}
小技巧:如果你在实现一个迭代器类,建议把后置 ++ 直接复用前置 ++ 的实现,减少代码重复:
Counter operator++(int) { Counter old = *this; ++(*this); return old; }
8.5 自减运算符:完全对称
自减运算符的规则和自增一模一样。前置返回引用,后置返回副本。我就不重复贴代码了,你照着自增的写法把 ++ 换成 -- 就行。
注意:自减运算符在迭代器中要特别小心。如果你实现的是双向迭代器,自减必须保证能正确回退。我曾经在写一个链表迭代器时,后置自减忘了保存前驱节点,结果回退时直接崩了。嗯,调试了整整一个下午。
8.6 总结一下
| 特性 | 前置 ++/-- | 后置 ++/-- |
|---|---|---|
| 语法 | T& operator++() |
T operator++(int) |
| 返回值 | 引用(T&) | 值(T) |
| 临时对象 | 无 | 有(拷贝一次) |
| 语义 | 先修改,后使用 | 先使用,后修改 |
| 推荐场景 | 迭代器、循环控制 | 需要旧值的表达式 |
说白了,前置和后置的选择,本质上是在语义正确和性能之间做权衡。大多数情况下,前置版本就够用了。只有当你确实需要“先取值再自增”的语义时,才去用后置。
我个人习惯:写自定义类型时,永远先实现前置版本。后置版本用前置版本去实现,省事又安全。