C++ 迭代器

迭代器(Iterator)是连接容器与算法的 "桥梁",它提供了一种统一的方式访问容器中的元素,而无需暴露容器的内部实现。

不同容器的底层实现差异很大(如 vector 是动态数组,list 是双向链表),但迭代器屏蔽了这些差异,让算法(如 sortfor_each)可以用相同的方式处理任何容器。

算法只需依赖迭代器提供的接口,无需关心容器的具体类型,实现了 "泛型编程" 的核心思想。

一、迭代器分类

迭代器类型 支持的操作(核心) 对应容器示例
输入迭代器(Input Iterator) 只读访问(*it)、单向递增(++it)、比较相等(==/!= istream_iterator(输入流迭代器)
输出迭代器(Output Iterator) 只写访问(*it = value)、单向递增(++it ostream_iterator(输出流迭代器)
前向迭代器(Forward Iterator) 读写访问、单向递增(++it)、可重复遍历 unordered_mapunordered_set
双向迭代器(Bidirectional Iterator) 读写访问、双向移动(++it/--it)、可双向遍历 listmapset
随机访问迭代器(Random Access Iterator) 读写访问、随机访问(it + n/it - nit[n])、比较大小(</> vectordequearray
  • 输入 / 输出迭代器:功能最弱,仅支持单方向遍历和基本读写(输入只读,输出只写),适用于流操作。
  • 前向迭代器:支持重复遍历同一序列(可多次递增),但只能向前移动,适合单向链表类容器。
  • 双向迭代器 :在向前迭代的基础上支持向后移动(--it),适合双向链表 、**平衡树(如红黑树)**类容器。
  • 随机访问迭代器 :功能最强,支持直接跳转到任意位置(如 it += 5),访问效率与数组下标相当,适合连续内存 容器(如 vector)。

功能最弱到最强依次为:输入迭代器 → 输出迭代器 → 前向迭代器 → 双向迭代器 → 随机访问迭代器

1、输入迭代器(Input Iterator):只读、单向遍历

1.1 核心特性

  • 功能 :仅支持 "只读访问" 和 "单向递增"(++),无法修改元素,且遍历过程中元素可能失效(不能重复遍历)。
  • 关键操作*it(读元素)、++it(向后移动)、it1 == it2/it1 != it2(比较)。
  • 本质:"一次性读取工具",如从流中读取数据,读取后数据可能不再可用。

1.2 适用容器 / 场景

  • 标准库中的 istream_iterator(从输入流读取数据)、forward_list 的 const 迭代器(部分场景)。示例:用 istream_iterator 读取标准输入

1.3 示例

istream_iterator 读取标准输入

cpp 复制代码
#include <iostream>
#include <iterator> // 包含 istream_iterator
#include <vector>
using namespace std;

int main() {
    cout << "输入多个整数(按 Ctrl+D 结束):" << endl;
    // istream_iterator<int> 是输入迭代器,从 cin 读取 int
    istream_iterator<int> input_begin(cin); 
    istream_iterator<int> input_end; // 尾后迭代器(标记结束)

    vector<int> nums;
    // 遍历输入流,读取所有整数(输入迭代器仅支持 ++ 和 *)
    for (auto it = input_begin; it != input_end; ++it) {
        nums.push_back(*it); // *it 读取当前元素(只读,不能修改)
    }

    // 输出结果
    cout << "你输入的整数:";
    for (int num : nums) {
        cout << num << " ";
    }
    return 0;
}

输出

复制代码
输入多个整数(按 Ctrl+D 结束):
10 20 30 40
你输入的整数:10 20 30 40

关键注意点

  • 输入迭代器不能重复遍历 :若再次 ++it 回到之前的位置,*it 结果未定义(如流已读取过数据,无法回溯)。
  • 仅支持 "只读":*it = 5 编译错误,无法修改元素。

2、输出迭代器(Output Iterator):只写、单向遍历

2.1 核心特性

  • 功能 :仅支持 "只写访问" 和 "单向递增"(++),无法读取元素,用于将数据写入目标(如流、容器)。
  • 关键操作*it = value(写元素)、++it(向后移动)。
  • 本质:"一次性写入工具",写入后无法读取,且每个位置仅能写入一次。

2.2 适用容器 / 场景

  • 标准库中的 ostream_iterator(向输出流写入数据)、back_inserter(向容器尾部插入元素)。

2.3 示例

ostream_iterator 写入标准输出

cpp 复制代码
#include <iostream>
#include <iterator> // 包含 ostream_iterator
#include <vector>
#include <algorithm> // 包含 copy
using namespace std;

int main() {
    vector<int> nums = {1, 2, 3, 4, 5};

    // ostream_iterator<int> 是输出迭代器,向 cout 写入 int(分隔符为 ", ")
    ostream_iterator<int> output(cout, ", ");

    // 用 copy 算法:将 nums 的元素通过输出迭代器写入 cout
    copy(nums.begin(), nums.end(), output); 
    return 0;
}

输出

plaintext 复制代码
1, 2, 3, 4, 5, 

关键注意点

  • 输出迭代器不能读取cout << *output 编译错误,无法解引用读取。
  • 每个位置仅能写入一次:若对同一迭代器位置多次 *it = value,结果未定义(可能覆盖或失效)。

3. 前向迭代器(Forward Iterator):读写、单向重复遍历

3.1 核心特性

  • 功能 :支持 "读写访问" 和 "单向递增"(++),可重复遍历同一序列(元素不会因遍历失效)。
  • 关键操作*it(读写元素)、++it/it++(向后移动)、it1 == it2/it1 != it2(比较)。
  • 本质 :"单向可复用工具",比输入 / 输出迭代器灵活,但无法反向移动(无 --)。

3.2 适用容器 / 场景

  • 无序关联容器:unordered_mapunordered_set(底层哈希表,迭代器仅支持单向移动)。
  • forward_list(单向链表,仅支持前向迭代)。

3.3 示例

遍历 unordered_map(前向迭代器)

cpp 复制代码
#include <iostream>
#include <unordered_map>
using namespace std;

int main() {
    unordered_map<string, int> score = {
        {"Alice", 95}, {"Bob", 88}, {"Charlie", 92}
    };

    // unordered_map 的迭代器是前向迭代器,支持 ++ 和读写
    auto it = score.begin();
    while (it != score.end()) {
        // 读写操作:修改 Bob 的分数(*it 是 pair<const string, int>)
        if (it->first == "Bob") {
            it->second = 90; // 写操作:修改 value
        }
        // 读操作:输出键值对
        cout << it->first << ": " << it->second << endl;
        ++it; // 仅支持单向递增(无 --)
    }

    // 重复遍历(前向迭代器支持复用)
    cout << "\n重复遍历:";
    for (auto it = score.begin(); it != score.end(); ++it) {
        cout << it->first << " ";
    }
    return 0;
}

输出

plaintext 复制代码
Bob: 90
Alice: 95
Charlie: 92

重复遍历:Bob Alice Charlie 

关键注意点

  • 不支持反向移动:--it 编译错误,只能 ++ 向后移动。
  • 无序容器的遍历顺序不固定:unordered_map 是哈希表,迭代器遍历顺序与插入顺序无关。

4. 双向迭代器(Bidirectional Iterator):读写、双向遍历

核心特性

  • 功能 :在 "前向迭代器" 基础上,支持 "反向移动"(--),可双向遍历序列。
  • 关键操作 :包含前向迭代器的所有操作 + --it/it--(向前移动)。
  • 本质:"双向可复用工具",适合需要反向遍历的场景(如链表)。

适用容器 / 场景

  • 双向链表:list
  • 有序关联容器:mapset(底层红黑树,支持双向移动)。

示例

遍历 list(双向迭代器,支持反向遍历)

cpp 复制代码
#include <iostream>
#include <list>
using namespace std;

int main() {
    list<int> nums = {10, 20, 30, 40};

    // 1. 正向遍历(++)
    cout << "正向遍历:";
    auto it = nums.begin();
    while (it != nums.end()) {
        *it += 5; // 写操作:每个元素加 5
        cout << *it << " ";
        ++it;
    }
    cout << endl;

    // 2. 反向遍历(--)
    cout << "反向遍历:";
    auto rit = nums.end();
    --rit; // 从尾后迭代器向前移动到最后一个元素
    while (true) {
        cout << *rit << " ";
        if (rit == nums.begin()) break;
        --rit; // 反向移动
    }
    return 0;
}

输出

plaintext 复制代码
正向遍历:15 25 35 45 
反向遍历:45 35 25 15 

关键注意点

  • 支持反向遍历,但不支持随机访问it + 2 编译错误,无法直接跳转到指定位置。
  • list 的迭代器在插入 / 删除时稳定性高:插入元素时所有迭代器有效,删除元素时仅被删元素的迭代器失效。

5. 随机访问迭代器(Random Access Iterator):读写、随机访问

核心特性

  • 功能:在 "双向迭代器" 基础上,支持 "随机访问"(直接跳转到任意位置),是功能最强的迭代器。
  • 关键操作 :包含双向迭代器的所有操作 + 算术运算(it + n/it - n)、下标访问(it[n])、大小比较(it1 < it2/it1 > it2)。
  • 本质:"数组式访问工具",效率与原生指针相当,适合连续内存容器。

适用容器 / 场景

  • 连续内存容器:vector(动态数组)、deque(双端队列,分段连续)、array(固定大小数组)。

示例

vector` 的随机访问迭代器

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> nums = {1, 2, 3, 4, 5};
    auto it = nums.begin();

    // 1. 随机访问:直接跳转到第 3 个元素(索引 2)
    it += 2;
    cout << "第 3 个元素:" << *it << endl; // 输出 3

    // 2. 算术运算:计算迭代器距离
    auto end_it = nums.end();
    cout << "容器大小:" << end_it - it << endl; // 输出 3(it 到 end_it 有 3 个元素)

    // 3. 下标访问:等价于 *(it + n)
    cout << "it[1] = " << it[1] << endl; // 输出 4(it+1 指向的元素)

    // 4. 大小比较:判断迭代器位置
    if (it < nums.end()) {
        cout << "it 在 end 之前" << endl;
    }

    // 5. 双向移动:++ 和 --
    ++it;
    cout << "it++ 后:" << *it << endl; // 输出 4
    --it;
    cout << "it-- 后:" << *it << endl; // 输出 3

    return 0;
}

输出

plaintext 复制代码
第 3 个元素:3
容器大小:3
it[1] = 4
it 在 end 之前
it++ 后:4
it-- 后:3

关键注意点

  • 随机访问迭代器可直接转为原生指针vector<int>::iterator 本质是 int*(多数编译器实现),因此效率极高。
  • 扩容时迭代器失效:vector 扩容会分配新内存,原迭代器指向的旧内存失效,需重新获取迭代器。

二、迭代器失效

迭代器失效是指迭代器指向的内存位置无效(如被释放、移动),访问失效迭代器会导致未定义行为(崩溃、数据错乱)。不同迭代器对应的容器,失效规则不同,核心与容器底层实现相关:

迭代器类型 对应容器 插入元素时失效规则 删除元素时失效规则
输入 / 输出迭代器 流、forward_list 所有迭代器失效(流数据一次性) 所有迭代器失效
前向迭代器 unordered_map、``unordered_set` 未扩容:有效;扩容:所有失效 仅被删除元素的迭代器失效
双向迭代器 listmapset 所有迭代器有效(节点不移动) 仅被删除元素的迭代器失效
随机访问迭代器 vectordeque 未扩容:插入位置后失效;扩容:全失效 删除位置后失效

1、序列式容器(vector、deque)

这类容器的元素在内存中是连续或半连续存储的,插入 / 删除操作可能导致元素移动或内存重分配,因此迭代器失效场景较多。

1.1 vector 容器

vector 基于动态数组实现,内存连续,迭代器本质是指向数组元素的指针。

失效场景:

  • 插入元素(push_back/insert):
    • 若插入后容器容量超过原容量(触发扩容,即内存重新分配),则所有迭代器、指针、引用全部失效(原内存被释放)。
    • 若未触发扩容,插入位置之后的迭代器失效(元素后移导致地址变化)。
  • 删除元素(erase/pop_back):
    • 删除位置之后的迭代器失效(元素前移导致地址变化)。
    • 被删除元素的迭代器直接失效。
  • resize/clear 操作:
    • resize 缩小容器时,超出新大小的元素被销毁,指向这些元素的迭代器失效。
    • clear 清空容器后,所有迭代器失效(指向已销毁的元素)。

解决方案:

  • 插入前通过reserve(n)预留足够容量,避免扩容导致的迭代器全失效。
  • 删除元素时,利用erase的返回值更新迭代器(erase返回被删除元素的下一个有效迭代器)。
  • 避免在遍历中同时修改容器(如需修改,通过返回值实时更新迭代器)。
cpp 复制代码
#include <vector>
#include <iostream>
using namespace std;

int main() {
    vector<int> vec = {1, 2, 3, 4, 5};
    
    // 错误示例:删除元素后迭代器失效
    // for (auto it = vec.begin(); it != vec.end(); ) {
    //     if (*it == 3) {
    //         vec.erase(it);  // it失效,后续++操作未定义
    //     } else {
    //         ++it;
    //     }
    // }
    
    // 正确示例:用erase返回值更新迭代器
    for (auto it = vec.begin(); it != vec.end(); ) {
        if (*it == 3) {
            it = vec.erase(it);  // 接收返回值,指向被删元素的下一个
        } else {
            ++it;
        }
    }
    
    // 插入前预留容量,避免扩容失效
    vec.reserve(10);  // 预留足够空间,后续插入不会扩容
    auto it = vec.begin() + 2;
    vec.insert(it, 6);  // 未扩容,仅插入位置后迭代器失效,it仍有效
    return 0;
}

1.2 deque 容器

deque 是 "双端队列",内存由多个连续块组成,迭代器需要同时记录块地址和块内偏移。

失效场景:

  • 插入元素:
    • 在两端(push_front/push_back)插入时,若未触发内存块分配,迭代器可能仍有效;若触发新块分配,所有迭代器失效
    • 在中间插入时,所有迭代器失效(内部结构重组)。
  • 删除元素:
    • 在两端删除时,可能导致迭代器失效(取决于具体实现)。
    • 在中间删除时,所有迭代器失效

解决方案:

  • 避免在 deque 中间进行插入 / 删除操作(性能差且易导致迭代器失效)。
  • 若需遍历中修改,操作后重新获取迭代器(不依赖原有迭代器)。

2、链表容器(list、forward_list)

list 是双向链表,forward_list 是单向链表,元素通过指针链接,内存不连续。

失效场景:

  • 插入元素:所有迭代器均有效(仅新增节点,不影响原有节点的指针关系)。
  • 删除元素 :仅指向被删除元素的迭代器失效,其他迭代器仍有效(节点间指针调整不影响其他节点)。

解决方案:

  • 删除元素后,不再使用指向被删元素的迭代器即可。
  • 遍历中删除时,同样可通过erase的返回值更新迭代器(list 的 erase 返回下一个有效迭代器)。
cpp 复制代码
#include <list>
#include <iostream>
using namespace std;

int main() {
    list<int> lst = {1, 2, 3, 4, 5};
    
    // 遍历删除元素,仅被删元素的迭代器失效
    for (auto it = lst.begin(); it != lst.end(); ) {
        if (*it == 3) {
            it = lst.erase(it);  // 安全更新迭代器
        } else {
            ++it;
        }
    }
    
    // 插入元素后,原有迭代器仍有效
    auto it = lst.begin();
    lst.insert(it, 6);  // 插入后it仍指向原位置(元素1)
    return 0;
}

3、关联式容器(set、map、multiset、multimap)

这类容器基于红黑树实现(有序),元素按键值排序,节点通过指针链接。

失效场景:

  • 插入元素:所有迭代器均有效(红黑树旋转仅调整指针,不改变节点地址)。
  • 删除元素 :仅指向被删除元素的迭代器失效,其他迭代器(包括指向其他节点的迭代器)仍有效。

解决方案:

  • 删除元素后,避免使用指向被删元素的迭代器。
  • 遍历中删除时,通过erase返回值更新迭代器(C++11 后,关联容器的 erase 返回下一个有效迭代器)。
cpp 复制代码
#include <map>
#include <iostream>
using namespace std;

int main() {
    map<int, string> mp = {{1, "a"}, {2, "b"}, {3, "c"}};
    
    // 遍历删除元素,仅被删元素的迭代器失效
    for (auto it = mp.begin(); it != mp.end(); ) {
        if (it->first == 2) {
            it = mp.erase(it);  // 更新迭代器
        } else {
            ++it;
        }
    }
    
    // 插入元素后,原有迭代器仍有效
    auto it = mp.find(1);
    mp.insert({4, "d"});  // 插入后it仍指向键1的节点
    return 0;
}

4、无序容器(unordered_set、unordered_map 等)

这类容器基于哈希表实现,元素存储在桶(bucket)中,桶内是链表或数组。

失效场景:

  • 插入元素:
    • 若插入后未触发哈希表重建(负载因子未超过阈值),仅被插入桶的迭代器可能受影响,其他迭代器有效。
    • 若触发重建(哈希表扩容并重新哈希),所有迭代器失效(节点地址可能改变)。
  • 删除元素 :仅指向被删除元素的迭代器失效,其他迭代器有效(哈希表结构未变)。

解决方案:

  • 插入前通过reserve(n)预留足够桶空间,避免重建导致的迭代器全失效。
  • 删除元素后,不使用指向被删元素的迭代器。
相关推荐
tkevinjd1 天前
C++线程池学习 Day07
c++
TangHao19871 天前
第一章 基础(Chapter 1 fundentals)
c++
沐怡旸1 天前
【底层机制】std::move 解决的痛点?是什么?如何实现?如何正确用?
c++·面试
tongsound2 天前
ros2 humble slam仿真环境搭建(turtlebot3 & Gazebo)
c++·docker
沐怡旸3 天前
【底层机制】std::weak_ptr解决的痛点?是什么?如何实现?如何正确用?
c++·面试
River4163 天前
Javer 学 c++(十六):对象特性篇(上)
c++·后端
感哥3 天前
C++ 左值、右值、左值引用、右值引用
c++
感哥3 天前
C++ 模板
c++
感哥3 天前
C++ 多重继承
c++