C++ list 容器完全指南:从入门到手撕双向链表

1.1 list 的基本概念

list 是 C++ 标准模板库(STL)中的一种序列容器,其底层实现是一个带头结点的双向循环链表。这意味着:

  • 每个元素(节点)包含三部分:前驱指针数据后继指针

  • 链表的头节点不存储有效数据,仅作为哨兵,简化边界操作。

  • 最后一个节点的后继指向头节点,头节点的前驱指向尾节点,形成循环。

这种结构赋予了 list 两个核心特性:

  • 任意位置插入/删除 O(1)(前提是已经找到位置)。

  • 不支持随机访问 (不能使用 []+ 偏移)。

1.2 为什么有了 vector 还需要 list?

操作 vector list
尾插 O(1) 均摊 O(1)
中间插入 O(n)(需搬移元素) O(1)(只需改指针)
随机访问 O(1) O(n)
缓存友好 高(连续内存) 低(节点分散)

因此,当你的程序需要频繁在中间位置插入删除 ,并且不关心随机访问 时,list 是更合适的选择。例如:实现 LRU 缓存、消息队列、邻接表等。

二、list 的基本使用

使用 list 需要包含 <list> 头文件。

2.1 构造方式

复制代码
#include <list>
#include <iostream>
using namespace std;

int main() {
    list<int> l1;                    // 空链表
    list<int> l2(5, 10);             // 5 个节点,值均为 10
    list<int> l3(l2);                // 拷贝构造
    list<int> l4(l2.begin(), l2.end()); // 迭代器区间构造

    int arr[] = {1,2,3,4,5};
    list<int> l5(arr, arr+5);        // 数组构造

    for (auto x : l5) cout << x << " "; // 1 2 3 4 5
    return 0;
}

2.2 迭代器与遍历

list 的迭代器是双向迭代器 ,支持 ++--,但不支持 +n-n

迭代器 说明
begin() / end() 正向迭代器,begin 指向第一个有效节点,end 指向头节点(哨兵)。
rbegin() / rend() 反向迭代器,rbegin 指向最后一个节点,rend 指向头节点之前的位置。

遍历方式示例:

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

// 正向迭代器
for (list<int>::iterator it = l.begin(); it != l.end(); ++it)
    cout << *it << " ";

// 反向迭代器(逆向遍历)
for (list<int>::reverse_iterator rit = l.rbegin(); rit != l.rend(); ++rit)
    cout << *rit << " ";   // 5 4 3 2 1

// 范围 for
for (auto e : l) cout << e << " ";

注意++ 操作对于 list 迭代器是 O(1) 的,但它是顺着节点的 next 指针移动,不是简单的地址加常数。因此不要试图用 it + 5,编译器会报错。

2.3 容量与元素访问

复制代码
list<int> l = {10,20,30};
cout << l.front() << endl;   // 10
cout << l.back() << endl;    // 30
函数 作用
empty() 判断是否为空
size() 返回节点个数(O(n),因为链表通常不存储长度变量?实际上大多数实现中 size 是缓存好的,O(1))
front() 返回第一个元素的引用
back() 返回最后一个元素的引用

2.4 修改操作:增删改查

函数 说明
push_front(val) 头插
pop_front() 头删
push_back(val) 尾插
pop_back() 尾删
insert(pos, val) 在迭代器 pos 之前插入 val
erase(pos) 删除迭代器 pos 处的节点
clear() 清空所有有效节点
swap(list&) 交换两个链表的内容(只是交换头指针,O(1))

代码示例:

复制代码
list<int> l;
l.push_back(1);   // 1
l.push_front(0);  // 0 1
l.push_back(2);   // 0 1 2
l.pop_front();    // 1 2
l.pop_back();     // 1

auto it = l.begin();
l.insert(it, 10);   // 在第一个元素前插入 10 -> 10 1

it = l.begin();
++it;
l.erase(it);        // 删除第二个元素(1) -> 10

for (auto e : l) cout << e << " ";  // 10

三、list 迭代器失效 ------ 与 vector 的重大区别

迭代器失效是指迭代器原本指向的元素被销毁,导致迭代器不再有效。由于 listvector 的底层结构不同,失效规则也完全不同。

3.1 list 的失效规则

  • 插入操作insertpush_frontpush_back不会导致任何现有迭代器失效。因为插入只是创建新节点、调整相邻节点的指针,原节点在内存中的位置不变。

  • 删除操作erasepop_frontpop_back 会使指向被删除节点的迭代器失效,但其他迭代器(包括被删除节点前后的节点)仍然有效。

3.2 典型错误示例

复制代码
list<int> l = {1,2,3,4,5};
auto it = l.begin();
while (it != l.end()) {
    l.erase(it);   // 删除 it 指向的节点,it 立即失效
    ++it;          // 错误!对失效迭代器执行 ++ 未定义行为
}

3.3 正确删除所有节点

方法一:利用 erase 返回下一个有效迭代器

复制代码
while (it != l.end()) {
    it = l.erase(it);   // erase 返回被删节点的下一个位置
}

方法二:使用 it++ 技巧(先拷贝,再递增原迭代器)

复制代码
while (it != l.end()) {
    l.erase(it++);   // 先用 it 的副本删除,然后 it 自增到下一个节点
}

两种方式均可安全删除所有节点。建议使用第一种,语义更清晰。

3.4 对比 vector

操作 vector 迭代器失效 list 迭代器失效
尾插(未扩容) 所有迭代器有效 所有迭代器有效
尾插(扩容) 所有迭代器失效 不适用(list 不扩容)
中间插入 插入点之后所有迭代器失效 仅新节点自己?不,插入不影响已有迭代器
删除 删除点之后所有迭代器失效 仅被删节点的迭代器失效

因此,list 在需要频繁修改、且希望迭代器保持稳定的场景下非常有优势。

四、list 的模拟实现 ------ 揭开底层面纱

为了真正理解 list,我们尝试手写一个简化版,命名为 bit::list。主要实现:节点结构、迭代器封装、基本增删查改。

4.1 节点结构

复制代码
namespace bit {
    template<class T>
    struct _list_node {
        _list_node<T>* _prev;
        _list_node<T>* _next;
        T _data;

        _list_node(const T& val = T())
            : _prev(nullptr), _next(nullptr), _data(val)
        {}
    };
}

4.2 迭代器封装

list 的迭代器不能是原生指针,因为节点在内存中不连续。我们需要封装一个类,重载 ++--*-> 等运算符。

复制代码
template<class T>
struct _list_iterator {
    typedef _list_node<T> node;
    typedef _list_iterator<T> self;

    node* _pnode;

    _list_iterator(node* p) : _pnode(p) {}

    // 解引用
    T& operator*() { return _pnode->_data; }
    T* operator->() { return &_pnode->_data; }

    // 前置++
    self& operator++() {
        _pnode = _pnode->_next;
        return *this;
    }
    // 后置++
    self operator++(int) {
        self tmp(*this);
        _pnode = _pnode->_next;
        return tmp;
    }
    // 前置--
    self& operator--() {
        _pnode = _pnode->_prev;
        return *this;
    }
    // 后置--
    self operator--(int) {
        self tmp(*this);
        _pnode = _pnode->_prev;
        return tmp;
    }

    bool operator==(const self& other) const { return _pnode == other._pnode; }
    bool operator!=(const self& other) const { return _pnode != other._pnode; }
};

4.3 list 主体

复制代码
template<class T>
class list {
public:
    typedef _list_node<T> node;
    typedef _list_iterator<T> iterator;

private:
    node* _head;   // 哨兵头节点(不存储有效数据)
    size_t _size;  // 可选,方便 size() O(1)

public:
    // 构造
    list() {
        _head = new node;
        _head->_prev = _head;
        _head->_next = _head;
        _size = 0;
    }

    // 拷贝构造
    list(const list<T>& other) : _head(nullptr), _size(0) {
        _head = new node;
        _head->_prev = _head;
        _head->_next = _head;
        for (auto e : other) push_back(e);
    }

    // 析构
    ~list() {
        clear();
        delete _head;
        _head = nullptr;
    }

    // 赋值运算符(现代写法)
    list<T>& operator=(list<T> other) {
        swap(other);
        return *this;
    }

    void swap(list<T>& other) {
        std::swap(_head, other._head);
        std::swap(_size, other._size);
    }

    // 迭代器
    iterator begin() { return iterator(_head->_next); }
    iterator end() { return iterator(_head); }

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

    // 元素访问
    T& front() { return *begin(); }
    T& back() { return *(--end()); }

    // 修改
    void push_back(const T& val) { insert(end(), val); }
    void push_front(const T& val) { insert(begin(), val); }
    void pop_back() { erase(--end()); }
    void pop_front() { erase(begin()); }

    iterator insert(iterator pos, const T& val) {
        node* cur = pos._pnode;
        node* prev = cur->_prev;
        node* new_node = new node(val);
        // 链接
        prev->_next = new_node;
        new_node->_prev = prev;
        new_node->_next = cur;
        cur->_prev = new_node;
        ++_size;
        return iterator(new_node);
    }

    iterator erase(iterator pos) {
        node* cur = pos._pnode;
        node* prev = cur->_prev;
        node* next = cur->_next;
        prev->_next = next;
        next->_prev = prev;
        delete cur;
        --_size;
        return iterator(next);
    }

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

4.4 反向迭代器的实现(了解)

STL 中反向迭代器可以通过适配正向迭代器来实现:

复制代码
template<class Iterator>
class ReverseListIterator {
    Iterator _it;
public:
    ReverseListIterator(Iterator it) : _it(it) {}
    auto operator*() {
        Iterator tmp = _it;
        --tmp;
        return *tmp;
    }
    ReverseListIterator& operator++() { --_it; return *this; }
    ReverseListIterator operator++(int) { auto tmp = *this; --_it; return tmp; }
    // 其它运算符类似 ...
};

五、list 与 vector 对比:一张表看懂

对比维度 vector list
底层结构 连续数组 带头双向循环链表
随机访问 O(1) O(n)
头部插入/删除 O(n)(搬移所有元素) O(1)
中间插入/删除 O(n)(搬移后续元素) O(1)(前提已知位置)
尾部插入/删除 O(1)(均摊) O(1)
内存占用 紧凑,无额外指针 每个元素多两个指针(8或16字节额外开销)
缓存命中率 极高(连续内存) 极低(节点分散,跳转多)
迭代器类型 随机访问迭代器 双向迭代器
插入时迭代器失效 扩容时全部失效 不失效
删除时迭代器失效 被删元素及之后全部失效 仅被删元素失效
适用场景 需要快速随机访问,元素数量相对稳定,尾部操作多 频繁在任意位置插入删除,不关心随机访问,如链表、LRU、队列

六、常见误区与最佳实践

6.1 避免对 list 使用 std::sort

std::sort 要求随机访问迭代器,而 list 只提供双向迭代器。如果想对 list 排序,应使用其成员函数 sort()(归并排序,O(n log n))。

复制代码
list<int> l = {5,2,8,1,4};
l.sort();   // 正确,1 2 4 5 8
// std::sort(l.begin(), l.end()); // 错误,编译失败

6.2 慎用 size() 在 O(n) 实现中

早期某些 STL 实现中,list::size() 是 O(n) 的(需要遍历链表)。虽然 C++11 要求为 O(1),但在某些旧环境或特定实现下仍可能存在。如果必须反复获取大小,可以自己维护一个变量。

6.3 利用 splice 高效转移节点

list 提供了 splice 操作,可以在 O(1) 时间内将一个链表的节点转移到另一个链表,无需复制数据。这是 vector 无法做到的。

复制代码
list<int> l1 = {1,2,3};
list<int> l2 = {4,5};
auto it = l1.begin();
++it;   // 指向 2
l1.splice(it, l2);   // 将 l2 全部节点插入到 it 之前,l2 变为空
// l1 变为 1 4 5 2 3

七、总结

  • list 的核心优势:O(1) 的任意位置插入删除,迭代器在插入时永不失效。

  • 核心劣势:不支持随机访问,缓存不友好,每个元素额外占用指针内存。

  • 使用场景:适合需要频繁增删且不关心索引的场景,如 LRU 缓存、消息队列、邻接表等。

  • 迭代器失效:只有删除操作会让指向被删节点的迭代器失效,插入不会。

  • 模拟实现 :理解 list 的节点结构、迭代器封装和双向指针维护,是迈向高手的重要一步。

学习 list 不仅能让你熟练使用 STL,更能加深对链式数据结构、迭代器设计模式的理解。下一篇文章我们将深入 deque 容器------它既是"双端队列",也是 stackqueue 的底层基石,敬请期待!

练习题推荐

  • LeetCode 146. LRU 缓存(需要用 list 或手写链表)

  • LeetCode 2. 两数相加(链表操作)

  • LeetCode 25. K 个一组翻转链表(链表综合题)

相关推荐
handler011 小时前
【Linux 网络】:poll/epoll 底层机制与 Reactor 并发模型
linux·运维·服务器·网络·c++·多路转接·多路复用
cpp_25011 小时前
P10109 [GESP202312 六级] 工作沟通
数据结构·c++·算法·题解·洛谷·gesp六级
Xeon_CC1 小时前
vs2026远程开发debian12容器的C++程序笔记
开发语言·c++·笔记
玉树临风ives1 小时前
atcoder ABC 460 题解
数据结构·c++·算法
少司府1 小时前
C++进阶:二叉搜索树
开发语言·数据结构·c++·二叉树·stl·二叉搜索树·tree
devpotato1 小时前
ArrayList 扩容机制:从源码细节到工程实践
java·list
Huangjin007_1 小时前
【C++ STL篇(十四)】哈希表实现:开放定址法与链地址法
c++·哈希算法·散列表
承渊政道1 小时前
【MySQL数据库学习】MySQL表的约束(上)
数据库·c++·学习·mysql·bash·数据库架构·数据库系统
minji...1 小时前
Linux高级IO(六)基于ET模式、单reactor反应堆的epoll版本的TCP计算服务器
linux·服务器·网络·c++·epoll·socket套接字·reactor反应堆模式