C++基础:(十三)list类的模拟实现

目录

前言

[一、 节点结构定义](#一、 节点结构定义)

[二、 list 类的核心成员与接口实现](#二、 list 类的核心成员与接口实现)

[2.1 list 类的成员变量与默认构造](#2.1 list 类的成员变量与默认构造)

[三、 list 反向迭代器实现](#三、 list 反向迭代器实现)

[四、list 与 vector 深度对比:选择合适的容器](#四、list 与 vector 深度对比:选择合适的容器)

[4.1 核心特性对比](#4.1 核心特性对比)

[4.2 适用场景对比](#4.2 适用场景对比)

[vector 适用场景:](#vector 适用场景:)

[list 适用场景:](#list 适用场景:)

[4.3 场景选择对比](#4.3 场景选择对比)

[场景 1:频繁随机访问 ------ 选择 vector](#场景 1:频繁随机访问 —— 选择 vector)

[场景 2:频繁中间插入 ------ 选择 list](#场景 2:频繁中间插入 —— 选择 list)

总结


前言

在上一篇博客中,我为大家介绍了list类的核心原理和接口的使用,本期我们将继续学期list类的模拟实现。话不多说,让我们开始吧!


理解 list 的底层实现,不仅能帮助我们更灵活地使用 list,还能掌握双向循环链表的设计思想。模拟实现 list 需完成三个核心部分:节点结构定义list 类的核心成员与接口实现反向迭代器实现

一、 节点结构定义

list 的每个节点包含数据、前驱指针、后继指针,因此我们首先定义一个节点模板结构体 ListNode

cpp 复制代码
template <class T>
struct ListNode {
    T _data;               // 存储数据
    ListNode<T>* _prev;    // 指向前驱节点
    ListNode<T>* _next;    // 指向后继节点

    // 节点构造函数:初始化数据,前驱和后继指针默认指向 nullptr
    ListNode(const T& data = T())
        : _data(data)
        , _prev(nullptr)
        , _next(nullptr)
    {}
};

二、 list 类的核心成员与接口实现

list 类的核心成员是头结点指针_head),所有操作(插入、删除、遍历)均围绕头结点展开。以下是 list 类的模板定义及关键接口实现:

2.1 list 类的成员变量与默认构造

cpp 复制代码
template <class T>
class list {
    // 定义节点类型别名,简化代码
    typedef ListNode<T> Node;
public:
    // -------------------------- 正向迭代器定义 --------------------------
    // 迭代器本质是对节点指针的封装,需支持 *、->、++、--、!=、== 等操作
    class iterator {
    public:
        typedef ListNode<T> Node;
        typedef iterator self;

        // 迭代器构造函数:用节点指针初始化
        iterator(Node* node)
            : _node(node)
        {}

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

        // 箭头运算符:返回节点数据的指针(用于自定义类型成员访问)
        T* operator->() {
            return &(_node->_data);
        }

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

        // 后置 ++:先返回当前迭代器,再移动
        self operator++(int) {
            self temp(*this);
            _node = _node->_next;
            return temp;
        }

        // 前置 --:移动到前一个节点
        self& operator--() {
            _node = _node->_prev;
            return *this;
        }

        // 后置 --:先返回当前迭代器,再移动
        self operator--(int) {
            self temp(*this);
            _node = _node->_prev;
            return temp;
        }

        // 相等比较:节点指针是否相同
        bool operator==(const self& it) const {
            return _node == it._node;
        }

        // 不等比较:节点指针是否不同
        bool operator!=(const self& it) const {
            return _node != it._node;
        }

        // 节点指针(供反向迭代器访问)
        Node* _node;
    };

    // -------------------------- list 核心接口 --------------------------
    // 默认构造:创建头结点,形成闭环
    list() {
        // 初始化头结点,前驱和后继都指向自身
        _head = new Node();
        _head->_prev = _head;
        _head->_next = _head;
    }

    // 析构函数:释放所有节点(包括头结点)
    ~list() {
        clear();          // 清空有效节点
        delete _head;     // 释放头结点
        _head = nullptr;  // 避免野指针
    }

    // 拷贝构造:深拷贝(用其他 list 初始化当前 list)
    list(const list<T>& l) {
        // 1. 初始化当前 list 的头结点
        _head = new Node();
        _head->_prev = _head;
        _head->_next = _head;

        // 2. 遍历 l 的有效节点,逐个插入到当前 list 尾部
        auto it = l.begin();
        while (it != l.end()) {
            push_back(*it);
            ++it;
        }
    }

    // 赋值运算符重载:深拷贝(现代写法,利用拷贝构造和 swap)
    list<T>& operator=(list<T> l) {
        swap(_head, l._head);
        return *this;
    }

    // -------------------------- 迭代器接口 --------------------------
    iterator begin() {
        // 第一个有效节点是头结点的 next
        return iterator(_head->_next);
    }

    iterator end() {
        // 尾迭代器是头结点
        return iterator(_head);
    }

    // -------------------------- 容量接口 --------------------------
    bool empty() const {
        // 头结点的 next 指向自身,说明无有效节点
        return _head->_next == _head;
    }

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

    // -------------------------- 元素访问接口 --------------------------
    T& front() {
        // 第一个有效节点的数据
        return *begin();
    }

    const T& front() const {
        return *begin();
    }

    T& back() {
        // 最后一个有效节点是头结点的 prev
        return *(--end());
    }

    const T& back() const {
        return *(--end());
    }

    // -------------------------- 元素修改接口 --------------------------
    // 头部插入:在头结点和第一个有效节点之间插入
    void push_front(const T& val) {
        Node* newNode = new Node(val);
        Node* first = _head->_next;  // 原第一个有效节点

        // 调整指针:头结点 <-> newNode <-> first
        _head->_next = newNode;
        newNode->_prev = _head;
        newNode->_next = first;
        first->_prev = newNode;
    }

    // 头部删除:删除第一个有效节点
    void pop_front() {
        if (empty()) {
            return;  // 空链表,无需删除
        }

        Node* first = _head->_next;  // 要删除的节点
        Node* second = first->_next; // 原第二个有效节点

        // 调整指针:头结点 <-> second
        _head->_next = second;
        second->_prev = _head;

        delete first;  // 释放删除的节点
    }

    // 尾部插入:在头结点和最后一个有效节点之间插入
    void push_back(const T& val) {
        Node* newNode = new Node(val);
        Node* last = _head->_prev;  // 原最后一个有效节点

        // 调整指针:last <-> newNode <-> 头结点
        last->_next = newNode;
        newNode->_prev = last;
        newNode->_next = _head;
        _head->_prev = newNode;
    }

    // 尾部删除:删除最后一个有效节点
    void pop_back() {
        if (empty()) {
            return;  // 空链表,无需删除
        }

        Node* last = _head->_prev;   // 要删除的节点
        Node* prevLast = last->_prev;// 原倒数第二个有效节点

        // 调整指针:prevLast <-> 头结点
        prevLast->_next = _head;
        _head->_prev = prevLast;

        delete last;  // 释放删除的节点
    }

    // 任意位置插入:在 pos 指向的节点之前插入
    iterator insert(iterator pos, const T& val) {
        Node* newNode = new Node(val);
        Node* cur = pos._node;       // pos 指向的节点
        Node* prev = cur->_prev;     // pos 节点的前驱

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

        // 返回指向新插入节点的迭代器
        return iterator(newNode);
    }

    // 任意位置删除:删除 pos 指向的节点,返回下一个节点的迭代器
    iterator erase(iterator pos) {
        if (pos == end()) {
            return end();  // 不能删除尾迭代器(头结点)
        }

        Node* cur = pos._node;       // 要删除的节点
        Node* prev = cur->_prev;     // 前驱节点
        Node* next = cur->_next;     // 后继节点

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

        delete cur;  // 释放删除的节点

        // 返回指向 next 节点的迭代器
        return iterator(next);
    }

    // 清空有效节点(头结点保留)
    void clear() {
        auto it = begin();
        while (it != end()) {
            it = erase(it);  // 用 erase 的返回值重置迭代器
        }
    }

    // 交换两个 list 的头结点(实现 O(1) 交换)
    void swap(list<T>& l) {
        std::swap(_head, l._head);
    }

private:
    Node* _head;  // 头结点指针(哨兵节点)
};

三、 list 反向迭代器实现

反向迭代器的核心逻辑是 "复用正向迭代器" ------ 反向迭代器的 ++ 对应正向迭代器的 --,反向迭代器的 -- 对应正向迭代器的 ++。因此,我们可以设计一个模板类ReverseIterator,内部包含一个正向迭代器,通过包装正向迭代器的接口实现反向迭代功能。

cpp 复制代码
// 反向迭代器模板类:模板参数为正向迭代器类型
template <class Iterator>
class ReverseIterator {
public:
    typedef typename Iterator::Ref Ref;   // 迭代器指向数据的引用类型(需用 typename 声明是类型)
    typedef typename Iterator::Ptr Ptr;   // 迭代器指向数据的指针类型
    typedef ReverseIterator<Iterator> Self;

    // 构造函数:用正向迭代器初始化
    ReverseIterator(Iterator it)
        : _it(it)
    {}

    // 解引用:反向迭代器的 * 对应正向迭代器的前一个节点
    Ref operator*() {
        Iterator temp = _it;  // 拷贝当前正向迭代器
        --temp;               // 移动到前一个节点
        return *temp;         // 返回前一个节点的数据
    }

    // 箭头运算符:返回数据的指针
    Ptr operator->() {
        return &(operator*());
    }

    // 前置 ++:反向迭代器向前移动(正向迭代器向后移动)
    Self& operator++() {
        --_it;  // 正向迭代器 --,对应反向迭代器 ++
        return *this;
    }

    // 后置 ++:先返回当前迭代器,再移动
    Self operator++(int) {
        Self temp(*this);
        --_it;
        return temp;
    }

    // 前置 --:反向迭代器向后移动(正向迭代器向前移动)
    Self& operator--() {
        ++_it;  // 正向迭代器 ++,对应反向迭代器 --
        return *this;
    }

    // 后置 --:先返回当前迭代器,再移动
    Self operator--(int) {
        Self temp(*this);
        ++_it;
        return temp;
    }

    // 相等比较:正向迭代器是否相同
    bool operator==(const Self& it) const {
        return _it == it._it;
    }

    // 不等比较:正向迭代器是否不同
    bool operator!=(const Self& it) const {
        return _it != it._it;
    }

private:
    Iterator _it;  // 内部存储的正向迭代器
};

我们还可以在 list 类的public 区域添加反向迭代器的类型定义和接口:

cpp 复制代码
template <class T>
class list {
    // ... 其他成员(节点定义、正向迭代器、核心接口等)...
public:
    // 正向迭代器的 Ref 和 Ptr 定义(供反向迭代器使用)
    typedef T& Ref;
    typedef T* Ptr;

    // 反向迭代器类型定义
    typedef ReverseIterator<iterator> reverse_iterator;

    // 反向迭代器接口
    reverse_iterator rbegin() {
        // rbegin() 对应正向迭代器的 end()
        return reverse_iterator(end());
    }

    reverse_iterator rend() {
        // rend() 对应正向迭代器的 begin()
        return reverse_iterator(begin());
    }

    // ... 其他成员 ...
};

反向迭代器的使用示例如下:

cpp 复制代码
#include <iostream>
#include "MyList.h"  // 包含自定义的 list 实现
using namespace std;

int main() {
    MyList::list<int> l = {1, 2, 3, 4, 5};  // 假设自定义 list 命名空间为 MyList

    // 反向遍历
    cout << "反向遍历 l: ";
    auto rit = l.rbegin();
    while (rit != l.rend()) {
        cout << *rit << " ";  // 输出:5 4 3 2 1
        ++rit;
    }
    cout << endl;

    return 0;
}

四、list 与 vector 深度对比:选择合适的容器

listvector 是 STL 中最常用的两个序列式容器,但由于底层结构不同,它们的特性、效率和适用场景差异极大。掌握两者的对比,是在实际开发中选择正确容器的关键。

4.1 核心特性对比

下表从底层结构、访问效率、插入删除效率等 7 个核心维度对比 listvector

对比维度 vector list
底层结构 动态顺序表(一段连续的内存空间) 带头结点的双向循环链表(非连续内存,节点动态开辟)
随机访问支持 支持(通过下标 []at() 访问,时间复杂度 O (1)) 不支持(需通过迭代器遍历,时间复杂度 O (N))
插入 / 删除效率 **1.**头部 / 中间插入 / 删除:需搬移后续元素,时间复杂度 O (N); 2. 尾部插入 / 删除(无扩容):时间复杂度 O (1); 3. 尾部插入(需扩容):需开辟新空间、拷贝元素、释放旧空间,效率低 **1.**任意位置插入 / 删除(找到位置后):仅修改指针,时间复杂度 O (1); **2.**查找位置需遍历,时间复杂度 O (N)(但插入 / 删除本身效率极高)
空间利用率 1. 连续内存,无节点开销,空间利用率高; **2.**扩容会预留额外空间(如 1.5 倍或 2 倍),可能造成内存浪费 1. 每个节点包含数据和两个指针,存在节点开销(小数据类型时开销占比高); **2.**节点动态开辟,易产生内存碎片,空间利用率低
缓存利用率 高。CPU 缓存基于 "局部性原理",连续内存中的元素会被批量加载到缓存,访问相邻元素时无需重新加载 低。节点内存不连续,访问下一个节点时大概率未被加载到缓存,需频繁从内存读取,效率低
迭代器类型 原生态指针(指向连续内存中的元素) 对节点指针的封装(需支持 ++-- 操作,指向相邻节点)
迭代器失效 **1.**插入元素(尾部插入且无扩容除外):所有迭代器失效(内存重新分配,原地址无效); 2. 删除元素:当前迭代器及后续迭代器失效(元素前移,原地址指向的元素改变) **1.**插入元素:所有迭代器均有效(仅修改指针,节点地址不变); 2. 删除元素:仅指向被删除节点的迭代器失效,其他迭代器有效

4.2 适用场景对比

根据上述特性,listvector 的适用场景有明确区分:

vector 适用场景:
  1. 需要频繁随机访问元素的场景:如数组排序、二分查找(需通过下标快速定位元素)。
  2. 元素插入 / 删除主要在尾部的场景:如日志记录(仅需在尾部追加日志)、栈(后进先出,仅操作尾部)。
  3. 对空间利用率和缓存效率要求高 的场景:如存储大量小数据类型(如 intfloat),连续内存可减少开销。
list 适用场景:
  1. 需要频繁在头部或中间插入 / 删除元素的场景:如链表式队列(头部删除、尾部插入)、双向队列(头尾均需操作)、频繁修改的列表(如购物车添加 / 删除商品)。
  2. 无需随机访问元素的场景:仅需遍历元素,或通过迭代器定位到特定位置后进行修改。
  3. 元素数量不确定,且需频繁动态调整 的场景:list 无需扩容,插入 / 删除时仅需分配 / 释放单个节点,避免 vector 扩容带来的性能开销。

4.3 场景选择对比

场景 1:频繁随机访问 ------ 选择 vector
cpp 复制代码
#include <vector>
#include <list>
#include <iostream>
#include <ctime>
using namespace std;

// 测试随机访问效率:通过下标访问第 10000 个元素
void TestRandomAccess() {
    const int N = 100000;
    vector<int> v(N, 0);
    list<int> l(N, 0);

    // 测试 vector 随机访问
    clock_t start = clock();
    for (int i = 0; i < 10000; ++i) {
        v[99999] = i;  // 直接下标访问,O(1)
    }
    clock_t end = clock();
    cout << "vector 随机访问时间:" << (double)(end - start) / CLOCKS_PER_SEC << "s" << endl;

    // 测试 list 随机访问(需遍历,O(N))
    start = clock();
    for (int i = 0; i < 10000; ++i) {
        auto it = l.begin();
        advance(it, 99999);  // 移动迭代器到第 100000 个元素,需遍历 99999 次
        *it = i;
    }
    end = clock();
    cout << "list 随机访问时间:" << (double)(end - start) / CLOCKS_PER_SEC << "s" << endl;
}

int main() {
    TestRandomAccess();
    // 输出示例:
    // vector 随机访问时间:0.0001s
    // list 随机访问时间:0.8s(时间差异巨大)
    return 0;
}
场景 2:频繁中间插入 ------ 选择 list
cpp 复制代码
#include <vector>
#include <list>
#include <iostream>
#include <ctime>
using namespace std;

// 测试中间插入效率:在容器中间插入 10000 个元素
void TestInsertInMiddle() {
    const int N = 10000;
    vector<int> v(1000, 0);  // 初始有 1000 个元素
    list<int> l(1000, 0);

    // 测试 vector 中间插入(需搬移元素,O(N))
    clock_t start = clock();
    auto vit = v.begin() + 500;  // 中间位置
    for (int i = 0; i < N; ++i) {
        vit = v.insert(vit, i);  // 插入后迭代器失效,需重新赋值
    }
    clock_t end = clock();
    cout << "vector 中间插入时间:" << (double)(end - start) / CLOCKS_PER_SEC << "s" << endl;

    // 测试 list 中间插入(仅修改指针,O(1))
    start = clock();
    auto lit = l.begin();
    advance(lit, 500);  // 定位到中间位置(仅遍历一次)
    for (int i = 0; i < N; ++i) {
        lit = l.insert(lit, i);  // 插入后迭代器有效,仅需重置
    }
    end = clock();
    cout << "list 中间插入时间:" << (double)(end - start) / CLOCKS_PER_SEC << "s" << endl;
}

int main() {
    TestInsertInMiddle();
    // 输出示例:
    // vector 中间插入时间:0.5s
    // list 中间插入时间:0.001s(时间差异巨大)
    return 0;
}

总结

本文从 list 的基础介绍出发,完整实现了 list 的模拟(包括节点结构、正向迭代器、反向迭代器),最后通过与 vector 的多维度对比,明确了两者的适用场景。掌握 list 的特性与使用技巧,不仅能在合适的场景中提升程序性能,还能加深对链表这种基础数据结构的理解,为后续学习更复杂的容器(如 dequeset等)打下坚实基础。

相关推荐
froginwe114 小时前
R Excel 文件:高效数据处理与可视化分析利器
开发语言
练习时长一年4 小时前
@Scope失效问题
java·开发语言
淘晶驰AK4 小时前
主流的 MCU 开发语言为什么是 C 而不是 C++?
c语言·开发语言·单片机
胖咕噜的稞达鸭4 小时前
算法入门:专题二---滑动窗口(长度最小的子数组)更新中
c语言·数据结构·c++·算法·推荐算法
敲上瘾6 小时前
Linux系统C++开发环境搭建工具(二)—— etcd 使用指南
linux·c++·etcd
流星白龙6 小时前
【Qt】1.安装QT
开发语言·qt
励志不掉头发的内向程序员6 小时前
【Linux系列】解码 Linux 内存地图:从虚拟到物理的寻宝之旅
linux·运维·服务器·开发语言·学习
深盾科技7 小时前
C/C++逆向分析实战:变量的奥秘与安全防护
c语言·c++·安全
superxxd8 小时前
跨平台音频IO处理库libsoundio实践
开发语言·qt·音视频