C++迭代器详解

一、迭代器到底是什么?它的核心作用又是什么?

迭代器是STL中最重要的抽象之一。它不是指针,而是一个对象(通常是类模板实例),其设计目标是:
提供一种统一的、泛型的遍历方式了,让算法与容器解耦。

我们来看看迭代器的核心作用,也就是为什么需要迭代器:

  1. 统一访问接口

    不同容器内部实现差异巨大,比如:std::vector 是连续内存数组;std::list 是双向链表;std::set 是红黑树。

    如果没有迭代器,开发者必须为每种容器写不同的遍历代码。迭代器把"如何遍历"封装起来,所有容器都提供begin()、end()、rbegin()等接口,返回对应迭代器。

  2. 支持泛型算法

    STL 算法(如 std::find、std::sort、std::copy、std::for_each)全部基于迭代器工作。只要传入满足要求的迭代器,就能对任意容器生效。这就是"算法与容器分离"的设计哲学。

  3. 封装与安全性

    用户无需关心容器内部结构(节点指针、内存布局等),只需通过 *it、++it 等操作访问元素。迭代器还能在 Debug 模式下进行边界检查。

  4. 高效性

    迭代器是针对具体容器优化的(如 vector 的随机访问迭代器本质上就是指针),性能与手写循环几乎无差别。

  5. 扩展性

    自定义容器只需实现迭代器,就能无缝接入 STL 算法。

总的来说,迭代器是容器与算法之间的桥梁,实现了"写一次算法,适用于所有容器"。

二、迭代器的分类

1.迭代器分类不是并列的,而是严格的继承/精炼关系。能力强的可以完全替代能力弱的。

想象你在"逛超市买东西":

  • Input Iterator(输入迭代器):
    你拿着购物篮,只往前走,边走边看货架上的东西(只能读),看完就不能回头了(单趟遍历)。
    典型:从文件/网络流(std::cin、istream_iterator)读数据。
    记忆口诀:只能进,不能退;只能看,不能改;只能走一次。
  • Output Iterator(输出迭代器):
    你拿着笔,只往前走,边走边往篮子里写东西(只能写),不能回头看之前写了什么。
    典型:往文件/控制台写(ostream_iterator、back_inserter)。
    记忆口诀:只写不读,只进不退,单趟。
    注意:Input 和 Output 经常一起出现,但它们是"平行"的(一个读、一个写)。
  • Forward Iterator(前向迭代器):
    Input + Output 的结合体,但可以多趟遍历(可以把迭代器复制一份,回头再走一遍)。
    像在一条单行道上可以来回走,但不能后退。
    典型:std::forward_list、unordered_set(无序容器)。
    记忆口诀:能多趟读写,只能往前(Forward = 前向)。
  • Bidirectional Iterator(双向迭代器):
    Forward + 可以后退(--it)。
    像在一条双行道上,可以往前走也可以往后退。
    典型:std::list、std::set、std::map(有序关联容器)。
    记忆口诀:双向 = Bidirectional = 可以 ++ 和 --。
  • Random Access Iterator(随机访问迭代器):
    Bidirectional + 可以跳跃(it + 5、it[3]、it < other 等)。
    像在超市里可以直接跑到第 10 个货架,完全随机位置。
    典型:std::vector、std::deque、std::string、std::array。
    记忆口诀:Random = 像数组指针一样随意跳(支持算术运算)。
  • Contiguous Iterator(连续迭代器,C++20 新增):
    Random Access + 内存物理连续(元素一个挨着一个,没有空隙)。
    这允许算法做更激进的优化(比如用 memcpy)。
    典型:std::vector、std::array、std::string(std::deque 是 Random 但不是 Contiguous)。
    记忆口诀:Contiguous = 连续内存,像 C 数组一样(C++20 才正式概念化)。

层次记忆:

Input/Output(单趟) -> Forward(多趟前向) -> Bidirectional(双向) -> Random Access(随机跳) -> Contiguous(连续内存)

从弱到强,每一级都包含上一级的所有能力。你只要记住这条升级链,就能快速推导出支持哪些操作。

2. 更简单的记忆方式

只能 ++:Input / Output / Forward

还能 --:Bidirectional

还能 +n / -n / [n] / < >:Random Access

还能保证内存连续:Contiguous

实际开发中:

大多数算法只需要 Forward 或更弱的就够了;std::sort、std::binary_search 等需要 Random Access;std::copy 等在 Contiguous 上会特别快。

3. 常见容器对应快速记忆表
容器类型 迭代器类别 记忆点
vector / array / string Random Access + Contiguous 像裸数组,最强
deque Random Access(非 Contiguous) 能随机,但内存分块
list / set / map Bidirectional 双向链表/树,能前后走
forward_list / unordered_* Forward 单向,只能往前
istream / ostream Input / Output 流,单趟读写

三、迭代器什么时候会失效

先了解迭代器失效的定义:迭代器不再指向合法元素,或指向的内存已被释放/移动,继续使用会导致未定义行为,可能崩溃、数据错乱、段错误等。

失效的根本原因:容器内部存储结构发生改变(如内存重分配、元素移动、节点删除等)。

C++ 将迭代器按能力从弱到强分为 5 类(C++20 增加第 6 类),能力越强,支持的操作越多(这里不展开说明)

按容器分类,来讲讲不同容器失效规则:
  1. std::vector / std::deque / std::string(最容易失效)
  • reallocation(重新内存分配)时,所有迭代器、指针、引用全部失效:
    (1)push_back、insert、emplace导致容量不足时;
    (2)reserve、resize(扩大);
    (3)clear后resize或insert。
  • erase/insert时:
    (1)被擦除/插入位置及之后的所有迭代器失效;
    (2)插入点之前(vector)的迭代器可能保持有效。
  • 其他:pop_back 仅使指向被弹出元素的迭代器有效。
  1. std::list / std::forward_list

    insert、erase、splice仅使指向被操作元素的迭代器失效,其他所有迭代器始终保持有效(这是链表的最大优势)。

  2. 关联容器(std::set / std::map / multiset / multimap)

    C++11 起:erase(it)仅使指向被擦除元素的迭代器失效,其他迭代器保持有效;insert绝不使任何现有迭代器失效(红黑树节点插入不移动其他节点)。

  3. 无序容器(std::unordered_set / unordered_map 等)

    insert 可能触发 rehash,导致所有迭代器失效;erase 仅使指向被擦除元素的迭代器失效。

  4. std::array

    因为std::array(静态数组)是固定大小几乎永不失效(除非整个容器被销毁)。

示例(vector失效):

cpp 复制代码
std::vector<int> v = {1, 2, 3, 4, 5};
auto it = v.begin() + 2;  // 指向 3
v.insert(v.begin(), 0);   // 可能 reallocate -> it 失效!
*it;                      // 未定义行为,危险!
应怎样避免迭代器失效:
  • C++11 起推荐写法:auto it = v.erase(it);(erase 返回下一个有效迭代器)。
  • 使用范围 for 循环或 std::erase_if(C++20)。
  • 需要多次修改时,优先选择 std::list 或先收集要删除的元素再统一删除。
  • Debug 模式下开启迭代器检查.

四、C++迭代器与指针的区别

讲解区别之前,先来看看两者的相似点:

都支持 * it、it->member、++it、it + n(随机访问时)等语法。

数组的指针 int* 天然就是 Random Access Iterator。

我用表格来对比两者的区别:

迭代器 指针
本质 类对象(可能封装了节点指针、偏移等) 裸内存地址(T*)
适用范围 特定容器(vector、list、set 等) 任意内存(数组、堆、栈)
操作能力 取决于类别(Forward 迭代器不支持 --) 永远是 Random Access(p + 5、p[3])
安全性 Debug 模式有边界检查;不支持空指针语义 无任何检查;nullptr 合法但危险
失效机制 容器特定规则(reallocation、erase 等) delete、realloc、vector reallocate 后失效
算术运算 仅 Random Access 支持 it + n、it - it2 任意指针都支持(但跨对象 UB)
类型系统 支持 std::iterator_traits、std::next 等 普通 T*,无 traits 支持
典型用途 遍历 STL 容器、泛型算法 底层内存操作、C 风格数组

总结来说:

指针是迭代器的一种特例,但迭代器不一定是指针(list 的迭代器是指向节点的指针 + 封装);

迭代器更"智能"、更安全、更泛型;指针更"原始"、更危险、更底层;

所以永远不要把迭代器当成指针做危险操作。

cpp 复制代码
// 迭代器
std::vector<int>::iterator it = v.begin();  // 可能就是 int*
// list 的迭代器
std::list<int>::iterator lit;               // 内部是 _List_node*,不是连续内存

五、一些注意事项

  1. 现代 C++ 优先:范围 for + 结构化绑定 > 手动迭代器。
  2. 保存迭代器时:只在单次循环内保存,修改容器前重新获取。
  3. const 正确性:能用 const_iterator 就不要用普通迭代器。
  4. C++20 增强:std::ranges 库让迭代器使用更安全(std::ranges::find 等)。
  5. 常见错误:
    循环中边遍历边 erase 不更新迭代器;
    保存 end() 迭代器后修改容器;
    对 unordered_* 容器假设迭代器永远有效。
相关推荐
qq_148115372 小时前
C++网络编程(Boost.Asio)
开发语言·c++·算法
weisian1512 小时前
Java并发编程--24-死锁排查与性能调优:线上并发问题诊断指南(死锁,CPU飙升,内存溢出)
java·开发语言·arthas·死锁·火焰图·cpu飙升
CSCN新手听安2 小时前
【Qt】Qt概述(三)Qt初识,HelloWorld的创建,对象树
开发语言·qt
2301_804215412 小时前
内存映射文件高级用法
开发语言·c++·算法
luanma1509802 小时前
PHP vs C#:30字秒懂两大语言核心差异
android·开发语言·python·php·laravel
Channing Lewis2 小时前
Python 全局变量调用了一个函数,如何实现每次使用时都运行一次函数获取最新的结果
开发语言·python
ZHENGZJM2 小时前
负载均衡式在线评测系统(Load-Balanced Online OJ)技术全景指南
c++·负载均衡·软件工程·idea
CoderCodingNo2 小时前
【GESP】C++八级考试大纲知识点梳理 (5) 代数与平面几何
开发语言·c++
毕设源码-朱学姐2 小时前
【开题答辩全过程】以 基于Java的运动场地预约系统为例,包含答辩的问题和答案
java·开发语言