de风——【从零开始学C++】(十一):list的基本使用和模拟实现


前言

噔噔哒!天空一声巨响,小小风闪亮登场!大家好呀,今天是【从零开始学 C++】专题的第十一篇,我们来学习 STL 中另一个非常重要的容器 ------list

如果你已经学过 vector,那今天的内容你会觉得既熟悉又陌生。熟悉是因为它们都是容器,接口长得很像;陌生是因为它们的底层实现完全不同,一个是数组,一个是链表。

💡 面试高频考点:vector 和 list 的区别,这是面试必考题!今天我会用大白话给你讲透,保证看完就能答上来。


第一部分:list 的基本使用

(一)list 的介绍和特点

简介作用

list 就是双向链表!

还记得数据结构里的链表吗?每个节点存着数据和两个指针,一个指向前一个节点,一个指向后一个节点。STL 里的 list 就是用带头双向循环链表实现的。

和 vector 对比:

|--------|----------------------|------------------|
| 特性 | vector(数组) | list(链表) |
| 底层 | 连续的数组空间 | 不连续的节点,靠指针连接 |
| 随机访问 | ✅ 支持 \[\] 直接访问,O (1) | ❌ 不支持,必须遍历,O (n) |
| 中间插入删除 | ❌ 要移动元素,O (n) | ✅ 只改指针,O (1) |
| 空间浪费 | 会预留空间,可能浪费 | 每个节点多两个指针开销 |

大白话总结:

  • vector 像高铁:座位是连续的,找第 10 排直接走过去(快),但中间插个人要让所有人挪位置(慢)

  • list 像珍珠项链:珠子不连续,靠线连,找第 10 颗得一颗颗数(慢),但中间插颗珠子只需要改两根线(快)

什么时候用 list?

  • 需要频繁在头部或中间插入删除元素

  • 不需要随机访问元素

  • 元素较大,拷贝代价高


(二)list 的构造函数

简介作用

list 的构造函数和 vector 基本一样,毕竟都是 STL 容器,设计风格统一。

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

int main() {
    // 示例1:无参构造 - 创建空的list
    list<int> l1;
    cout << "l1的大小:" << l1.size() << endl; // 0

    // 示例2:n个元素构造 - 创建包含5个元素的list,每个都是默认值
    list<int> l2(5); // 5个0
    
    // 示例3:n个指定值构造 - 创建包含5个10的list
    list<int> l3(5, 10); // 5个10
    
    // 示例4:迭代器区间构造 - 用另一个容器的区间初始化
    int arr[] = {1, 2, 3, 4, 5};
    list<int> l4(arr, arr + 5); // 用数组初始化
    
    // 示例5:拷贝构造 - 用另一个list拷贝
    list<int> l5(l4); // 和l4一模一样
    
    return 0;
}
🐛 经典 Bug 和坑点

坑点 1:注意区分构造和赋值

cpp 复制代码
list<int> l(5, 10); // 正确:构造5个10
// list<int> l();     // 错误!这是函数声明,不是构造空对象
list<int> l;         // 正确:构造空对象

(三)list 的迭代器

简介作用

重点来了!list 的迭代器不是原生指针 ,这是和 vector 最大的区别之一!

  • vector 的迭代器本质就是原生指针,因为底层是连续数组

  • list 的迭代器是一个封装了节点指针的类,因为节点不连续,不能直接 ++--

迭代器类型:

  • 正向迭代器:iterator

  • const 正向迭代器:const_iterator(只读)

  • 反向迭代器:reverse_iterator

  • const 反向迭代器:const_reverse_iterator

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

int main() {
    list<int> l;
    for (int i = 1; i <= 5; i++) {
        l.push_back(i);
    }

    // 示例1:正向迭代器遍历
    cout << "正向遍历:";
    list<int>::iterator it = l.begin();
    while (it != l.end()) {
        cout << *it << " ";
        ++it; // 可以++,但不能it += 2!
    }
    cout << endl;

    // 示例2:const迭代器(只读)
    cout << "const迭代器:";
    list<int>::const_iterator cit = l.cbegin();
    while (cit != l.cend()) {
        cout << *cit << " ";
        // *cit = 10; // 错误!const迭代器不能修改
        ++cit;
    }
    cout << endl;

    // 示例3:反向迭代器
    cout << "反向遍历:";
    list<int>::reverse_iterator rit = l.rbegin();
    while (rit != l.rend()) {
        cout << *rit << " ";
        ++rit;
    }
    cout << endl;

    return 0;
}
🐛 经典 Bug 和坑点

坑点 1:list 迭代器不支持随机访问!

cpp 复制代码
list<int>::iterator it = l.begin();
++it;       // ✅ 支持
--it;       // ✅ 支持
// it += 3;  // ❌ 不支持!链表不能跳
// it[2];    // ❌ 不支持!

坑点 2:迭代器类型不匹配

cpp 复制代码
// const对象只能用const迭代器
const list<int> cl(l);
// list<int>::iterator it = cl.begin(); // ❌ 错误!
list<int>::const_iterator it = cl.begin(); // ✅ 正确

💡 面试考点:为什么 list 迭代器不是原生指针? 因为链表节点在内存中不连续,原生指针 ++ 会跳到下一个字节而不是下一个节点!必须封装成类,重载 ++ 运算符让它指向下一个节点。


(四)list 的容量和元素访问

简介作用

list 没有 capacity 和 reserve,因为链表不需要预分配空间!

常用接口:

  • size():返回元素个数

  • empty():判断是否为空

  • front():返回第一个元素的引用

  • back():返回最后一个元素的引用

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

int main() {
    list<int> l;
    
    cout << "是否为空:" << (l.empty() ? "是" : "否") << endl; // 是
    cout << "元素个数:" << l.size() << endl; // 0
    
    for (int i = 1; i <= 5; i++) {
        l.push_back(i);
    }
    
    cout << "是否为空:" << (l.empty() ? "是" : "否") << endl; // 否
    cout << "元素个数:" << l.size() << endl; // 5
    
    // 访问首尾元素
    cout << "第一个元素:" << l.front() << endl; // 1
    cout << "最后一个元素:" << l.back() << endl; // 5
    
    // 可以修改
    l.front() = 100;
    l.back() = 200;
    cout << "修改后首元素:" << l.front() << endl; // 100
    cout << "修改后尾元素:" << l.back() << endl; // 200
    
    return 0;
}

⚠️ 注意:list不支持 \[\] 随机访问,也没有 at () 函数!要访问中间元素只能遍历。


(五)list 的增删查改接口

1. push_back/pop_back、push_front/pop_front
简介作用

这是 list 的强项!头尾插入删除都是 O (1),比 vector 快多了。

vector 的 push_front 要移动所有元素,O (n),而 list 只需要改指针。

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

void printList(const list<int>& l) {
    for (auto x : l) {
        cout << x << " ";
    }
    cout << endl;
}

int main() {
    list<int> l;
    
    // 尾插尾删
    l.push_back(1);
    l.push_back(2);
    l.push_back(3);
    cout << "push_back后:";
    printList(l); // 1 2 3
    
    l.pop_back();
    cout << "pop_back后:";
    printList(l); // 1 2
    
    // 头插头删(vector没有push_front!)
    l.push_front(0);
    l.push_front(-1);
    cout << "push_front后:";
    printList(l); // -1 0 1 2
    
    l.pop_front();
    cout << "pop_front后:";
    printList(l); // 0 1 2
    
    return 0;
}

2. insert、erase
简介作用

insert:在指定位置插入元素 erase:删除指定位置的元素

这两个是 list 的核心优势!在任意位置插入删除都是 O (1)(前提是已经找到位置)。

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

void printList(const list<int>& l) {
    for (auto x : l) {
        cout << x << " ";
    }
    cout << endl;
}

int main() {
    list<int> l;
    for (int i = 1; i <= 5; i++) {
        l.push_back(i);
    }
    cout << "原始:";
    printList(l); // 1 2 3 4 5
    
    // 找到第3个元素的位置
    list<int>::iterator it = l.begin();
    ++it; ++it; // 现在指向3
    
    // 示例1:在it位置插入一个元素
    l.insert(it, 30); // 在3前面插入30
    cout << "insert后:";
    printList(l); // 1 2 30 3 4 5
    
    // 示例2:在it位置插入n个元素
    it = l.begin();
    l.insert(it, 3, 0); // 在开头插入3个0
    cout << "insert n个后:";
    printList(l); // 0 0 0 1 2 30 3 4 5
    
    // 示例3:删除it位置的元素
    it = l.begin();
    ++it; // 指向第二个0
    l.erase(it);
    cout << "erase后:";
    printList(l); // 0 0 1 2 30 3 4 5
    
    // 示例4:删除区间元素
    it = l.begin();
    auto end = l.begin();
    ++end; ++end; // 指向1
    l.erase(it, end); // 删除[begin, end)区间
    cout << "erase区间后:";
    printList(l); // 1 2 30 3 4 5
    
    return 0;
}
🐛 经典 Bug 和坑点

坑点:erase 会导致迭代器失效!

cpp 复制代码
list<int>::iterator it = l.begin();
l.erase(it);  // 删除后it就失效了!
// ++it;       // ❌ 错误!it已经失效

正确写法:

cpp 复制代码
it = l.erase(it); // erase会返回下一个有效迭代器

💡 注意:list 的 insert不会导致其他迭代器失效,这和 vector 不一样!


3. swap、clear
简介作用
  • swap:交换两个 list 的内容,O (1)!只交换头节点指针

  • clear:清空所有元素

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

void printList(const list<int>& l, const string& name) {
    cout << name << ": ";
    for (auto x : l) {
        cout << x << " ";
    }
    cout << endl;
}

int main() {
    list<int> l1 = {1, 2, 3};
    list<int> l2 = {10, 20, 30, 40};
    
    printList(l1, "l1"); // l1: 1 2 3
    printList(l2, "l2"); // l2: 10 20 30 40
    
    // 交换两个list
    l1.swap(l2);
    
    printList(l1, "交换后l1"); // 交换后l1: 10 20 30 40
    printList(l2, "交换后l2"); // 交换后l2: 1 2 3
    
    // 清空
    l1.clear();
    cout << "clear后l1的大小:" << l1.size() << endl; // 0
    
    return 0;
}

4. sort、reverse、unique(list 特有接口)
简介作用

为什么 list 要有自己的 sort? 因为算法库的 sort 要求随机访问迭代器,list 不支持,所以必须自己实现。

  • sort():排序(默认升序)

  • reverse():反转链表

  • unique():去重(注意:只会去掉相邻的重复元素,所以一般先排序再去重)

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

void printList(const list<int>& l) {
    for (auto x : l) {
        cout << x << " ";
    }
    cout << endl;
}

int main() {
    list<int> l = {3, 1, 4, 1, 5, 9, 2, 6};
    cout << "原始:";
    printList(l); // 3 1 4 1 5 9 2 6
    
    // 反转
    l.reverse();
    cout << "reverse后:";
    printList(l); // 6 2 9 5 1 4 1 3
    
    // 排序
    l.sort();
    cout << "sort后:";
    printList(l); // 1 1 2 3 4 5 6 9
    
    // 去重(必须先排序!)
    l.unique();
    cout << "unique后:";
    printList(l); // 1 2 3 4 5 6 9
    
    // 自定义排序:降序
    l.sort(greater<int>());
    cout << "降序排序:";
    printList(l); // 9 6 5 4 3 2 1
    
    return 0;
}
🐛 经典 Bug 和坑点

坑点:unique 只去相邻重复!

cpp 复制代码
list<int> l = {1, 2, 1, 2, 1};
l.unique(); // 结果还是{1, 2, 1, 2, 1},因为没有相邻重复!
// 正确做法:先排序再去重
l.sort();
l.unique(); // 结果{1, 2}

(六)vector 和 list 的对比总结(面试必背)

|-----------|--------------------------|----------------------------|
| 对比项 | vector | list |
| 底层结构 | 动态连续数组 | 带头双向循环链表 |
| 随机访问 | ✅ 支持 \[\],O (1) | ❌ 不支持,O (n) |
| 插入删除 | 头尾 O (1),中间 O (n) | 任意位置 O (1) |
| 迭代器类型 | 随机访问迭代器(原生指针) | 双向迭代器(封装类) |
| 空间利用率 | 高,会预留空间 | 低,每个节点多两个指针 |
| 缓存利用率 | 高,空间连续 | 低,节点分散 |
| 迭代器失效 | insert/erase 可能导致所有迭代器失效 | erase 只导致被删节点失效,insert 不失效 |

适用场景:

  • 用 vector:需要随机访问、尾插尾删多、元素小、追求效率

  • 用 list:频繁中间插入删除、不需要随机访问、元素大拷贝成本高

💡 面试金句:vector 和 list 没有绝对的好坏,要看场景。90% 的情况用 vector 就够了,因为 CPU 缓存命中高,实际运行更快。只有真的需要频繁中间插入时才考虑 list。


第二部分:list 的模拟实现

现在我们来自己实现一个 list!这是面试手写代码的高频题,一定要掌握。

我们实现的是带头双向循环链表,这是 STL 的标准实现。


(一)节点结构定义

简介作用

链表的每个节点包含:数据、前驱指针、后继指针。

代码实现
cpp 复制代码
// 节点结构体
template<class T>
struct ListNode {
    T _data;           // 数据
    ListNode<T>* _prev; // 前驱指针
    ListNode<T>* _next; // 后继指针
    
    // 节点的构造函数
    ListNode(const T& x = T())
        : _data(x)
        , _prev(nullptr)
        , _next(nullptr)
    {}
};
🐛 经典 Bug 和坑点

坑点:给默认值! 节点构造函数一定要给默认值,这样才能创建空节点(头节点不需要数据)。


(二)迭代器类的实现(面试高频考点!)

简介作用

重点中的重点! list 迭代器不是原生指针,是封装了节点指针的类。

我们需要重载这些运算符:

  • ++--:让迭代器移动到下一个 / 上一个节点

  • *:解引用,获取节点的数据

  • ->:访问节点数据的成员

  • ==!=:比较两个迭代器是否相等

还要区分普通迭代器和 const 迭代器!

代码实现
cpp 复制代码
// 迭代器类
// 模板参数技巧:用Ref和Ptr来区分普通和const迭代器
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)
    {}
    
    // *it 解引用
    Ref operator*() {
        return _node->_data;
    }
    
    // it-> 访问成员
    Ptr operator->() {
        return &_node->_data;
    }
    
    // ++it 前置++
    Self& operator++() {
        _node = _node->_next;
        return *this;
    }
    
    // it++ 后置++
    Self operator++(int) {
        Self tmp(*this);
        _node = _node->_next;
        return tmp;
    }
    
    // --it 前置--
    Self& operator--() {
        _node = _node->_prev;
        return *this;
    }
    
    // it-- 后置--
    Self operator--(int) {
        Self tmp(*this);
        _node = _node->_prev;
        return tmp;
    }
    
    // ==
    bool operator==(const Self& it) const {
        return _node == it._node;
    }
    
    // !=
    bool operator!=(const Self& it) const {
        return _node != it._node;
    }
};
🐛 经典 Bug 和坑点

坑点 1:模板参数的巧妙设计

cpp 复制代码
// 普通迭代器:Ref=T&, Ptr=T*
typedef ListIterator<T, T&, T*> iterator;

// const迭代器:Ref=const T&, Ptr=const T*
typedef ListIterator<T, const T&, const T*> const_iterator;

用模板参数来复用代码,不用写两个迭代器类!

坑点 2:-> 运算符的重载

cpp 复制代码
// 假设T是一个结构体
struct A { int a; int b; };
list<A> la;
la.push_back({1, 2});
auto it = la.begin();
cout << it->a << endl; // 等价于 (*it).a
// it-> 返回&A::_data,编译器会自动再应用一次->

💡 面试考点:list 迭代器为什么要封装成类?

  1. 节点不连续,原生指针 ++ 不行,必须重载 ++ 指向下一个节点

  2. 封装后使用方式和指针一样,符合 STL 迭代器规范

  3. 可以区分普通和 const 迭代器


(三)list 的核心结构

简介作用

list 只需要一个成员变量:头节点指针。

带头循环链表的好处:尾插尾删不用找尾,头节点的_prev 就是尾!

代码实现
cpp 复制代码
template<class T>
class list {
public:
    typedef ListNode<T> Node;
    
    // 迭代器类型定义
    typedef ListIterator<T, T&, T*> iterator;
    typedef ListIterator<T, const T&, const T*> const_iterator;
    
private:
    Node* _head; // 头节点指针
};

(四)默认成员函数

1. 无参构造
简介作用

创建空链表,初始化头节点,让头节点自己指向自己形成循环。

代码实现
cpp 复制代码
// 初始化头节点
void empty_init() {
    _head = new Node;
    _head->_next = _head;
    _head->_prev = _head;
}

// 无参构造
list() {
    empty_init();
}
🐛 经典 Bug 和坑点

坑点:头节点一定要自己成环!

cpp 复制代码
// 错误:没有成环
_head = new Node; // _next和_prev都是nullptr

// 正确:自己指向自己
_head->_next = _head;
_head->_prev = _head;

不然后面的尾插会访问空指针!


2. 拷贝构造
简介作用

深拷贝,创建一个全新的链表。

代码实现
cpp 复制代码
// 拷贝构造
list(const list<T>& lt) {
    empty_init(); // 先创建自己的头节点
    
    // 把lt的元素一个个尾插过来
    for (auto& x : lt) {
        push_back(x);
    }
}

坑点:一定要先初始化自己的头节点! 拷贝构造是创建新对象,必须先调用 empty_init () 创建自己的头节点,不然_head 是野指针。


3. 析构函数
简介作用

释放所有节点,包括头节点。

代码实现
cpp 复制代码
// 析构函数
~list() {
    clear(); // 先清掉所有数据节点
    delete _head; // 再删掉头节点
    _head = nullptr;
}

4. 赋值运算符重载
简介作用

现代写法:利用拷贝构造 + swap。

代码实现
cpp 复制代码
// 赋值运算符重载(现代写法)
list<T>& operator=(list<T> lt) { // 传值调用拷贝构造
    swap(lt); // 和临时对象交换
    return *this;
}

// 交换两个list
void swap(list<T>& lt) {
    std::swap(_head, lt._head);
}

(五)常用接口实现

1. begin、end、size、empty
简介作用
  • begin ():第一个数据节点(头节点的下一个)

  • end ():头节点本身(哨兵位)

  • 带头循环链表的 end 就是头节点!

代码实现
cpp 复制代码
iterator begin() {
    return iterator(_head->_next);
}

iterator end() {
    return iterator(_head);
}

const_iterator begin() const {
    return const_iterator(_head->_next);
}

const_iterator end() const {
    return const_iterator(_head);
}

size_t size() const {
    size_t count = 0;
    const_iterator it = begin();
    while (it != end()) {
        ++count;
        ++it;
    }
    return count;
}

bool empty() const {
    return _head->_next == _head;
}

2. push_back、pop_back、push_front、pop_front
简介作用

有了带头循环链表,尾插太简单了!头节点的_prev 就是尾。

代码实现
cpp 复制代码
// 在pos位置之前插入
iterator insert(iterator pos, const T& x) {
    Node* cur = pos._node;
    Node* prev = cur->_prev;
    Node* newnode = new Node(x);
    
    // prev newnode cur
    prev->_next = newnode;
    newnode->_prev = prev;
    newnode->_next = cur;
    cur->_prev = newnode;
    
    return iterator(newnode);
}

// 尾插
void push_back(const T& x) {
    insert(end(), x); // 在end(头节点)前插入就是尾插
}

// 头插
void push_front(const T& x) {
    insert(begin(), x); // 在begin前插入就是头插
}

// 删除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);
}

// 尾删
void pop_back() {
    erase(--end());
}

// 头删
void pop_front() {
    erase(begin());
}

3. insert、erase
简介作用

insert 和 erase 是核心,上面已经实现了。

🐛 经典 Bug 和坑点

坑点:erase 的迭代器失效

cpp 复制代码
// 错误写法
for (auto it = l.begin(); it != l.end(); ++it) {
    if (*it % 2 == 0) {
        l.erase(it); // erase后it失效了!
    }
}

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

4. clear、swap
代码实现
cpp 复制代码
// 清空所有数据节点
void clear() {
    iterator it = begin();
    while (it != end()) {
        it = erase(it);
    }
}

// 交换
void swap(list<T>& lt) {
    std::swap(_head, lt._head);
}

(六)list 迭代器失效问题(面试考点)

和 vector 对比:

|------------|--------------------|--------------------------|
| 操作 | vector 迭代器失效 | list 迭代器失效 |
| insert | ✅ 所有迭代器都可能失效(空间扩容) | ❌ 只有被插入位置的迭代器?不!都不失效 |
| erase | ✅ 当前及之后的迭代器都失效 | ❌ 只有被删除的那个迭代器失效 |

原因:

  • vector 是连续空间,insert 可能扩容,地址都变了,所有迭代器都失效

  • list 每个节点独立,insert 只是改指针,其他节点地址都没变,所以迭代器不失效

  • erase 时,vector 后面的元素会往前移,所以后面的迭代器都失效

  • list erase 只删那个节点,其他节点不动,所以只有被删的那个失效


完整测试代码

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

// 把上面所有实现放这里...

int main() {
    list<int> l;
    
    // 测试push_back
    l.push_back(1);
    l.push_back(2);
    l.push_back(3);
    
    // 测试迭代器遍历
    cout << "遍历:";
    for (auto x : l) {
        cout << x << " ";
    }
    cout << endl;
    
    // 测试insert
    auto it = l.begin();
    ++it;
    l.insert(it, 20);
    
    // 测试erase
    l.erase(l.begin());
    
    cout << "操作后:";
    for (auto x : l) {
        cout << x << " ";
    }
    cout << endl;
    
    return 0;
}

总结

今天我们学习了 list 的使用和模拟实现,重点掌握:

  1. list 和 vector 的区别:数组 vs 链表,随机访问 vs 插入删除

  2. list 迭代器的封装:不是原生指针,重载运算符

  3. 带头双向循环链表:头节点的妙用

  4. 迭代器失效:list 只有 erase 会导致当前迭代器失效

下一篇我们将学习 STL 中的另一个容器 ------stack 和 queue,敬请期待!

创作不易,如果觉得有帮助,欢迎点赞、收藏、关注!有问题可以在评论区留言~

相关推荐
三行数学1 小时前
Matlab之父克利夫·莫勒尔逝世
开发语言·matlab
陌路201 小时前
C++高级进阶--夯实进阶基础(1)
开发语言·c++
梦想三三2 小时前
【PYthon词频统计与文本向量化】苏宁易购评论分析实战
开发语言·python
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题 第93题】【Mysql篇】第23题:从查找速度来看,聚集索引和非聚集索引哪个更快?
java·开发语言·数据库·mysql·面试
Cheng小攸3 小时前
入侵检测环境部署
开发语言·php
郝学胜-神的一滴3 小时前
中级OpenGL教程 008:精准控制高光光斑大小与强度
c++·unity·godot·three.js·图形学·opengl·unreal
我是唐青枫3 小时前
Java MyBatis-Flex 实战指南:从 BaseMapper 到 QueryWrapper 的轻量 ORM 用法
java·开发语言·mybatis
牢姐与蒯3 小时前
c++数据结构之c++11(一)
数据结构·c++
ShyanZh3 小时前
Markitdown 多格式文档智能解析实战指南
开发语言·c#