自定义迭代器与运算符重载:重载++、*、->、!=等实现自定义迭代器

迭代器,说白了就是让自定义类型也能像指针一样遍历。我刚开始学C++时,总觉得迭代器是STL容器才有的东西,直到有一次我需要遍历一个自定义的矩阵类……嗯,那会儿我才意识到,自己写迭代器其实没那么玄乎。

为什么需要自定义迭代器?

你想想看,标准库里的vector、list都有begin()和end(),用起来多顺手。但如果你自己写了一个容器,比如一个环形缓冲区、一个跳表,或者一个二叉树,用户怎么遍历?总不能每次都暴露内部指针吧?

自定义迭代器就是为了解决这个问题。它让你的自定义类型也能用范围for循环,也能用STL算法。说白了,就是让你的类看起来像个正经容器。

核心思想:迭代器是容器和算法之间的桥梁。你只需要重载几个关键运算符,就能让自定义类型融入C++标准库的生态。

迭代器需要重载哪些运算符?

一个可用的前向迭代器,至少需要重载这四个运算符:

运算符 作用 典型签名
operator++ 前进到下一个元素 Iterator& operator++();
operator* 解引用,获取当前元素 T& operator*();
operator-> 成员访问,等价于指针 T* operator->();
operator!= 比较两个迭代器是否不等 bool operator!=(const Iterator& other);

我在项目中遇到过只重载了++*就交差的代码,结果用户想用!=判断循环结束,编译直接报错。所以这四个运算符,一个都不能少。

实战:为自定义数组实现迭代器

咱们直接上代码。假设我有一个简单的数组类,我想让它支持范围for循环:

template<typename T, size_t N>
class MyArray {
private:
    T data[N];
    
public:
    // 迭代器类定义在容器内部
    class Iterator {
    private:
        T* ptr;
    public:
        explicit Iterator(T* p) : ptr(p) {}
        
        // 前置++
        Iterator& operator++() {
            ++ptr;
            return *this;
        }
        
        // 后置++(虽然不常用,但最好也提供)
        Iterator operator++(int) {
            Iterator tmp = *this;
            ++ptr;
            return tmp;
        }
        
        // 解引用
        T& operator*() {
            return *ptr;
        }
        
        // 成员访问
        T* operator->() {
            return ptr;
        }
        
        // 不等比较
        bool operator!=(const Iterator& other) const {
            return ptr != other.ptr;
        }
        
        // 相等比较(通常也需要)
        bool operator==(const Iterator& other) const {
            return ptr == other.ptr;
        }
    };
    
    Iterator begin() { return Iterator(data); }
    Iterator end()   { return Iterator(data + N); }
};

你看,代码其实挺直白的。Iterator内部就存了一个裸指针,所有运算符重载都是对这个指针的操作。但关键就在于——用户不需要知道内部有指针,他们只需要用for (auto& x : arr)就行了。

小技巧:后置++通常用前置++来实现,这样代码更简洁,也避免重复逻辑。我习惯把后置++写成Iterator tmp = *this; ++(*this); return tmp;

使用示例

有了上面的迭代器,我们就可以这样用:

MyArray<int, 5> arr = {10, 20, 30, 40, 50};

// 范围for循环
for (auto& x : arr) {
    std::cout << x << " ";
}
// 输出:10 20 30 40 50

// 手动使用迭代器
auto it = arr.begin();
while (it != arr.end()) {
    std::cout << *it << " ";
    ++it;
}

这两种写法效果完全一样。但范围for循环更安全,因为迭代器的生命周期由编译器管理,你不用担心忘记++导致死循环。

避坑指南:迭代器失效

我曾经在项目里犯过一个低级错误:在遍历容器的同时修改了容器大小,结果迭代器全部失效,程序直接崩溃。迭代器失效是C++里最隐蔽的bug之一,尤其是当你自定义迭代器时,一定要明确你的迭代器在什么情况下会失效。

常见的迭代器失效场景:

  • 容器重新分配内存(比如vector扩容)
  • 插入或删除元素导致内部结构变化
  • 迭代器指向的元素被销毁

对于自定义迭代器,我建议你在文档里明确写清楚:哪些操作会导致迭代器失效。比如上面那个MyArray,因为底层是固定数组,所以迭代器永远不会失效——除非你手动delete了数组指针。

SVG:自定义迭代器的核心逻辑

下面这张图展示了自定义迭代器的整体流程,从容器到迭代器再到遍历:

自定义迭代器核心逻辑 自定义容器 MyArray<T, N> 内部存储:T data[N] begin()/end() 自定义迭代器 Iterator 内部指针:T* ptr 遍历 遍历结果 元素序列 10, 20, 30... 必须重载的运算符 • operator++() → 前进到下一个元素 • operator*() → 解引用,获取当前元素 • operator->() → 成员访问,等价于指针 • operator!=() → 比较两个迭代器是否不等 • operator==() → 比较两个迭代器是否相等

进阶:让迭代器更强大

上面我们实现的是最基础的前向迭代器。如果你想支持更多功能,比如反向遍历、随机访问,那还需要重载更多运算符:

  • 双向迭代器:额外重载operator--(前置和后置)
  • 随机访问迭代器:重载operator+operator-operator[]operator<

我个人习惯是,先实现最基础的前向迭代器,等确实需要更高级功能时再扩展。不要一开始就想着把所有运算符都重载一遍,那样容易把自己绕晕。

一个小建议:如果你想让迭代器兼容STL算法,最好从std::iterator_traits继承,或者手动定义iterator_categoryvalue_type等类型别名。这样std::sortstd::find这些算法才能正确识别你的迭代器类型。

总结

自定义迭代器其实就三件事:

  1. 在容器内部定义一个迭代器类
  2. 重载++*->!=这四个关键运算符
  3. 在容器中提供begin()end()方法

嗯,就这么简单。但要注意迭代器失效的问题,这个坑我踩过,希望你别踩。


公众号:蓝海资料掘金营,微信deep3321