C++ list 模拟实现(二):const迭代器与反向迭代器,模板的黑魔法
摘要 :上一篇我们手撕了 list 的基础骨架,但很多读者留言说被那个
<T, Ref, Ptr>的三参数模板绕晕了。为什么一个迭代器要传三个参数?const 迭代器是怎么保证数据不被修改的?反向迭代器又是如何"倒着走"的?本文将揭开 STL 泛型编程中最精妙的设计模式。
一、一个让你当场卡壳的面试题
面试官:"你的 list 迭代器,怎么实现 const 版本的?"
候选人 A:"我再写一个 ConstListIterator 类......"
面试官:"那如果还有反向迭代器呢?再写一个 ReverseListIterator?"
候选人 A:"额......那得写好几个类。"
面试官:"STL 源码里只写了一个模板,你看看它是怎么实现的?"
痛点就在这里 :如果普通迭代器、const迭代器、反向迭代器、const反向迭代器都各写一个类,会有大量的代码重复。SGI STL 用模板参数的精妙设计,只写了一份代码,就搞定了所有情况。
二、模板三参数:<T, Ref, Ptr> 的魔法
回顾一下我们上篇写的迭代器:
cpp
template <typename T, typename Ref, typename Ptr>
struct ListIterator {
typedef ListNode<T> Node;
Node* _node;
Ref operator*() { return _node->_data; } // Ref 控制读写的权限
Ptr operator->() { return &(_node->_data); } // Ptr 控制指针类型
// ... ++, --, ==, !=
};
魔法就在于模板参数 Ref 和 Ptr。我们在 list 类中这样定义:
cpp
template <typename T>
class list {
typedef ListNode<T> Node;
public:
// 普通迭代器:T&, T*
typedef ListIterator<T, T&, T*> iterator;
// const迭代器:const T&, const T* ------ 完美复用同一套代码!
typedef ListIterator<T, const T&, const T*> const_iterator;
iterator begin() { return iterator(_head->_next); }
const_iterator begin() const { return const_iterator(_head->_next); }
};
💡 面试金句 :"STL 通过提取
Ref和Ptr作为模板参数,实现了一份代码,多种实例化。这就是泛型编程的威力。"
三、反向迭代器:适配器模式的典范
rbegin() 和 rend() 是怎么实现的?它并不是重新发明一个"倒着走的链表",而是对正向迭代器做了一层封装。
cpp
template <typename Iterator>
class ReverseListIterator {
Iterator _it; // 内部持有一个正向迭代器
public:
ReverseListIterator(Iterator it) : _it(it) {}
// 解引用:实际调用的是正向迭代器的前一个位置
auto operator*() {
Iterator tmp = _it;
return *(--tmp);
}
// ++ 实际上是 --
ReverseListIterator& operator++() {
--_it;
return *this;
}
// -- 实际上是 ++
ReverseListIterator& operator--() {
++_it;
return *this;
}
bool operator!=(const ReverseListIterator& rit) { return _it != rit._it; }
};
这是经典的适配器模式(Adapter Pattern) :在底层迭代器的基础上,通过反转 ++ 和 -- 的语义,把正向遍历变成逆向遍历。
四、完整的测试验证
cpp
void test_iterator_magic() {
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
// 测试 const 迭代器
const list<int>& clt = lt;
list<int>::const_iterator cit = clt.begin();
// *cit = 10; // ❌ 编译报错,完美保护数据!
// 测试反向迭代器
cout << "反向遍历: ";
for (auto rit = lt.rbegin(); rit != lt.rend(); ++rit) {
cout << *rit << " ";
}
cout << endl; // 期望:3 2 1
}
五、面试高频 FAQ
Q1:为什么 const_iterator 不能直接继承 iterator?
虽然在语法上可以继承,但 iterator 的 operator* 返回的是 T&,允许修改数据。const_iterator 必须返回 const T&。通过三参数模板进行实例化,比继承更干净、更符合泛型编程思想。
Q2:反向迭代器的 base() 有什么用?
base() 返回内部持有的正向迭代器。注意一个反直觉的点:rbegin().base() == end()。因为 rbegin() 指向最后一个元素,它的底层正向迭代器其实是 end(),解引用时做 -- 操作。
Q3:为什么 list 的迭代器没有 operator+?
因为 list 是双向链表,不支持随机访问。it + 3 无法在 O(1) 时间内找到目标节点。
六、总结与挑战
| 核心设计 | 实现原理 |
|---|---|
const 迭代器 |
三参数模板 <T, Ref, Ptr> |
| 反向迭代器 | 适配器模式,反转 ++ / -- |
| 代码复用 | 一份模板代码,实例化四种迭代器 |
核心结论:迭代器的封装是 STL 中最考验设计能力的部分。掌握了这一套"黑魔法",你再看其他容器的源码,思路就会清晰很多。
👇 评论区互动 :
你觉得三参数模板 <T, Ref, Ptr> 绕吗?
扣1 觉得绕,但懂了;
扣2 觉得这设计太牛了。
评论区见!
🔥 系列爆款推荐 :
👉 [C++ vector 迭代器失效:一张图+3句话,面试直接背](#C++ vector 迭代器失效:一张图+3句话,面试直接背)
👉 [C++ vector 为什么不用 new\[\]?从源码讲透 allocator 空间配置器](#C++ vector 为什么不用 new[]?从源码讲透 allocator 空间配置器)