遍历数组(或其他数据结构)中的数据是编程中极常见的操作。迄今为止,我们已介绍多种遍历方式:使用循环与索引(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::begin
与 std::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;
}