目录
一、一个让人困惑的地方
二、前置和后置的区别
标准实现范式
三、为什么前置返回引用,后置返回值?
前置返回引用的原因
后置返回值的原因
四、性能差异
五、完整例子:迭代器风格的计数器
六、自减运算符的实现
七、常见错误
1. 后置返回引用
2. 前置返回void
3. 忘记处理自减时的边界
4. 后置不借助前置实现(代码重复)
八、选择原则
STL中的例子
九、这一篇的收获
一、一个让人困惑的地方
对于内置类型,前置和后置的行为不同:
cpp
int a = 5, b = 5; int x = ++a; // 前置:a变成6,x=6 int y = b++; // 后置:b变成6,y=5
当你给自己的类重载++时,也需要区分这两种行为。但函数名都是operator++,编译器怎么知道你要重载哪一个?
答案:C++通过参数列表来区分:
前置:
operator++()后置:
operator++(int)—— 那个int只是个占位符,永远不会被使用
cpp
class Counter { public: Counter& operator++(); // 前置++ Counter operator++(int); // 后置++(参数int不用写名字) };二、前置和后置的区别
| 特性 | 前置++obj | 后置obj++ |
|---|---|---|
| 函数签名 | T& operator++() | T operator++(int) |
| 参数 | 无 | 一个int占位符 |
| 返回值 | 引用(通常是*this) | 值(修改前的副本) |
| 语义 | 先修改,后返回 | 先返回旧值,后修改 |
| 性能 | 高效(无拷贝) | 较低(需要临时对象) |
标准实现范式
cpp
class Counter { private: int value; public: Counter(int v = 0) : value(v) {} // 前置++:先加,后返回自身 Counter& operator++() { ++value; // 修改自身 return *this; // 返回引用 } // 后置++:先保存副本,再加,返回副本 Counter operator++(int) { Counter old = *this; // 保存旧值 ++(*this); // 调用前置++(复用代码) return old; // 返回旧值(副本) } friend ostream& operator<<(ostream& os, const Counter& c) { os << c.value; return os; } };关键点:后置版本通常复用前置版本的实现,避免重复代码。
三、为什么前置返回引用,后置返回值?
前置返回引用的原因
cpp
Counter c(5); ++++c; // 合法:先执行 ++(++c)
如果前置返回void或值,第二次++就没有对象可操作了。返回引用保证了链式调用的正确性。
后置返回值的原因
cpp
Counter c(5); c++ = Counter(10); // 对临时对象赋值,没有意义,但能编译
后置返回的是修改前的临时副本,这个副本是纯右值(即将消亡的值),对它继续操作通常没有意义,所以不需要返回引用。
四、性能差异
cpp
void test() { Counter c; for (int i = 0; i < 1000000; i++) { ++c; // 前置:只修改自身 // c++; // 后置:每次循环都创建一个临时对象 } }后置开销:
拷贝构造临时对象
old修改
*this返回
old时可能还有一次拷贝(被优化后通常只有一次)
前置开销:
直接修改
*this返回引用
结论:如果不需要旧值,优先使用前置版本。这是C++的一个常见优化建议。
五、完整例子:迭代器风格的计数器
cpp
#include <iostream> #include <string> using namespace std; class Index { private: int pos; string name; public: Index(int p = 0, const string& n = "") : pos(p), name(n) {} // 前置++ Index& operator++() { ++pos; return *this; } // 后置++ Index operator++(int) { Index old = *this; ++(*this); // 复用前置 return old; } // 前置-- Index& operator--() { --pos; return *this; } // 后置-- Index operator--(int) { Index old = *this; --(*this); return old; } int getPos() const { return pos; } friend ostream& operator<<(ostream& os, const Index& idx) { os << idx.name << "[" << idx.pos << "]"; return os; } }; int main() { cout << "=== 基本测试 ===" << endl; Index i(5, "item"); cout << "初始: " << i << endl; cout << "前置++: " << ++i << endl; cout << "后置++: " << i++ << " (返回旧值)" << endl; cout << "最终: " << i << endl; cout << "\n=== 链式调用测试 ===" << endl; Index j(0, "counter"); cout << "前置链式: " << ++(++j) << endl; cout << "\n=== 性能对比演示 ===" << endl; Index k; for (int n = 0; n < 5; n++) { cout << "k" << (n == 0 ? "" : "++") << " = " << k++; cout << " (k=" << k << ")" << endl; } return 0; }输出:
text
=== 基本测试 === 初始: item[5] 前置++: item[6] 后置++: item[6] (返回旧值) 最终: item[7] === 链式调用测试 === 前置链式: counter[2] === 性能对比演示 === k = counter[0] (k=counter[1]) k++ = counter[1] (k=counter[2]) k++ = counter[2] (k=counter[3]) k++ = counter[3] (k=counter[4]) k++ = counter[4] (k=counter[5])
六、自减运算符的实现
自减与自增完全对称:
cpp
class Counter { int value; public: // 前置-- Counter& operator--() { --value; return *this; } // 后置-- Counter operator--(int) { Counter old = *this; --(*this); return old; } };七、常见错误
1. 后置返回引用
cpp
Counter& operator++(int) { // ❌ 返回局部对象的引用 Counter old = *this; ++(*this); return old; // 悬空引用! }2. 前置返回void
cpp
void operator++() { ++value; } // ❌ 不能链式调用3. 忘记处理自减时的边界
cpp
Index& operator--() { --pos; // 如果pos是0,减到-1,可能不是你想要的 return *this; }考虑是否需要检查下界。
4. 后置不借助前置实现(代码重复)
cpp
Counter operator++(int) { Counter old = *this; ++value; // ❌ 直接修改了成员,而不是调用前置 return old; }应该调用++(*this)复用前置的逻辑。
八、选择原则
| 场景 | 推荐 |
|---|---|
| 不需要旧值 | 用前置(++obj) |
需要旧值(如arr[i++]) | 用后置(obj++) |
| 实现后置时 | 复用前置 |
| 写通用模板代码 | 如果只是增加,用前置(效率更高) |
STL中的例子
cpp
vector<int> v = {1,2,3}; auto it = v.begin(); *it++; // 后置:先取*it,然后it指向下一个 *(++it); // 前置:it先指向下一个,再取值九、这一篇的收获
你现在应该理解:
前置:
T& operator++(),先修改,返回自身引用后置:
T operator++(int),保存副本,修改,返回旧副本int参数:纯占位符,区分前置和后置,永远用不到它的值
性能:前置不产生临时对象,比后置高效;无需旧值时优先前置
实现复用:后置通过调用前置实现,避免重复代码
💡 小作业:实现一个
CircularIndex类,索引在[0, size-1]范围内循环。重载前置/后置++和--,当超出边界时回到另一端。实现<<输出当前值。测试循环行为。
下一篇预告:第24篇《类型转换运算符:自定义隐式转换与explicit》——如何让自定义类型隐式转换成其他类型?operator int()这种写法是干什么的?为什么有时候需要explicit禁止隐式转换?下篇揭晓。