迭代器简介

遍历数组(或其他数据结构)中的数据是编程中极常见的操作。迄今为止,我们已介绍多种遍历方式:使用循环与索引(while 循环与 for 循环)、使用指针与指针算术,以及使用基于范围的 for 循环:

#include <array>
#include <cstddef>
#include <iostream>

int main()
{
    // C++17 中,变量 arr 的类型被推导为 std::array<int, 7>
    // 若编译时出现错误,请参见下方警告
    std::array arr{ 0, 1, 2, 3, 4, 5, 6 };
    std::size_t length{ std::size(arr) };

    // 使用索引的 while 循环
    std::size_t index{ 0 };
    while (index < length)
    {
        std::cout << arr[index] << ' ';
        ++index;
    }
    std::cout << '\n';

    // 使用索引的 for 循环
    for (index = 0; index < length; ++index)
    {
        std::cout << arr[index] << ' ';
    }
    std::cout << '\n';

    // 使用指针的 for 循环(ptr 不能为 const,因为需要递增)
    for (auto ptr{ &arr[0] }; ptr != (&arr[0] + length); ++ptr)
    {
        std::cout << *ptr << ' ';
    }
    std::cout << '\n';

    // 基于范围的 for 循环
    for (int i : arr)
    {
        std::cout << i << ' ';
    }
    std::cout << '\n';

    return 0;
}

警告
本节示例使用了 C++17 的“类模板实参推导”功能,根据初始化器自动推断模板实参。如上例中,编译器将 std::array arr{ 0, 1, 2, 3, 4, 5, 6 }; 推导为 std::array<int, 7> arr{ 0, 1, 2, 3, 4, 5, 6 };
若编译器未启用 C++17,将出现类似“缺少模板实参”的错误。请按本节——配置编译器:选择语言标准启用 C++17;若无法启用,请将使用类模板实参推导的语句改为显式指定模板实参,例如 std::array<int, 7> arr{ … };

各种遍历方式的局限

  • 基于索引的循环:仅当容器支持随机访问(如数组)时才适用,且代码冗长。
  • 指针与指针算术:语法冗长,对不熟悉指针的读者不友好;仅适用于内存连续的元素(数组),不适用于链表、树、映射等结构。
  • 基于范围的 for 循环:机制隐藏,却能遍历数组、链表、树、映射等多种结构——其背后依赖迭代器

迭代器

迭代器是一个专门用于遍历容器(如数组元素、字符串字符等)的对象,沿途提供对每个元素的访问。
同一容器可提供多种迭代器,例如数组可提供前向迭代器(正向遍历)和反向迭代器(逆向遍历)。
创建合适类型的迭代器后,程序员即可通过统一接口遍历并访问元素,而无需关心底层存储方式。
C++ 迭代器通常使用 operator++ 移动到下一元素,operator* 访问当前元素,从而对不同容器保持一致的遍历方法。

指针作为迭代器

最简单的迭代器是指针,用于顺序存储的数据:

#include <array>
#include <iostream>

int main()
{
    std::array arr{ 0, 1, 2, 3, 4, 5, 6 };

    auto begin{ &arr[0] };
    auto end{ begin + std::size(arr) }; // 指向尾后位置

    for (auto ptr{ begin }; ptr != end; ++ptr)
        std::cout << *ptr << ' ';
    std::cout << '\n';
    return 0;
}

输出:0 1 2 3 4 5 6

警告
切勿使用 int* end{ &arr[std::size(arr)] }; 获取尾后指针,这会造成越界解引用,导致未定义行为。
正确写法:int* end{ arr.data() + std::size(arr) };data() 返回指向首元素的指针)。

标准库迭代器

所有标准库容器均直接支持迭代。无须手动计算首尾,只需调用容器的 begin()end() 成员函数:

#include <array>
#include <iostream>

int main()
{
    std::array array{ 1, 2, 3 };

    auto begin{ array.begin() };
    auto end{ array.end() };

    for (auto p{ begin }; p != end; ++p)
        std::cout << *p << ' ';
    std::cout << '\n';
    return 0;
}

输出:1 2 3

<iterator> 头文件还提供了泛型函数 std::beginstd::end,可用于 C 风格数组或标准容器:

#include <array>
#include <iostream>

int main()
{
    std::array array{ 1, 2, 3 };
    auto begin{ std::begin(array) };
    auto end{ std::end(array) };

    for (auto p{ begin }; p != end; ++p)
        std::cout << *p << ' ';
    std::cout << '\n';
    return 0;
}

(提示:对 C 风格数组,std::begin/std::end 定义在 <iterator>;对容器,则位于相应头文件。)

operator<operator!= 在循环条件中的差异

  • 对数值索引循环,优先使用 index < length(见本节)。
  • 对迭代器循环,惯例使用 p != end,因某些迭代器类型不支持关系比较,而 != 对所有迭代器均适用。

回到基于范围的 for 循环

任何拥有 begin()end() 成员函数,或可配合 std::begin()/std::end() 的类型,均可用于基于范围的 for 循环:

#include <array>
#include <iostream>

int main()
{
    std::array array{ 1, 2, 3 };
    for (int i : array)
        std::cout << i << ' ';
    std::cout << '\n';
    return 0;
}

基于范围的 for 循环背后调用容器的 begin()end()
C 风格定长数组可用 std::begin/std::end,故也支持;动态或退化数组不支持,因其类型信息不含长度。

迭代器失效(悬垂迭代器)

与指针、引用类似,迭代器也可能因元素地址变化或销毁而“悬垂”,此时称迭代器失效。访问失效迭代器将导致未定义行为。
某些修改容器的操作(如向 std::vector 添加元素)可能导致元素地址变化,从而令现有迭代器失效。良好的 C++ 文档会注明哪些操作可能或必然导致迭代器失效。

示例:在遍历中修改容器导致失效

#include <vector>

int main()
{
    std::vector v{ 0, 1, 2, 3 };
    for (auto num : v)          // 隐式使用迭代器
    {
        if (num % 2 == 0)
            v.push_back(num + 1); // 使 v 的迭代器失效,未定义行为
    }
    return 0;
}

}


**示例:使用失效迭代器**

```cpp
#include <iostream>
#include <vector>

int main()
{
    std::vector v{ 1, 2, 3, 4, 5, 6, 7 };
    auto it{ v.begin() };
    ++it;
    std::cout << *it << '\n'; // 输出 2

    v.erase(it); // 删除当前元素,使 it 及后续迭代器失效

    ++it;        // 未定义行为
    std::cout << *it << '\n';
    return 0;
}

修复方法erase 返回指向被删元素下一位置的迭代器,可将其赋值给原迭代器:

it = v.erase(it); // 重新获得有效迭代器
std::cout << *it << '\n'; // 安全,输出 3

失效迭代器可通过赋予新的有效迭代器(如 begin()end() 或容器操作返回值)重新生效。

#include <iostream>
#include <vector>

int main()
{
	std::vector v{ 1, 2, 3, 4, 5, 6, 7 };

	auto it{ v.begin() };

	++it; // move to second element
	std::cout << *it << '\n';

	it = v.erase(it); // erase the element currently being iterated over, set `it` to next element

	std::cout << *it << '\n'; // now ok, prints 3

	return 0;
}

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"