C++从入门到实战(二十一)List迭代器实现
C++从入门到实战(二十一)List迭代器实现
- 前言
- 一、为什么需要自定义list迭代器?
- 二、list迭代器的核心:运算符重载
- 2.1 基础准备:节点结构回顾
- 2.2 迭代器类的初步实现(普通迭代器)
- 2.3 关键运算符详解:operator* 与 operator->
- 2.3.1 案例:遍历存储自定义类型的list
- 2.3.2 用operator*访问成员
- 2.3.3 用operator->访问成员(编译器优化)
- 原理拆解:
- 2.3.4 常见错误:直接用节点指针访问
- 2.4 补充:后置++与后置--的重载
- 三、const迭代器的实现:模板复用解决冗余
- 3.1 痛点:直接复制代码的冗余
- 3.2 解决方案:增加模板参数,复用迭代器类
- 3.2.1 模板化迭代器类实现
- 3.2.2 在list类中定义两种迭代器
- 3.2.3 const迭代器的使用示例
- 四、迭代器的封装与STL设计理念
- 五、list迭代器与vector迭代器的对比
- 六、完整模拟实现:迭代器+list集成
前言
- 上一篇博客中,我们在模拟实现
list
时提到:list
的迭代器并非原生指针。由于list
底层是双向循环链表(离散节点),原生指针无法直接模拟“双向遍历”“节点访问”等行为——这就需要我们自定义迭代器类,通过运算符重载封装节点指针,让迭代器像原生指针一样易用。 - 本篇将聚焦
list
迭代器的核心实现:从“为什么需要自定义迭代器”出发,深入讲解operator*
、operator->
等关键运算符的重载逻辑,解决“const迭代器”的实现痛点,并通过模板复用减少代码冗余,最终理解STL迭代器“封装底层、统一接口”的设计思想。
我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343
我的C++知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_12880513.html?spm=1001.2014.3001.5482
C++官方list迭代器文档
https://cplusplus.com/reference/list/list/iterator/
一、为什么需要自定义list迭代器?
vector
的迭代器可以直接用原生指针(如int*
),因为其底层是连续内存——原生指针的++
、*
、[]
等操作天然匹配vector
的访问需求。但list
完全不同:
- 底层结构不兼容:
list
是离散节点,每个节点包含_prev
、_next
指针,原生指针无法通过++
直接跳转到下一个节点(需访问_next
成员)。 - 隐藏底层细节:用户无需知道
list
的节点结构(如ListNode
的_prev
/_next
),只需通过迭代器的统一接口(++
、*
)操作元素。 - 支持const语义:需要区分“可修改元素的迭代器”(
iterator
)和“只读元素的迭代器”(const_iterator
),原生指针无法直接满足。
简言之,list
迭代器的核心目标是:封装节点指针,通过运算符重载模拟原生指针行为,同时统一容器访问接口。
二、list迭代器的核心:运算符重载
自定义迭代器的本质是“重载一系列运算符”,让迭代器支持以下操作(与原生指针一致):
- 移动:
++it
(向后)、--it
(向前) - 解引用:
*it
(获取元素引用)、it->
(获取元素指针,访问自定义类型成员) - 比较:
it1 == it2
(是否指向同一节点)、it1 != it2
(是否指向不同节点)
2.1 基础准备:节点结构回顾
首先回顾list
的节点结构,迭代器将围绕节点指针展开:
template <class T>
struct ListNode {ListNode<T>* _prev; // 前驱指针ListNode<T>* _next; // 后继指针T _data; // 存储的数据// 节点构造函数ListNode(const T& data = T()): _prev(nullptr), _next(nullptr), _data(data){}
};
2.2 迭代器类的初步实现(普通迭代器)
先实现支持“修改元素”的普通迭代器,核心是封装ListNode<T>*
并重载关键运算符:
template <class T>
struct ListIterator {typedef ListNode<T> Node; // 简化节点类型名typedef ListIterator<T> Self; // 简化迭代器类型名Node* _node; // 核心:封装节点指针// 1. 迭代器构造函数(接收节点指针)ListIterator(Node* node): _node(node){}// 2. 重载operator*:解引用,返回元素引用(支持修改)T& operator*() {return _node->_data; // 直接返回节点中的数据引用}// 3. 重载operator->:返回元素指针(访问自定义类型成员)T* operator->() {return &(_node->_data); // 返回数据的地址}// 4. 重载operator++:前置++,向后移动到下一个节点Self& operator++() {_node = _node->_next; // 借助节点的_next指针移动return *this; // 返回自身(支持链式操作,如++(++it))}// 5. 重载operator--:前置--,向前移动到前一个节点Self& operator--() {_node = _node->_prev; // 借助节点的_prev指针移动return *this;}// 6. 重载operator==:判断是否指向同一节点bool operator==(const Self& other) const {return _node == other._node;}// 7. 重载operator!=:判断是否指向不同节点bool operator!=(const Self& other) const {return _node != other._node;}
};
2.3 关键运算符详解:operator* 与 operator->
operator*
和operator->
是迭代器访问元素的核心,尤其是operator->
在访问自定义类型时容易产生疑惑,我们通过案例拆解:
2.3.1 案例:遍历存储自定义类型的list
假设我们有一个自定义类A
,包含两个成员_a1
和_a2
,用list
存储A
的对象:
#include <iostream>
using namespace std;// 自定义类A
struct A {int _a1;int _a2;A(int a1 = 0, int a2 = 0): _a1(a1), _a2(a2){}
};// 初始化list<A>
list<A> lt2;
lt2.push_back(A(1, 2));
lt2.push_back(A(3, 4));
2.3.2 用operator*访问成员
通过*it
获取A
对象的引用,再用.
访问成员:
// 普通迭代器遍历
list<A>::iterator it = lt2.begin();
while (it != lt2.end()) {// *it 返回A&,通过.访问成员cout << (*it)._a1 << ":" << (*it)._a2 << endl;++it;
}
// 输出:
// 1:2
// 3:4
注意:(*it)
必须加括号,因为.
的优先级高于*
,不加括号会被解析为*(it._a1)
(错误)。
2.3.3 用operator->访问成员(编译器优化)
如果用it->
访问成员,代码会更简洁,但背后有编译器的特殊优化:
while (it != lt2.end()) {// it-> 返回A*,理论上需写 it->->_a1,但编译器自动省略一个->cout << it->_a1 << ":" << it->_a2 << endl;++it;
}
原理拆解:
it->
调用operator->()
,返回&(_node->_data)
(即A*
类型)。- 理论上,访问成员需要
(it.operator->())->_a1
(两次->
:一次调用迭代器的operator->
,一次原生指针的->
)。 - C++编译器为了简化代码,自动省略中间的一个
->
,允许直接写it->_a1
。
这就是operator->
重载的“特殊之处”——看似只写了一个->
,实则隐含了两次指针访问。
2.3.4 常见错误:直接用节点指针访问
如果不封装迭代器,直接用Node*
遍历,代码会暴露底层结构,且可读性差:
// 不推荐:直接操作节点指针,暴露底层实现
ListNode<A>* node = lt2._head->_next; // 假设_head是public(实际应私有)
while (node != lt2._head) {cout << node->_data._a1 << ":" << node->_data._a2 << endl;node = node->_next;
}
显然,迭代器的封装让代码更简洁、更安全(无需访问list
的私有成员_head
)。
2.4 补充:后置++与后置–的重载
前面实现了前置++/–(如++it
),实际开发中还会用到后置++/–(如it++
)。两者的核心区别是:
- 前置:先移动,再返回自身(返回引用)。
- 后置:先返回当前状态,再移动(返回值,不能是引用)。
实现后置++/–时,需用int
作为参数(无实际意义,仅用于区分前置和后置):
template <class T>
struct ListIterator {// ... 其他成员 ...// 后置++:参数int用于区分,返回移动前的迭代器(值返回)Self operator++(int) {Self temp(*this); // 保存当前迭代器状态_node = _node->_next; // 移动到下一个节点return temp; // 返回移动前的状态}// 后置--:同理Self operator--(int) {Self temp(*this); // 保存当前状态_node = _node->_prev; // 移动到前一个节点return temp; // 返回移动前的状态}
};
使用示例:
list<int> l = {1,2,3};
auto it = l.begin();
auto it2 = it++; // it2指向1(移动前),it指向2(移动后)
cout << *it2 << " " << *it << endl; // 输出:1 2
三、const迭代器的实现:模板复用解决冗余
普通迭代器(iterator
)支持修改元素,但当list
是const
类型时(如const list<int>
),需要只读迭代器(const_iterator
)——即迭代器本身可移动,但指向的元素不能修改。
3.1 痛点:直接复制代码的冗余
如果直接复制ListIterator
改名为ConstListIterator
,并将operator*
和operator->
的返回值改为const
,会导致大量重复代码(如++
、--
、==
等逻辑完全相同):
// 不推荐:重复代码多,维护成本高
template <class T>
struct ConstListIterator {typedef ListNode<T> Node;typedef ConstListIterator<T> Self;Node* _node;ConstListIterator(Node* node) : _node(node) {}// 仅修改返回值为const T&const T& operator*() { return _node->_data; }// 仅修改返回值为const T*const T* operator->() { return &(_node->_data); }// 以下代码与ListIterator完全重复Self& operator++() { _node = _node->_next; return *this; }Self& operator--() { _node = _node->_prev; return *this; }bool operator==(const Self& other) const { return _node == other._node; }// ...
};
3.2 解决方案:增加模板参数,复用迭代器类
核心思路:在迭代器类中增加两个模板参数Ref
(元素引用类型)和Ptr
(元素指针类型),通过传入不同的Ref
和Ptr
,同时生成普通迭代器和const迭代器。
3.2.1 模板化迭代器类实现
// 模板参数:T(元素类型)、Ref(引用类型)、Ptr(指针类型)
template <class T, class Ref, class Ptr>
struct ListIterator {typedef ListNode<T> Node;typedef ListIterator<T, Ref, Ptr> Self; // 迭代器类型自引用Node* _node;// 构造函数ListIterator(Node* node) : _node(node) {}// 1. operator*:返回Ref类型(普通迭代器是T&,const迭代器是const T&)Ref operator*() {return _node->_data;}// 2. operator->:返回Ptr类型(普通迭代器是T*,const迭代器是const T*)Ptr operator->() {return &(_node->_data);}// 3. 移动与比较:逻辑完全复用,无需修改Self& operator++() {_node = _node->_next;return *this;}Self& operator--() {_node = _node->_prev;return *this;}bool operator==(const Self& other) const {return _node == other._node;}bool operator!=(const Self& other) const {return _node != other._node;}// 后置++/--同样复用Self operator++(int) {Self temp(*this);_node = _node->_next;return temp;}Self operator--(int) {Self temp(*this);_node = _node->_prev;return temp;}
};
3.2.2 在list类中定义两种迭代器
在list
类中,通过指定Ref
和Ptr
的类型,分别定义iterator
和const_iterator
:
template <class T>
class List {typedef ListNode<T> Node;
public:// 1. 普通迭代器:Ref=T&,Ptr=T*(支持修改元素)typedef ListIterator<T, T&, T*> iterator;// 2. const迭代器:Ref=const T&,Ptr=const T*(只读元素)typedef ListIterator<T, const T&, const T*> const_iterator;// 3. 获取普通迭代器iterator begin() {return iterator(_head->_next); // 指向第一个数据节点}iterator end() {return iterator(_head); // 指向哨兵位(结束标志)}// 4. 获取const迭代器const_iterator cbegin() const {return const_iterator(_head->_next);}const_iterator cend() const {return const_iterator(_head);}// ... 其他list接口(push_back、insert等,与上一篇一致) ...private:Node* _head; // 哨兵位头节点(私有,隐藏底层)
};
3.2.3 const迭代器的使用示例
当list
为const
类型时,只能使用cbegin()
/cend()
获取const_iterator
,且无法修改元素:
void PrintConstList(const list<int>& l) {// 必须用const_iterator,因为l是constlist<int>::const_iterator it = l.cbegin();while (it != l.cend()) {// *it = 10; // 错误!const迭代器指向的元素不可修改cout << *it << " ";++it; // 迭代器本身可移动(允许++)}
}int main() {list<int> l = {1,2,3};PrintConstList(l); // 输出:1 2 3return 0;
}
核心区别总结:
迭代器类型 | Ref类型 | Ptr类型 | 元素是否可修改 | 迭代器是否可移动 |
---|---|---|---|---|
iterator | T& | T* | 是 | 是 |
const_iterator | const T& | const T* | 否 | 是 |
四、迭代器的封装与STL设计理念
list迭代器的实现,完美体现了STL的两大核心设计理念:
-
封装底层,解耦接口:
- 用户无需关心
list
的节点结构(_prev
/_next
),只需通过begin()
/end()
获取迭代器,用++
/*
操作元素。 - 即使未来修改
list
的底层实现(如改用双向循环链表的其他结构),只要迭代器的接口不变,用户代码无需修改。
- 用户无需关心
-
统一容器访问方式:
- 无论是
vector
(连续内存)、list
(离散节点)还是deque
(双端队列),迭代器的使用方式完全一致:// list遍历 list<int> l = {1,2,3}; for (auto it = l.begin(); it != l.end(); ++it) { ... }// vector遍历(接口完全相同) vector<int> v = {1,2,3}; for (auto it = v.begin(); it != v.end(); ++it) { ... }
- 这种统一性让算法(如
sort
、find
)可以跨容器复用(前提是迭代器类型匹配)。
- 无论是
五、list迭代器与vector迭代器的对比
虽然两者接口一致,但底层实现和能力差异显著,呼应上一篇的“迭代器分类”:
对比维度 | list 迭代器(双向迭代器) | vector 迭代器(随机访问迭代器) |
---|---|---|
底层实现 | 封装ListNode* ,重载运算符 | 原生指针(如int* )或轻量封装 |
支持的移动操作 | 仅支持++it 、--it (双向移动) | 支持++it 、--it 、it+n 、it-n 等 |
解引用效率 | 需访问节点_data (间接访问) | 直接访问内存(效率更高) |
迭代器失效场景 | 仅删除当前节点时失效,其他节点迭代器安全 | 扩容、插入/删除中间元素时,后续迭代器失效 |
适用算法 | 仅支持双向遍历算法(如reverse ) | 支持随机访问算法(如sort 、binary_search ) |
六、完整模拟实现:迭代器+list集成
将上述内容整合,给出list
迭代器与list
类的完整模拟实现
#include <iostream>
#include <assert.h>
using namespace std;// 1. 节点结构
template <class T>
struct ListNode {ListNode<T>* _prev;ListNode<T>* _next;T _data;ListNode(const T& data = T()): _prev(nullptr), _next(nullptr), _data(data){}
};// 2. 模板化迭代器类
template <class T, class Ref, class Ptr>
struct ListIterator {typedef ListNode<T> Node;typedef ListIterator<T, Ref, Ptr> Self;Node* _node;ListIterator(Node* node) : _node(node) {}// 解引用Ref operator*() { return _node->_data; }Ptr operator->() { return &(_node->_data); }// 移动Self& operator++() { _node = _node->_next; return *this; }Self& operator--() { _node = _node->_prev; return *this; }Self operator++(int) { Self temp(*this); _node = _node->_next; return temp; }Self operator--(int) { Self temp(*this); _node = _node->_prev; return temp; }// 比较bool operator==(const Self& other) const { return _node == other._node; }bool operator!=(const Self& other) const { return _node != other._node; }
};// 3. list类
template <class T>
class List {typedef ListNode<T> Node;
public:// 定义两种迭代器typedef ListIterator<T, T&, T*> iterator;typedef ListIterator<T, const T&, const T*> const_iterator;// 构造函数:初始化哨兵位List() {_head = new Node();_head->_prev = _head;_head->_next = _head;}// 尾插(用于测试)void push_back(const T& data) {Node* newNode = new Node(data);Node* tail = _head->_prev;tail->_next = newNode;newNode->_prev = tail;newNode->_next = _head;_head->_prev = newNode;}// 获取迭代器iterator begin() { return iterator(_head->_next); }iterator end() { return iterator(_head); }const_iterator cbegin() const { return const_iterator(_head->_next); }const_iterator cend() const { return const_iterator(_head); }// 析构函数(避免内存泄漏)~List() {clear();delete _head;_head = nullptr;}// 清空数据节点(保留哨兵位)void clear() {iterator it = begin();while (it != end()) {it = erase(it);}}// 删除节点(用于测试)iterator erase(iterator pos) {assert(pos != end()); // 不能删除哨兵位Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;prev->_next = next;next->_prev = prev;delete cur;return iterator(next);}private:Node* _head;
};// 测试代码
int main() {// 1. 测试普通迭代器(修改元素)List<int> l;l.push_back(1);l.push_back(2);l.push_back(3);List<int>::iterator it = l.begin();while (it != l.end()) {*it *= 2; // 修改元素(1→2,2→4,3→6)cout << *it << " ";++it;}cout << endl; // 输出:2 4 6// 2. 测试const迭代器(只读)const List<int> cl = l;List<int>::const_iterator cit = cl.cbegin();while (cit != cl.cend()) {cout << *cit << " ";++cit;}cout << endl; // 输出:2 4 6return 0;
}
我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343
我的C++知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_12880513.html?spm=1001.2014.3001.5482
非常感谢您的阅读,喜欢的话记得三连哦 |