C++ list深度解析:迭代器的类封装设计与反向迭代器的镜像原理

文章目录

  • [一、list 的底层结构](#一、list 的底层结构)
  • [二、list 和 vector 的最大区别:迭代器设计](#二、list 和 vector 的最大区别:迭代器设计)
  • 三、反向迭代器:用正向迭代器包装出来
  • 四、常用接口速查
  • [五、迭代器失效:list 比 vector 简单](#五、迭代器失效:list 比 vector 简单)
  • [六、list 的模拟实现](#六、list 的模拟实现)
    • [6.1 list 本体](#6.1 list 本体)
    • [6.2 为什么哨兵节点让代码更简洁](#6.2 为什么哨兵节点让代码更简洁)
  • [七、vector vs list:选择框架](#七、vector vs list:选择框架)
  • 总结

vector 胜在连续内存和随机访问,但一遇到频繁的中间插入删除就萎了------搬移元素是 O(n) 的硬伤。list 正是为这个场景设计的:底层是带头结点的双向循环链表,任意位置插入删除都是 O(1)。这篇从底层结构拆起,重点讲迭代器的封装设计和反向迭代器的实现技巧,最后给出 vector vs list 的选型决策框架。


一、list 的底层结构

cpp 复制代码
template<class T>
struct __list_node
{
    __list_node<T>* _prev;   // 前驱节点
    __list_node<T>* _next;   // 后继节点
    T _data;                 // 节点数据
};

每个节点是一个独立的堆对象,节点之间通过指针串联。list 本身只持有一个哨兵节点

复制代码
  ┌───────────────────────────────────────────┐
  │          list 对象                          │
  │  ┌─────┐                                  │
  │  │_head│→ [哨兵] ←→ [节点1] ←→ [节点2] ←→ │
  │  └─────┘   ↑___________________________↓   │
  └───────────────────────────────────────────┘

哨兵节点的 _next 指向首节点,_prev 指向尾节点;首节点的 _prev 指向哨兵,尾节点的 _next 指向哨兵。环形结构 让边界条件统一------空链表时哨兵的前后都指向自己,不需要任何 nullptr 判断。


二、list 和 vector 的最大区别:迭代器设计

vector 的迭代器是原生指针 T*,直接 ++it 就是偏移一个元素大小。list 不行------list 的节点在内存中是分散的,++it 需要跳到下一个节点的地址。

list 的迭代器必须是一个类,封装了节点的跳转逻辑

cpp 复制代码
template<class T>
struct __list_iterator
{
    typedef __list_node<T>* NodePtr;
    NodePtr _node;                          // 指向当前节点

    // 解引用 → 返回节点数据的引用
    T& operator*() { return _node->_data; }

    // ++ → 移动到下一个节点
    __list_iterator& operator++()
    {
        _node = _node->_next;
        return *this;
    }

    // -- → 移动到上一个节点
    __list_iterator& operator--()
    {
        _node = _node->_prev;
        return *this;
    }

    bool operator!=(const __list_iterator& it) const
    {
        return _node != it._node;
    }
};

list 迭代器的本质:把「指针」封装成「迭代器对象」 ,让 ++/-- 这些操作被重载为链表节点的前后跳转。对使用者来说 ++it 的语义和 vector 完全一致,但底层实现截然不同------这就是 STL 迭代器抽象的价值。


三、反向迭代器:用正向迭代器包装出来

反向迭代器的核心思路是复用正向迭代器的所有逻辑,只颠倒方向

复制代码
正向遍历: begin → ... → end
反向遍历: rbegin = end的前一个位置 → ... → rend = begin的前一个位置

关键设计:rbegin() 返回的是 end() 的前一个位置,rend() 返回的是 begin() 的前一个位置。这样 ++ 反向迭代器就等价于 -- 正向迭代器。

cpp 复制代码
template<class Iterator>
class ReverseIterator
{
public:
    typedef typename Iterator::Ref Ref;
    typedef typename Iterator::Ptr Ptr;

    ReverseIterator(Iterator it) : _it(it) {}

    // 解引用 ------ 注意:返回的是 _it 的前一个位置的值
    // 因为 rbegin 指向 end(),实际数据在 end() 前面
    Ref operator*()
    {
        Iterator tmp = _it;
        --tmp;
        return *tmp;
    }

    // ++反向迭代器 → --正向迭代器
    ReverseIterator& operator++()
    {
        --_it;
        return *this;
    }

    // --反向迭代器 → ++正向迭代器
    ReverseIterator& operator--()
    {
        ++_it;
        return *this;
    }

    bool operator!=(const ReverseIterator& it) const
    {
        return _it != it._it;
    }

private:
    Iterator _it;   // 包装的正向迭代器
};

解引用返回「前一个位置」的数据,这个设计是反向迭代器里最精妙也最容易写错的地方。想象一下:rbegin() 返回的迭代器内部存的是一个指向哨兵节点的正向迭代器,哨兵没有数据,真正的最后一个数据在哨兵前面的节点里------所以 operator* 必须先 --_it 再返回。

通用性 :这个反向迭代器不依赖任何具体容器------只要求 Iterator 支持 ++--*。所以同一个 ReverseIterator 模板可以同时用于 list 和 vector 的反向迭代器,无需重写。


四、常用接口速查

cpp 复制代码
list<int> l;

// 头尾操作 ------ 全是 O(1)
l.push_back(1);
l.push_front(2);
l.pop_back();
l.pop_front();

// 任意位置插入删除 ------ O(1)
auto it = l.begin();
++it;
l.insert(it, 10);      // 在 it 前面插入 10
it = l.erase(it);       // 删除 it 指向的节点,返回下一个

// front/back ------ 直接访问首尾元素
int val = l.front();
l.back() = 20;

五、迭代器失效:list 比 vector 简单

cpp 复制代码
list<int> l = {1, 2, 3, 4, 5, 6};

auto it = l.begin();
while (it != l.end())
{
    if (*it % 2 == 0)
        it = l.erase(it);   // erase 返回下一个有效迭代器
    else
        ++it;
}

list 的迭代器失效规则:

操作 哪些迭代器失效
insert 不失效------新节点插入链表,已有节点的地址不变
erase 仅被删除的节点失效,其他节点不受影响
push_back/push_front 不失效
pop_back/pop_front 仅被删除的节点失效

和 vector 的最大区别:list 的节点地址在整个生命周期中从不改变------没有扩容搬迁这回事。这是链表结构自带的稳定性。


六、list 的模拟实现

6.1 list 本体

cpp 复制代码
template<class T>
class list
{
    typedef __list_node<T> Node;
public:
    typedef __list_iterator<T, T&, T*> iterator;

    list()
    {
        _head = new Node;         // 创建哨兵节点
        _head->_next = _head;
        _head->_prev = _head;     // 空链表:前后都指向自己
    }

    iterator begin() { return iterator(_head->_next); }
    iterator end()   { return iterator(_head); }  // end 就是哨兵

    void push_back(const T& x)
    {
        insert(end(), x);          // 在哨兵前面插入 = 尾部插入
    }

    void push_front(const T& x)
    {
        insert(begin(), x);        // 在首节点前面插入
    }

    void pop_back()  { erase(--end()); }
    void pop_front() { erase(begin()); }

    // insert:在 pos 前插入,返回新插入节点的迭代器
    iterator insert(iterator pos, const T& x)
    {
        Node* cur = pos._node;
        Node* prev = cur->_prev;
        Node* newnode = new Node(x);

        // 串联四个指针
        prev->_next = newnode;
        newnode->_prev = prev;
        newnode->_next = cur;
        cur->_prev = newnode;

        return iterator(newnode);
    }

    // erase:删除 pos 指向的节点,返回下一个
    iterator erase(iterator pos)
    {
        Node* cur = pos._node;
        Node* prev = cur->_prev;
        Node* next = cur->_next;

        prev->_next = next;
        next->_prev = prev;
        delete cur;

        return iterator(next);
    }

    ~list()
    {
        clear();
        delete _head;
    }

    void clear()
    {
        auto it = begin();
        while (it != end())
            it = erase(it);
    }

private:
    Node* _head;   // 哨兵节点
};

6.2 为什么哨兵节点让代码更简洁

如果没有哨兵节点,空链表时 _headnullptr,每次插入删除都要判断是不是头节点、是不是空链表。有了哨兵节点之后,首节点和中间节点在结构上完全平等------只管插、只管删,空链表时哨兵的前后都指向自己,插入逻辑不需要任何特判。


七、vector vs list:选择框架

维度 vector list
底层 连续动态数组 双向循环链表(带头节点)
随机访问 O(1) O(n)
任意位置插入删除 O(n)(搬移元素) O(1)(调指针)
扩容 需重新分配+拷贝 无需扩容,节点独立
CPU缓存 极好(连续内存) 差(节点分散)
内存开销 元素本身 + 少量预留空间 元素 + 2个指针(16字节)
迭代器 T* 原生指针 封装节点指针的类对象
插入时迭代器 可能全部失效 不失效
删除时迭代器 当前位置及之后失效 仅当前位置失效

决策树

  • 需要 v[100] 这种随机访问 → vector
  • 频繁在中间做插入删除 → list
  • 不确定 → vector(连续内存对CPU缓存的红利远超直觉,大部分场景的插入删除比例不至于让 list 翻身)
  • 元素很大且需要频繁搬移 → list 有优势,因为搬指针比搬数据便宜

总结

三条核心认知:

  1. list 的迭代器不是指针,是类对象 。它封装了链表的跳转逻辑,让 ++/-- 的语义和 vector 保持一致------这是 STL 泛型编程思想的核心体现。
  2. 反向迭代器是对正向迭代器的镜像封装++ 反向就是 -- 正向,解引用时取前一个位置。一套代码适用所有支持双向移动的容器。
  3. 哨兵节点消灭了边界特判。空链表、头插、尾插、中间插入------所有逻辑统一,代码量少一半,bug 也少一半。
相关推荐
_Twink1e6 小时前
openJiuwen 实训营 Day 1 · WorkSwarm 入门教程
开发语言·c++·openjiuwen
dong_junshuai7 小时前
每天一个开源项目#87 fmt:24.5K Stars 的 C++ 格式化核心
c++·开源·github
雪的季节8 小时前
Multisim14.0的详细中文安装步骤【附安装包】
c++
Persistent的粽子!9 小时前
C++:类与对象(二)
开发语言·c++
蒸蒸yyyyzwd9 小时前
cpp 选手备战秋招学习笔记 day23
c++·求职招聘
.YM.Z9 小时前
C++ STL 容器深度剖析:vector 与 list 的模拟实现及对比
开发语言·c++·vector·list
mengzhi啊11 小时前
QEventLoop loop配合loop.exec();替代while(1)。电脑cpu占用为0,使用while(1)cpu占用率100%
c++·qt
HugoStudio_SWAN11 小时前
洛谷 P1319 / P1320 压缩技术——同一枚硬币的正反面
c++·学习·程序人生·算法
小小晓.12 小时前
C++单例模式万字详解:6种实现演进+线程安全彻底解决+CRTP终极模板
c++·单例模式
j7~13 小时前
【C++11】C++11 新特性全套详解(列表初始化、右值引用、lambda 表达式、function 与 bind 包装器等)
c++·右值引用·lambda表达式·可变参数模板·移动语义·包装器·c++11发展史