深入解剖STL List:从源码剖析到相关接口实现

❤️@燃于AC之乐 来自重庆 计算机专业的一枚大学生

✨专注 C/C++ Linux 数据结构 算法竞赛 AI

🏞️志同道合的人会看见同一片风景!

👇点击进入作者专栏:

《算法画解》

《linux系统编程》

《C++》

🌟《算法画解》算法相关题目点击即可进入实操🌟

感兴趣的可以先收藏起来,请多多支持,还有大家有相关问题都可以给我留言咨询,希望希望共同交流心得,一起进步,你我陪伴,学习路上不孤单!

文章目录

  • 前言
  • [一、STL list概述与设计哲学](#一、STL list概述与设计哲学)
    • [1.1 vector与list的对比](#1.1 vector与list的对比)
    • [1.2 何时使用list?](#1.2 何时使用list?)
  • 二、list节点结构设计
    • [2.1 STL源码中的节点定义](#2.1 STL源码中的节点定义)
    • [2.2 手写实现的节点结构](#2.2 手写实现的节点结构)
  • 三、list迭代器设计精髓
    • [3.1 为什么list需要特殊迭代器?](#3.1 为什么list需要特殊迭代器?)
    • [3.2 STL迭代器设计](#3.2 STL迭代器设计)
    • [3.3 手写迭代器实现](#3.3 手写迭代器实现)
  • 四、核心操作源码剖析
    • [4.1 transfer操作:list的灵魂](#4.1 transfer操作:list的灵魂)
    • [4.2 基于transfer的splice操作](#4.2 基于transfer的splice操作)
    • [4.3 手写实现的插入删除操作](#4.3 手写实现的插入删除操作)
  • 五、list的高级操作
    • [5.1 merge合并操作](#5.1 merge合并操作)
    • [5.2 reverse反转操作](#5.2 reverse反转操作)
    • [5.3 sort排序操作](#5.3 sort排序操作)
  • 六、完整手写list实现
    • [6.1 list类框架](#6.1 list类框架)
    • [6.2 初始化与清理](#6.2 初始化与清理)
    • [6.3 常用接口实现](#6.3 常用接口实现)
  • 七、测试与验证
    • [7.1 基础功能测试](#7.1 基础功能测试)
    • [7.2 迭代器失效测试](#7.2 迭代器失效测试)
    • [7.3 复杂类型支持](#7.3 复杂类型支持)
  • 八、关键特性总结
    • [8.1 list的核心优势](#8.1 list的核心优势)
    • [8.2 与vector的对比选择](#8.2 与vector的对比选择)
    • [8.3 手写实现的收获](#8.3 手写实现的收获)
  • 九、学习建议

前言

STL(Standard Template Library)作为C++标准库的核心组件,其精妙的设计思想和高效的实现一直是C++开发者深入学习的宝藏。在众多容器中,list双向链表以其独特的节点式存储结构和稳定的迭代器特性,展现了与vector截然不同的设计哲学。

本文将从STL源码的深度剖析出发,结合亲手实现的简化版list,带领大家一探双向链表的设计精髓。我们将从节点结构、迭代器设计、核心操作实现等多个维度,层层揭开STL list的神秘面纱,理解其为何能在插入删除操作中保持高效,以及迭代器稳定性的实现机制。

无论你是正在学习数据结构,还是希望深入理解STL内部机制,相信通过本文的源码级分析和实践指导,都能获得新的启发和收获。

深入理解STL双向链表的设计精髓,掌握list容器的底层实现原理

一、STL list概述与设计哲学

1.1 vector与list的对比

STL中的list是双向链表容器,与vector的连续线性空间形成鲜明对比:

特性 vector list
存储方式 连续内存空间 分散节点存储
插入/删除 可能引起内存重分配 O(1)时间复杂度
随机访问 O(1) O(n)
空间使用 可能浪费 精准配置

核心差异list每次插入或删除一个元素,就配置或释放一个元素空间,对空间的运用有绝对的精简。

1.2 何时使用list?

根据《STL源码剖析》的建议,选择容器应考虑:

  • 元素类型和构造复杂度
  • 是否频繁在中间位置插入删除
  • 是否需要随机访问
  • 内存使用效率要求

二、list节点结构设计

2.1 STL源码中的节点定义

STL采用分离设计:list本身和list节点是不同的结构。

cpp 复制代码
// STL源码中的list节点结构
template <class T>
struct __list_node {
    typedef void* void_pointer;
    void_pointer prev;  // 实际应为__list_node<T>*
    void_pointer next;
    T data;
};

这是一个典型的双向链表节点,包含前后指针和数据域。

2.2 手写实现的节点结构

cpp 复制代码
// 手写实现的list节点
template<class T>
struct list_node
{
    T _data;
    list_node<T>* _next;
    list_node<T>* _prev;

    list_node(const T& data = T())
        :_data(data)
        ,_next(nullptr)
        ,_prev(nullptr)
    {}
};

对比分析

  • STL使用void_pointer增强通用性
  • 手写实现直接使用具体指针类型
  • 都包含前驱、后继和数据三个部分

三、list迭代器设计精髓

3.1 为什么list需要特殊迭代器?

list不再能够像vector一样以普通指针作为迭代器,因为:

  1. 节点不连续:不能通过简单指针运算访问相邻元素
  2. 需要特殊操作:递增指向下一个节点,递减指向上一个节点
  3. 需要取值语义:解引用应获取节点数据

3.2 STL迭代器设计

cpp 复制代码
template<class T, class Ref, class Ptr>
struct _list_iterator {
    typedef _list_iterator<T, T&, Ptr> iterator;
    typedef bidirectional_iterator_tag iter_type;  // 双向迭代器标签
    typedef T value_type;
    typedef Ptr pointer;
    typedef Ref reference;
    // ... 其他成员定义
};

重要性质

  • 插入和接合操作不会造成迭代器失效
  • 只有指向被删除元素的迭代器会失效
  • 提供Bidirectional Iterators能力

3.3 手写迭代器实现

cpp 复制代码
template<class T, class Ref, class Ptr>
struct list_iterator
{
    typedef list_node<T> Node;
    typedef list_iterator<T, Ref, Ptr> Self;
    Node* _node;

    list_iterator(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 tmp(*this);
        _node = _node->_next;
        return tmp;
    }

    // 相等性判断
    bool operator!=(const Self& s) const
    {
        return _node != s._node;
    }
};

关键点

  • 迭代器本质是节点的包装器
  • 通过运算符重载模拟指针行为
  • 箭头运算符的简化处理(省略一个->

四、核心操作源码剖析

4.1 transfer操作:list的灵魂

transfer()是list内部最核心的操作,所有复杂操作都基于它:

cpp 复制代码
// STL源码中的transfer实现
void transfer(iterator position, iterator first, iterator last) {
    if (position != last) {
        (*link_type)((*last.node).prev)).next = position.node;  // (1)
        (*link_type)((*first.node).prev)).next = last.node;     // (2)
        link_type tmp = link_type((*position.node).prev);       // (3)
        (*position.node).prev = (*last.node).prev;              // (4)
        (*last.node).prev = (*first.node).prev;                 // (5)
        (*first.node).prev = tmp;                               // (6)
    }
}

作用 :将[first, last)区间内的元素移动到position之前。

4.2 基于transfer的splice操作

splicetransfer的公开接口,用于将元素从一个list移动到另一个:

cpp 复制代码
// 1. 将整个list x接合到position之前
void splice(iterator position, list& x) {
    if (!x.empty()) {
        transfer(position, x.begin(), x.end());
    }
}

// 2. 将单个元素接合到position之前
void splice(iterator position, list&, iterator i) {
    iterator j = i;
    ++j;
    if (position == i || position == j) return;
    transfer(position, i, j);
}

// 3. 将区间接合到position之前
void splice(iterator position, list&, iterator first, iterator last) {
    if (first != last)
        transfer(position, first, last);
}

4.3 手写实现的插入删除操作

cpp 复制代码
// 插入操作
iterator insert(iterator pos, const T& x)
{
    Node* cur = pos._node;
    Node* prev = cur->_prev;
    Node* newnode = new Node(x);

    // 调整指针:prev <-> newnode <-> cur
    newnode->_next = cur;
    cur->_prev = newnode;
    newnode->_prev = prev;
    prev->_next = newnode;

    ++_size;
    return newnode;  // 返回新插入节点的迭代器
}

// 删除操作(注意迭代器失效问题)
iterator erase(iterator pos)
{
    assert(pos != end());  // 不能删除哨兵节点

    Node* prev = pos._node->_prev;
    Node* next = pos._node->_next;

    prev->_next = next;
    next->_prev = prev;
    delete pos._node;

    --_size;
    return next;  // 返回被删除节点的下一个节点
}

重要特性

  • insert后迭代器不失效
  • erase后当前迭代器失效,但返回下一个有效迭代器
  • 需要维护_size计数器

五、list的高级操作

5.1 merge合并操作

cpp 复制代码
template <class T, class Alloc>
void list<T, Alloc>::merge(list<T, Alloc>& x) {
    iterator first1 = begin();
    iterator last1 = end();
    iterator first2 = x.begin();
    iterator last2 = x.end();

    // 前提:两个lists都已经递增排序
    while (first1 != last1 && first2 != last2) {
        if (*first2 < *first1) {
            iterator next = first2;
            transfer(first1, first2, ++next);
            first2 = next;
        }
        else
            ++first1;
    }
    if (first2 != last2) transfer(last1, first2, last2);
}

5.2 reverse反转操作

基于transfer的反转实现,通过逐个移动节点实现整个链表的反转。

5.3 sort排序操作

list的sort通常实现为归并排序,因为链表结构适合归并操作。

六、完整手写list实现

6.1 list类框架

cpp 复制代码
template<class T>
class list
{
    typedef list_node<T> Node;
public:
    // 迭代器类型定义
    typedef list_iterator<T, T&, T*> iterator;
    typedef list_iterator<T, const T&, const T*> const_iterator;

    // 构造函数
    list() { empty_init(); }
    
    // 拷贝构造
    list(const list<T>& lt) { /*...*/ }
    
    // 赋值运算符
    list<T>& operator=(list<T> lt) { swap(lt); return *this; }
    
    // 析构函数
    ~list() { clear(); delete _head; }
    
private:
    Node* _head;     // 哨兵节点
    size_t _size;    // 元素个数
};

6.2 初始化与清理

cpp 复制代码
// 空链表初始化(创建哨兵节点)
void empty_init()
{
    _head = new Node;
    _head->_next = _head;
    _head->_prev = _head;
    _size = 0;
}

// 清空链表(保留哨兵节点)
void clear()
{
    auto it = begin();
    while (it != end())
    {
        it = erase(it);
    }
}

6.3 常用接口实现

cpp 复制代码
// 迭代器相关
iterator begin() { return _head->_next; }
iterator end() { return _head; }

// 容量相关
size_t size() const { return _size; }
bool empty() const { return _size == 0; }

// 元素访问
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()); }

七、测试与验证

7.1 基础功能测试

cpp 复制代码
void test_list1()
{
    list<int> lt;
    lt.push_back(1);
    lt.push_back(2);
    lt.push_back(3);
    lt.push_back(4);

    // 迭代器遍历
    list<int>::iterator it = lt.begin();
    while (it != lt.end()) {
        *it += 10;  // 修改元素
        cout << *it << " ";
        ++it;
    }
    cout << endl;
    
    // 范围for遍历
    for (auto e : lt) {
        cout << e << " ";
    }
    cout << endl;
}

7.2 迭代器失效测试

cpp 复制代码
void test_list2()
{
    list<int> lt;
    lt.push_back(1);
    lt.push_back(2);
    lt.push_back(3);
    lt.push_back(4);

    // insert后迭代器不失效
    list<int>::iterator it = lt.begin();
    lt.insert(it, 10);
    *it += 100;  // 仍然有效
    
    // erase后迭代器失效,需要接收返回值
    it = lt.begin();
    while (it != lt.end()) {
        if (*it % 2 == 0) {
            it = lt.erase(it);  // 正确写法
        } else {
            ++it;
        }
    }
}

7.3 复杂类型支持

cpp 复制代码
struct AA
{
    int _a1 = 1;
    int _a2 = 2;
};

void test_complex_type()
{
    list<AA> lta;
    lta.push_back(AA());
    
    list<AA>::iterator ita = lta.begin();
    // 箭头运算符简化:ita->_a1 等价于 ita.operator->()->_a1
    cout << ita->_a1 << ":" << ita->_a2 << endl;
}

八、关键特性总结

8.1 list的核心优势

  1. 插入删除高效:任意位置O(1)复杂度
  2. 空间利用精准:按需分配,无内存浪费
  3. 迭代器稳定:插入操作不会使迭代器失效
  4. 支持双向遍历:Bidirectional Iterators

8.2 与vector的对比选择

场景 推荐容器 理由
频繁随机访问 vector 连续内存,缓存友好
频繁中间插入删除 list 无需移动元素
内存紧张 list 精准分配
需要排序 看情况 list排序慢但稳定

8.3 手写实现的收获

通过手写list实现,我们深刻理解了:

  • 哨兵节点的巧妙设计
  • 迭代器与容器的分离
  • 异常安全的重要性
  • 模板编程的实际应用

九、学习建议

  1. 先理解原理,再阅读源码:STL源码为了效率做了很多优化,直接阅读可能困难
  2. 动手实现一遍:只有自己实现过,才能真正理解设计精妙之处
  3. 关注迭代器失效规则:这是使用STL容器最容易出错的地方
  4. 理解空间与时间的权衡:不同容器适合不同场景,没有绝对的好坏

参考资料:

  1. 《STL源码剖析》- 侯捷
  2. 《Effective STL》- Scott Meyers
  3. C++标准文档

通过深入剖析STL list的源码和手写实现,我们不仅掌握了双向链表的数据结构,更理解了STL设计哲学:效率、通用性、安全性的完美平衡。希望这篇文章能帮助你更好地理解和使用STL list容器!

加油!志同道合的人会看到同一片风景。

看到这里请点个赞关注 ,如果觉得有用就收藏一下吧。后续还会持续更新的。 创作不易,还请多多支持!

相关推荐
汉克老师6 小时前
GESP2025年6月认证C++二级( 第一部分选择题(9-15))
c++·循环结构·求余·gesp二级·gesp2级·整除、
不想睡觉_6 小时前
优先队列priority_queue
c++·算法
rainbow688916 小时前
EffectiveC++入门:四大习惯提升代码质量
c++
秋邱17 小时前
用 Python 写出 C++ 的性能?用CANN中PyPTO 算子开发硬核上手指南
开发语言·c++·python
我在人间贩卖青春17 小时前
C++之析构函数
c++·析构函数
我在人间贩卖青春17 小时前
C++之数据类型的扩展
c++·字符串·数据类型
wangjialelele18 小时前
平衡二叉搜索树:AVL树和红黑树
java·c语言·开发语言·数据结构·c++·算法·深度优先
苏宸啊18 小时前
C++栈和队列
c++
森G18 小时前
七、04ledc-sdk--------makefile有变化
linux·c语言·arm开发·c++·ubuntu