C++ 迭代器详解(2)
迭代器适配器(Iterator Adapters)
迭代器适配器(Iterator Adapters)是C++标准库中的一类工具,用于对现有的迭代器进行包装和修改,以提供更灵活的迭代方式。通过这些适配器,可以轻松地将标准迭代器转换为特定需求的迭代器,避免在使用过程中手动处理迭代器行为。常见的迭代器适配器主要包括以下几种:
std::reverse_iterator
std::reverse_iterator
用于将一个正向迭代器转换为反向迭代器。通过该适配器,你可以从容器的末尾向前迭代,而不是从头到尾。
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
std::reverse_iterator<std::vector<int>::iterator> rbegin(vec.end());
std::reverse_iterator<std::vector<int>::iterator> rend(vec.begin());
for (auto it = rbegin; it != rend; ++it) {
std::cout << *it << " ";
}
// Output: 5 4 3 2 1
}
使用方法:
可以通过rbegin()和rend()成员函数快速生成容器的反向迭代器。例如:vec.rbegin()和vec.rend()直接返回反向迭代器。
std::back_insert_iterator
std::back_insert_iterator适配器用于向容器的末尾插入元素。常与std::back_inserter函数配合使用,通过此适配器,算法可以在操作时自动向容器尾部添加元素,而不需要关心容器大小是否合适。
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {1, 2, 3};
std::vector<int> additional = {4, 5, 6};
std::copy(additional.begin(), additional.end(), std::back_inserter(vec));
for (int n : vec) {
std::cout << n << " ";
}
// Output: 1 2 3 4 5 6
}
使用方法
std::back_inserter是一个辅助函数,用来生成一个back_insert_iterator,用法如std::back_inserter(vec)。
std::front_insert_iterator
std::front_insert_iterator与back_insert_iterator类似,但它用于从容器的前端插入元素。常用于支持前插入的容器,如std::deque或std::list。适配器和std::front_inserter函数一起使用。
#include <iostream>
#include <list>
#include <algorithm>
int main() {
std::list<int> lst = {1, 2, 3};
std::vector<int> additional = {4, 5, 6};
std::copy(additional.begin(), additional.end(), std::front_inserter(lst));
for (int n : lst) {
std::cout << n << " ";
}
// Output: 6 5 4 1 2 3
}
使用方法
std::front_inserter(lst)可以生成一个front_insert_iterator。
std::insert_iterator
std::insert_iterator用于在指定位置插入元素。不同于back_insert_iterator和front_insert_iterator,它允许插入到容器的任意位置。该适配器常与std::inserter辅助函数配合使用。
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {1, 2, 3};
std::vector<int> additional = {4, 5, 6};
std::copy(additional.begin(), additional.end(), std::inserter(vec, vec.begin() + 1));
for (int n : vec) {
std::cout << n << " ";
}
// Output: 1 4 5 6 2 3
}
使用方法
使用std::inserter(vec, position)生成insert_iterator,其中position表示插入位置。
std::istream_iterator 和 std::ostream_iterator
std::istream_iterator适配器用于将输入流(如std::cin)与算法连接,便于从流中读取数据。
std::ostream_iterator则将输出流(如std::cout)与算法连接,便于向流中输出数据。
#include <iostream>
#include <vector>
#include <iterator>
#include <algorithm>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
std::copy(vec.begin(), vec.end(), std::ostream_iterator<int>(std::cout, " "));
// Output: 1 2 3 4 5
}
使用方法
创建时指定流对象(如std::cout)以及分隔符(如" ")即可。
小结
迭代器适配器在STL算法中有广泛应用,通过这些适配器,可以实现更灵活的迭代操作,如反向迭代、前后插入、流输入输出等。了解并掌握这些适配器可以显著简化代码,提高代码的通用性和可读性。
迭代器适配器 | 适用容器 | 容器要求 | 说明 |
---|---|---|---|
std::reverse_iterator | std::vector , std::deque , std::list , std::array | 必须支持双向迭代(Bidirectional Iterator) | 使容器反向遍历,std::forward_list 不支持双向迭代,不能使用反向迭代器。 |
std::back_insert_iterator | std::vector , std::deque , std::list | 必须支持push_back | 用于在容器尾部插入元素。std::array 和std::forward_list 不支持push_back 。 |
std::front_insert_iterator | std::deque , std::list , std::forward_list | 必须支持push_front | 用于在容器头部插入元素。std::vector 和std::array 不支持push_front 。 |
std::insert_iterator | std::vector , std::deque , std::list , std::set , std::map | 必须支持insert | 在指定位置插入元素,适用于大多数支持插入的容器。 |
std::move_iterator | 所有标准容器 | 只需支持解引用(Dereferenceable) | 以移动语义访问元素,适用于所有标准容器。 |
迭代器的失效
迭代器的失效是指一个迭代器变得无效,不能再用于访问容器中的元素。迭代器失效通常发生在容器被修改(例如添加或删除元素)时,特别是在容器的结构发生变化时。
迭代器失效的原因
-
容器大小变化(如插入和删除)
插入元素:当你在容器中间或尾部插入元素时,通常会导致容器的重新分配。如果容器是std::vector这类动态数组容器,插入元素可能会导致内部数组重新分配,所有指向原数组元素的迭代器都将失效。
删除元素:如果删除容器中的元素,原本指向已删除元素的迭代器会失效。同样,删除操作可能使得迭代器指向的容器位置发生变化,特别是对于std::list和std::deque这样的容器。 -
容器的重新分配(通常发生在
std::vector
)
当std::vector
的容量不足以容纳新的元素时,它会重新分配内存并将元素移动到新的位置。这会导致原来指向容器元素的所有迭代器失效。插入操作可能会触发这种重新分配。 -
容器的缩减(例如删除所有元素)
当容器的所有元素都被删除时,容器的begin()
和end()
迭代器会失效,或者指向的元素会变为空。
迭代器失效的规则
C++标准库对不同容器在执行插入、删除等操作时,对迭代器的失效有不同的规则。以下是几种常见容器的迭代器失效规则:
-
std::vector
:由于std::vector
是一个动态数组,当容器增长时,它可能会重新分配内存,导致原有的迭代器失效。如果你在使用迭代器时频繁插入或删除元素,应当避免插入操作导致的大规模内存重分配,或者在插入前先预留足够的空间。 -
std::deque
:std::deque
是一个双端队列,插入元素时,头部和尾部的插入可能导致某些迭代器失效,但不像std::vector
那样对所有迭代器造成影响。对于中间插入,std::deque
通常不会导致迭代器失效。 -
std::list
:std::list
是一个双向链表,插入和删除操作不会导致其他位置的迭代器失效。但是,如果删除的元素是迭代器所指向的元素,则该迭代器会失效。 -
std::set
和std::map
:这些容器使用红黑树实现,插入和删除操作不会导致所有迭代器失效,但删除的元素对应的迭代器会失效。如果删除的是当前迭代器指向的元素,那么该迭代器将变得无效。 -
std::forward_list
:与std::list
类似,但std::forward_list
是单向链表。它的插入操作不会使其他位置的迭代器失效,删除操作仅会使指向已删除元素的迭代器失效。
迭代器失效的处理方式
- 避免在迭代过程中修改容器大小:尽量避免在遍历容器时进行插入和删除操作,特别是对于
std::vector
和std::deque
等动态数组容器。如果确实需要修改容器,应考虑先进行所有插入和删除操作,再进行遍历。
示例:
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 先遍历容器
for (auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " "; // 输出:1 2 3 4 5
}
// 然后再进行修改(删除操作)
vec.push_back(6); // 插入新元素
vec.erase(vec.begin()); // 删除第一个元素
// 遍历修改后的容器
for (auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " "; // 输出:2 3 4 5 6
}
}
- 使用
std::advance
:在某些情况下,可以通过std::advance
来避免手动管理迭代器,尤其是在删除元素时,std::advance
可以帮助你正确地更新迭代器。
示例1:
#include <list>
#include <iostream>
#include <iterator>
int main() {
std::list<int> lst = {1, 2, 3, 4, 5};
auto it = lst.begin();
// 删除过程中通过保存下一个元素的迭代器来避免失效
while (it != lst.end()) {
auto next_it = std::next(it); // 获取下一个元素的迭代器
if (*it == 3) {
lst.erase(it); // 删除元素
}
it = next_it; // 更新迭代器到下一个元素
}
// 打印修改后的列表
for (const auto& num : lst) {
std::cout << num << " "; // 输出:1 2 4 5
}
}
示例2:
#include <iostream>
#include <list>
#include <iterator> // std::advance
int main() {
std::list<int> lst = {1, 2, 3, 4, 5, 6, 7, 8};
auto it = lst.begin();
while (it != lst.end()) {
if (*it % 2 == 0) { // 如果元素是偶数
it = lst.erase(it); // 删除当前元素,并返回删除元素后的位置
} else {
std::advance(it, 1); // 使用std::advance将迭代器移动到下一个元素
}
}
// 打印修改后的列表
for (const auto& num : lst) {
std::cout << num << " "; // 输出:1 3 5 7
}
}
- 在删除操作后更新迭代器:如果在容器中删除元素,删除后应当重新获取有效的迭代器,特别是当迭代器指向已删除元素时。
示例:
#include <vector>
#include <iostream>
#include <algorithm>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); /*没有++it*/) {
if (*it == 3) {
it = vec.erase(it); // 删除元素并更新迭代器
} else {
++it;
}
}
// 打印修改后的容器
for (auto value : vec) {
std::cout << value << " "; // 输出:1 2 4 5
}
}
- 避免依赖失效的迭代器:如果你需要在遍历过程中删除容器中的元素,可以选择先将要删除的元素标记出来,最后在一次遍历结束后统一进行删除,或者使用
erase-remove
惯用法。
示例:
#include <vector>
#include <iostream>
#include <algorithm>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 3, 5};
// 使用erase-remove惯用法删除所有值为3的元素
vec.erase(std::remove(vec.begin(), vec.end(), 3), vec.end());
// 打印修改后的容器
for (auto value : vec) {
std::cout << value << " "; // 输出:1 2 4 5
}
}
- 容器预分配空间:对于
std::vector
等容器,如果需要频繁插入元素,可以考虑使用reserve()
来预分配足够的空间,避免由于容器重新分配内存而导致的迭代器失效。
示例:
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec;
vec.reserve(10); // 预分配足够的空间以避免重新分配内存
// 插入元素,不会导致重新分配
for (int i = 1; i <= 10; ++i) {
vec.push_back(i);
}
// 打印容器中的元素
for (auto value : vec) {
std::cout << value << " "; // 输出:1 2 3 4 5 6 7 8 9 10
}
}
小结
迭代器失效是C++容器中常见的一个问题,尤其是在进行插入和删除操作时。不同容器对迭代器的失效有不同的规则,理解这些规则可以帮助开发者避免潜在的错误。在处理迭代器失效时,最好的做法是尽量避免在遍历过程中修改容器,或者确保在修改容器后重新获得有效的迭代器。
自定义迭代器
自定义迭代器在C++中是指用户定义的特殊迭代器类型,用来满足自定义数据结构或算法的需求。通过自定义迭代器,可以实现对非标准容器的数据访问,以符合C++标准库的迭代器接口,使其在标准算法中使用。
自定义迭代器主要由以下几个关键概念组成:
-
迭代器类型:定义迭代器所实现的行为类型,如输入迭代器、输出迭代器、前向迭代器、双向迭代器和随机访问迭代器。
-
标准迭代器接口:需要提供特定的操作符支持,如解引用(
operator*
)、递增(operator++
)、相等性比较(operator==
)等。 -
迭代器适配器:迭代器适配器可用于对已有的迭代器进行包装,为它们添加额外的行为。
自定义迭代器的组成
在C++中,自定义迭代器主要包含以下几个必要的成员:
类型定义(type aliases):定义iterator_category
、value_type
、difference_type
、pointer
和reference
等类型。
构造函数:用于初始化迭代器。
解引用操作符(operator*
):返回迭代器当前指向的元素。
递增和递减操作符:前置递增(operator++
)和后置递增(operator++(int)
)。对于双向迭代器和更高级别的迭代器,通常还需要支持前置和后置递减(operator--
和operator--(int)
)。
相等和不相等比较(operator==
和operator!=
):用于比较两个迭代器是否相等。
自定义迭代器的实现步骤
- 定义基本迭代器类型
首先,定义迭代器类别,如输入迭代器、双向迭代器等,这决定了它能支持的操作。例如,输入迭代器允许读取元素,但不一定支持修改或双向遍历。以下是一些常见的迭代器类别:
std::input_iterator_tag //输入迭代器
std::output_iterator_tag//输出迭代器
std::forward_iterator_tag//前向迭代器
std::bidirectional_iterator_tag//双向迭代器
std::random_access_iterator_tag//随机访问迭代器
- 自定义一个简单的前向迭代器
以下代码实现了一个简单的前向迭代器,适用于遍历一个整数数组。
#include <iterator>
class SimpleIterator {
public:
using iterator_category = std::forward_iterator_tag;
using value_type = int;
using difference_type = std::ptrdiff_t;
using pointer = int*;
using reference = int&;
// 构造函数
SimpleIterator(pointer ptr) : m_ptr(ptr) {}
// 解引用操作符,获取当前元素
reference operator*() const {
return *m_ptr;
}
// 前置递增操作符,返回自增后的迭代器
SimpleIterator& operator++() {
++m_ptr;
return *this;
}
// 后置递增操作符,返回自增前的迭代器
SimpleIterator operator++(int) {
SimpleIterator temp = *this;
++(*this); // 调用前置递增
return temp;
}
// 相等性比较操作符
bool operator==(const SimpleIterator& other) const {
return m_ptr == other.m_ptr;
}
// 不等性比较操作符
bool operator!=(const SimpleIterator& other) const {
return m_ptr != other.m_ptr;
}
private:
pointer m_ptr; // 指向当前元素的指针
};
代码解析
- 类型定义:
iterator_category
指定为std::forward_iterator_tag
,表示它是一个前向迭代器。另外定义了迭代器的数据类型、指针类型、引用类型等。 - 构造函数:构造函数接收一个指针,用于初始化迭代器。
- 解引用操作符:
operator*
返回当前元素的引用,可以用于访问元素值。 - 前置递增操作符:operator++将迭代器移动到下一个元素,返回自增后的迭代器自身。
- 后置递增操作符:operator++(int)实现后置递增操作,返回自增前的迭代器值。
- 相等和不等比较操作符:operator==和operator!=用于比较两个迭代器是否指向相同的元素。
使用示例
下面是如何使用这个SimpleIterator的示例代码:
#include <iostream>
int main() {
int arr[] = {1, 2, 3, 4, 5};
SimpleIterator begin(arr);
SimpleIterator end(arr + 5);
for (SimpleIterator it = begin; it != end; ++it) {
std::cout << *it << " "; // 输出:1 2 3 4 5
}
return 0;
}
处理更复杂的迭代器类型
如果想要实现更复杂的迭代器类型(如双向迭代器、随机访问迭代器),需要进一步支持操作符,如:
双向迭代器:除了++,还要实现--
操作符(前置和后置)。
随机访问迭代器:需要实现加法(operator+
)、减法(operator-
)、下标访问(operator[]
)、比较操作符(<
, <=
, >
, >=
),以支持随机访问和更高效的遍历。
自定义迭代器的实际应用
- 自定义容器的迭代器:实现特定数据结构的迭代器(如树、图),使它们可以与标准库算法兼容。
- 包装现有迭代器:使用迭代器适配器模式增强已有迭代器的功能(如加密、解密数据流,过滤器等)。
- 特殊用途的迭代器:如生成器迭代器(生成一系列值)、转换迭代器(转换值)等。
小结
自定义迭代器通过实现标准的迭代器接口,使自定义数据结构或特殊数据流能够与C++标准算法库兼容,实现灵活的元素访问和遍历。同时根据迭代器类型的不同,自定义迭代器可以支持不同的操作和行为。