C++从入门到实战(二十一)List迭代器实现
- 前言
- 一、为什么需要自定义list迭代器?
- 二、list迭代器的核心:运算符重载
-
- [2.1 基础准备:节点结构回顾](#2.1 基础准备:节点结构回顾)
- [2.2 迭代器类的初步实现(普通迭代器)](#2.2 迭代器类的初步实现(普通迭代器))
- [2.3 关键运算符详解:operator* 与 operator->](#2.3 关键运算符详解:operator* 与 operator->)
-
- [2.3.1 案例:遍历存储自定义类型的list](#2.3.1 案例:遍历存储自定义类型的list)
- [2.3.2 用operator*访问成员](#2.3.2 用operator*访问成员)
- [2.3.3 用operator->访问成员(编译器优化)](#2.3.3 用operator->访问成员(编译器优化))
- [2.3.4 常见错误:直接用节点指针访问](#2.3.4 常见错误:直接用节点指针访问)
- [2.4 补充:后置++与后置--的重载](#2.4 补充:后置++与后置--的重载)
- 三、const迭代器的实现:模板复用解决冗余
-
- [3.1 痛点:直接复制代码的冗余](#3.1 痛点:直接复制代码的冗余)
- [3.2 解决方案:增加模板参数,复用迭代器类](#3.2 解决方案:增加模板参数,复用迭代器类)
-
- [3.2.1 模板化迭代器类实现](#3.2.1 模板化迭代器类实现)
- [3.2.2 在list类中定义两种迭代器](#3.2.2 在list类中定义两种迭代器)
- [3.2.3 const迭代器的使用示例](#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
的节点结构,迭代器将围绕节点指针展开:
cpp
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>*
并重载关键运算符:
cpp
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
的对象:
cpp
#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
对象的引用,再用.
访问成员:
cpp
// 普通迭代器遍历
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->
访问成员,代码会更简洁,但背后有编译器的特殊优化:
cpp
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*
遍历,代码会暴露底层结构,且可读性差:
cpp
// 不推荐:直接操作节点指针,暴露底层实现
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
作为参数(无实际意义,仅用于区分前置和后置):
cpp
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; // 返回移动前的状态
}
};
使用示例:
cpp
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
,会导致大量重复代码(如++
、--
、==
等逻辑完全相同):
cpp
// 不推荐:重复代码多,维护成本高
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 模板化迭代器类实现
cpp
// 模板参数: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
:
cpp
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
,且无法修改元素:
cpp
void PrintConstList(const list<int>& l) {
// 必须用const_iterator,因为l是const
list<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 3
return 0;
}
核心区别总结:
迭代器类型 | Ref类型 | Ptr类型 | 元素是否可修改 | 迭代器是否可移动 |
---|---|---|---|---|
iterator |
T& |
T* |
是 | 是 |
const_iterator |
const T& |
const T* |
否 | 是 |
四、迭代器的封装与STL设计理念
list迭代器的实现,完美体现了STL的两大核心设计理念:
-
封装底层,解耦接口:
- 用户无需关心
list
的节点结构(_prev
/_next
),只需通过begin()
/end()
获取迭代器,用++
/*
操作元素。 - 即使未来修改
list
的底层实现(如改用双向循环链表的其他结构),只要迭代器的接口不变,用户代码无需修改。
- 用户无需关心
-
统一容器访问方式:
-
无论是
vector
(连续内存)、list
(离散节点)还是deque
(双端队列),迭代器的使用方式完全一致:cpp// 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
类的完整模拟实现
cpp
#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 6
return 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
|--------------------|
| 非常感谢您的阅读,喜欢的话记得三连哦 |
