目录

一、一个让人困惑的地方

二、前置和后置的区别

标准实现范式

三、为什么前置返回引用,后置返回值?

前置返回引用的原因

后置返回值的原因

四、性能差异

五、完整例子:迭代器风格的计数器

六、自减运算符的实现

七、常见错误

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++; // 后置:每次循环都创建一个临时对象
    }
}

后置开销

  1. 拷贝构造临时对象 old

  2. 修改 *this

  3. 返回 old 时可能还有一次拷贝(被优化后通常只有一次)

前置开销

  1. 直接修改 *this

  2. 返回引用

结论:如果不需要旧值,优先使用前置版本。这是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 禁止隐式转换?下篇揭晓。

更多推荐