【c++面向对象编程】第23篇:自增/自减运算符重载:前置与后置的区别
目录
一、一个让人困惑的地方
对于内置类型,前置和后置的行为不同:
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 禁止隐式转换?下篇揭晓。
更多推荐



所有评论(0)