【c++丨STL】list模拟实现(附源码)

简介: 本文介绍了如何模拟实现C++中的`list`容器。`list`底层采用双向带头循环链表结构,相较于`vector`和`string`更为复杂。文章首先回顾了`list`的基本结构和常用接口,然后详细讲解了节点、迭代器及容器的实现过程。最终,通过这些步骤,我们成功模拟实现了`list`容器的功能。文章最后提供了完整的代码实现,并简要总结了实现过程中的关键点。如果你对双向链表或`list`的底层实现感兴趣,建议先掌握相关基础知识后再阅读本文,以便更好地理解内容。

前言

       通过之前对list的学习,我们已经基本掌握了其底层结构以及常用接口。今天我们在此基础上,尝试模拟实现list。


       与vector、string不同,由于list的底层是一个双向带头循环链表,所以它的实现上要更加复杂。vector和string的迭代器可以是原生指针,但是list的迭代器需要用一个类模板来实现,并且数据之间都是以节点的指针相连接,我们还需要定义其节点结构。当然,与vector一样,在接下来模拟实现list的过程中,我们也将采用类模板进行定义,并不会将声明和定义分离



建议大家在掌握了双向带头循环链表以及vector、string的实现之后,再来阅读本文,否则其中部分内容可能较难理解。


一、节点、迭代器以及函数声明

       首先是链表节点、迭代器和我们要实现接口的声明:

#include <iostream>
#include <cassert>
using namespace std;
 
//节点
template<class T>
struct list_node
{
    T _data;//数据域
    list_node<T>* _next;//指向后一个节点
    list_node<T>* _prev;//指向前一个节点
 
    //节点的默认构造
    list_node(const T& x = T());
};
 
//迭代器
template<class T, class Ref, class Ptr>
struct list_iterator
{
    typedef list_node<T> Node;
    typedef list_iterator<T, Ref, Ptr> Self;
 
    Node* ptr;//封装节点的指针
 
    //迭代器构造
    list_iterator(Node* node);
 
    //解引用
    Ref operator*();
    //结构成员间接访问
    Ref operator->();
    //前置++
    &Self operator++();
    //前置--
    &Self operator--();
    //后置++
    Self operator++(int);
    //后置--
    Self operator--(int);
    //不等于
    bool operator!=(const Self& it);
};
 
//容器
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;
 
    //迭代器接口
    iterator begin();
    iterator end();
    const_iterator begin() const;
    const_iterator end() const;
 
    //空链表的初始化
    void empty_init();
 
    //无参构造
    List();
    //n个val值构造
    List(size_t n, const T& val = T());
    //拷贝构造
    List(const List<T>& l);
    //赋值重载
    List<T>& operator=(List<T> l);
    //析构
    ~List();
 
    //容量接口
    size_t size() const;
 
    //插入和删除
    iterator insert(iterator pos, const T& x);
    iterator erase(iterator pos);
    void push_back(const T& x);
    void push_front(const T& x);
    void pop_back();
    void pop_front();
 
    //清空链表
    void clear();
 
    //交换两个链表
    void swap(List<T> l);
private:
    Node* _head;//头节点的指针
    size_t _size;//节点个数
};
 
//交换两个链表--非成员函数版
template <class T>
void swap(List<T>& l1, List<T>& l2);

接下来,我们开始逐一实现这三个部分。


二、list功能实现

1. 节点

       和传统的双向带头循环链表相同,节点当中有一个数据域用于存放数据;两个指针分别指向前驱节点和后继节点。 我们需要实现节点的默认构造函数,便于容器构造节点。


代码实现:

//节点
template<class T>
struct list_node
{
    T _data;//数据域
    list_node<T>* _next;//指向后一个节点
    list_node<T>* _prev;//指向前一个节点
 
    //节点的默认构造
    list_node(const T& x = T())
        :_data(x)
        , _next(nullptr)
        , _prev(nullptr)
    {}
};

可以看到,这里的节点我们也定义成了一个类模板,参数T表示的是传入的数据类型。对于构造函数,如果我们没有显式传参,则数据x就会调用其默认构造函数初始化。


2. 迭代器

       迭代器也是一个类模板,我们模拟实现的迭代器具有三个模板参数:数据类型T,T的引用类型Ref(普通引用或const引用)、T的指针类型Ptr(普通指针或const指针)。 为什么要这样设计呢?其实这样做的目的是为了提高代码复用率。如果我们创建了一个list普通对象和一个const对象,那么使用它们的迭代器时,相对应就会有普通迭代器和const迭代器。这样我们在实现迭代器时,就要将同样的接口写两遍。而当我们定义了三个模板参数后,容器内部就可以通过传参来定义对应的普通迭代器和const迭代器,减少了代码冗余。

//list容器内部定义迭代器
typedef list_iterator<T, T&, T*> iterator;//普通迭代器
typedef list_iterator<T, const T&, const T*> const_iterator;//const迭代器

其次,为了简化代码,我们typedef一下节点类型以及迭代器本身类型:

typedef list_node<T> Node;//节点
typedef list_iterator<T, Ref, Ptr> Self;//迭代器本身

除此之外,由于迭代器是将指针进行封装,所以我们要将其成员变量设置为一个指向链表节点的指针:

Node* _ptr;//封装节点的指针

接下来,我们开始实现迭代器的操作函数。


迭代器的默认构造

//迭代器构造
list_iterator(Node* node)
    :_ptr(node)
{}

对于构造函数,我们将传入的指针赋值给成员指针即可。


operator*

//解引用
Ref operator*()
{
    return _ptr->_data;
}

解引用用于访问指针所指对象的数据,所以函数返回迭代器指向节点的数据域。


operator->

//结构成员间接访问
Ref operator->()
{
    return &_ptr->_data;
}

       正常来讲,该函数的作用应该是用于访问指针所指向数据的成员,但是它并没有参数,而且返回值却是数据地址。这是为什么呢?这其实和c++的语法规定有关。当我们使用该运算符重载时,要访问到数据的成员,原本应该这样写:迭代器 ->-> 成员。但是连续两个“->”看起来并不美观,并且难以理解,所以c++语法在这里省略了一个“->”,仅支持 迭代器->成员或迭代器.operator->()->成员 的写法。所以我们在实现该重载时,要返回数据的地址,编译器就会默认根据这个地址找到指定成员(成员名写在->后即可,不传参)。



前置++和--

        由于链表底层结构并非连续,所以不能直接使指针++/--来改变指向,而应该使用类似链表遍历的方式。实现如下:

//前置++
Self& operator++()
{
    _ptr = _ptr->next;
    return *this;
}
//前置--
Self& operator--()
{
    _ptr = _ptr->prev;
    return *this;
}

后置++和--

       后置++/--的实现方法与前置++/--类似。这里创建一个临时迭代器来保存未移动时的指向。

//后置++
Self operator++(int)
{
    Self tmp(*this);
    _ptr = _ptr->next;
    return tmp;
}
//后置--
Self operator--(int)
{
    Self tmp(*this);
    _ptr = _ptr->prev;
    return tmp;
}

operator!=

//不等于
bool operator!=(const Self& it)
{
    return _ptr != it._ptr;
}

这里判断它们的成员指针是否相等即可。


3. 容器

       为了简化代码,还是先typedef一下节点类型:

typedef list_node<T> Node;

       接下来,我们开始实现list容器的常用接口。


交换两个容器的内容

       还是老套路,我们仅需交换它们的成员变量,就可以完成容器的交换。代码实现:

//交换两个链表
void swap(List<T> l)
{
    std::swap(_head, l.head);//交换头节点指针
    std::swap(_size, l._size);//交换size
}
//交换两个链表--非成员函数版
template <class T>
void swap(List<T>& l1, List<T>& l2)
{
    l1.swap(l2);//直接调用成员函数版
}

迭代器接口

        首先是普通迭代器和const迭代器的定义:

typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;

迭代器接口实现:

//迭代器接口
iterator begin()
{
    return iterator(_head->next);
}
iterator end()
{
    return _head;
}
const_iterator begin() const
{
    return iterator(_head->next);
}
const_iterator end() const
{
    return _head;
}

这里要注意一下它们的返回值:begin的返回值是指向首元素的迭代器,由于我们的链表头部有一个哨兵节点,所以首元素是哨兵节点的下一个节点;end的返回值是指向尾元素的“下一个元素”的迭代器,比较抽象,这里刚好可以定义为哨兵节点。



空链表初始化

       该函数用于创建哨兵节点并且初始化链表的各项属性,便于后续构造函数调用。

//空链表的初始化
void empty_init()
{
    _head = new Node();
    _head->next = _head->prev = _head;
    _size = 0;
}

构造函数

       这里我们实现一下无参构造和n个val值构造:

//无参构造
List()
{
    empty_init();
}
//n个val值构造
List(size_t n, const T& val = T())
{
    empty_init();
    for (size_t i = 0; i < n; i++)
    {
        push_back(val);//循环尾插
    }
}

拷贝构造函数

       拷贝构造函数的逻辑与n个val值构造相同,遍历源链表内容,循环尾插给当前链表即可。

//拷贝构造
List(const List<T>& l)
{
    empty_init();
    for (auto& e : l)
    {
        push_back(e);
    }
}

赋值重载

       这里我们仍然使用新式写法,构造新对象然后交换,完成赋值操作。 代码如下:

//赋值重载
List<T>& operator=(List<T> l)
{
    swap(l);
    return *this;//支持连续赋值
}

析构函数

       对于析构函数,我们首先释放掉所有的节点空间,然后删除哨兵节点。

//析构
~List()
{
    clear();//调用clear函数清空链表
    delete _head;
    _head = nullptr;
}

容量接口size

       容量接口size用于获取链表数据个数。我们在函数当中返回成员变量_size的值。

//容量接口
size_t size() const
{
    return _size;
}

插入和删除

       和之前实现vector时的逻辑相同,我们首先实现insert和erase函数,这两个函数分别用于在任意位置进行插入和删除操作。然后,其他的插入和删除函数可以直接调用它们,能确保代码的高效性和复用性。


insert和erase

       insert的逻辑和我们c语言实现双向链表时大体相同(在pos位置之前插入),注意插入元素后,pos指向的不再是原本需要插入的位置,迭代器失效,所以函数要返回新插入的节点的迭代器。

//插入
iterator insert(iterator pos, const T& x)
{
    Node* cur = pos._ptr;
    Node* newnode = new Node(x);
    newnode->_next = cur;
    newnode->_prev = cur->_prev;
    cur->_prev->_next = newnode;
    cur->_prev = newnode;
    _size++;
    return iterator(newnode);
}

       erase负责删除pos指向的节点。注意删除节点之后,该节点的迭代器会失效,所以函数返回指向被删除节点的后一个节点的迭代器。

iterator erase(iterator pos)
{
    assert(pos != end());//避免删除哨兵节点
    Node* del = pos._ptr;
    Node* next = del->_next;
    Node* prev = del->_prev;
    next->_prev = del->_prev;
    prev->_next = del->_next;
    delete del;
    _size--;
    return iterator(next);
}

push_back、push_front、pop_back、pop_front

尾插、头插、尾删、头删直接调用insert和erase函数即可。注意尾删节点时,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());
}


清空链表

       清空链表的操作类似于我们c语言实现的销毁链表,这里循环调用erase函数来完成数据的清除。注意不要删除哨兵节点。

//清空链表
void clear()
{
    auto it = begin();//从首元素开始,一个个删除
    while (it != end())
    {
        it = erase(it);
    }
}

三、程序全部代码

       关于list模拟实现的全部代码如下:

#include <iostream>
#include <cassert>
using namespace std;
 
//节点
template<class T>
struct list_node
{
    T _data;//数据域
    list_node<T>* _next;//指向后一个节点
    list_node<T>* _prev;//指向前一个节点
 
    //节点的默认构造
    list_node(const T& x = T())
        :_data(x)
        , _next(nullptr)
        , _prev(nullptr)
    {}
};
 
//迭代器
template<class T, class Ref, class Ptr>
struct list_iterator
{
    typedef list_node<T> Node;
    typedef list_iterator<T, Ref, Ptr> Self;
 
    Node* _ptr;//封装节点的指针
 
    //迭代器构造
    list_iterator(Node* node)
        :_ptr(node)
    {}
 
    //解引用
    Ref operator*()
    {
        return _ptr->_data;
    }
    //结构成员间接访问
    Ref operator->()
    {
        return &_ptr->_data;
    }
    //前置++
    Self& operator++()
    {
        _ptr = _ptr->next;
        return *this;
    }
    //前置--
    Self& operator--()
    {
        _ptr = _ptr->prev;
        return *this;
    }
    //后置++
    Self operator++(int)
    {
        Self tmp(*this);
        _ptr = _ptr->next;
        return tmp;
    }
    //后置--
    Self operator--(int)
    {
        Self tmp(*this);
        _ptr = _ptr->prev;
        return tmp;
    }
    //不等于
    bool operator!=(const Self& it)
    {
        return _ptr != it._ptr;
    }
};
 
//容器
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;
 
    //迭代器接口
    iterator begin()
    {
        return iterator(_head->next);
    }
    iterator end()
    {
        return _head;
    }
    const_iterator begin() const
    {
        return iterator(_head->next);
    }
    const_iterator end() const
    {
        return _head;
    }
 
    //空链表的初始化
    void empty_init()
    {
        _head = new Node();
        _head->next = _head->prev = _head;
        _size = 0;
    }
 
    //无参构造
    List()
    {
        empty_init();
    }
    //n个val值构造
    List(size_t n, const T& val = T())
    {
        empty_init();
        for (size_t i = 0; i < n; i++)
        {
            push_back(val);//循环尾插
        }
    }
    //拷贝构造
    List(const List<T>& l)
    {
        empty_init();
        for (auto& e : l)
        {
            push_back(e);
        }
    }
    //赋值重载
    List<T>& operator=(List<T> l)
    {
        swap(l);
        return *this;//支持连续赋值
    }
    //析构
    ~List()
    {
        clear();//调用clear函数清空链表
        delete _head;
        _head = nullptr;
    }
 
    //容量接口
    size_t size() const
    {
        return _size;
    }
 
    //插入和删除
    iterator insert(iterator pos, const T& x)
    {
        Node* cur = pos._ptr;
        Node* newnode = new Node(x);
        newnode->_next = cur;
        newnode->_prev = cur->_prev;
        cur->_prev->_next = newnode;
        cur->_prev = newnode;
        _size++;
        return iterator(newnode);
    }
    iterator erase(iterator pos)
    {
        assert(pos != end());//避免删除哨兵节点
        Node* del = pos._ptr;
        Node* next = del->_next;
        Node* prev = del->_prev;
        next->_prev = del->_prev;
        prev->_next = del->_next;
        delete del;
        _size--;
        return iterator(next);
    }
    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());
    }
 
    //清空链表
    void clear()
    {
        auto it = begin();//从首元素开始,一个个删除
        while (it != end())
        {
            it = erase(it);
        }
    }
 
    //交换两个链表
    void swap(List<T> l)
    {
        std::swap(_head, l.head);//交换头节点指针
        std::swap(_size, l._size);//交换size
    }
private:
    Node* _head;//头节点的指针
    size_t _size;//节点个数
};
 
//交换两个链表--非成员函数版
template <class T>
void swap(List<T>& l1, List<T>& l2)
{
    l1.swap(l2);//直接调用成员函数版
}


总结

       本篇文章,我们在掌握list使用方法及其底层原理的基础上,模拟实现出了list容器。由于底层数据内存的非连续性,它的迭代器实现与vector、string有较大差异。之后博主会和大家一起开始stack和queue的学习。如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤

目录
打赏
0
0
1
0
64
分享
相关文章
|
25天前
|
【c++丨STL】stack和queue的使用及模拟实现
本文介绍了STL中的两个重要容器适配器:栈(stack)和队列(queue)。容器适配器是在已有容器基础上添加新特性或功能的结构,如栈基于顺序表或链表限制操作实现。文章详细讲解了stack和queue的主要成员函数(empty、size、top/front/back、push/pop、swap),并提供了使用示例和模拟实现代码。通过这些内容,读者可以更好地理解这两种数据结构的工作原理及其实现方法。最后,作者鼓励读者点赞支持。 总结:本文深入浅出地讲解了STL中stack和queue的使用方法及其模拟实现,帮助读者掌握这两种容器适配器的特性和应用场景。
56 21
【c++丨STL】list的使用
本文介绍了STL容器`list`的使用方法及其主要功能。`list`是一种双向链表结构,适用于频繁的插入和删除操作。文章详细讲解了`list`的构造函数、析构函数、赋值重载、迭代器、容量接口、元素访问接口、增删查改操作以及一些特有的操作接口如`splice`、`remove_if`、`unique`、`merge`、`sort`和`reverse`。通过示例代码,读者可以更好地理解如何使用这些接口。最后,作者总结了`list`的特点和适用场景,并预告了后续关于`list`模拟实现的文章。
69 7
|
1月前
|
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
68 19
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
51 13
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
53 5
【C++面向对象——群体类和群体数据的组织】实现含排序功能的数组类(头歌实践教学平台习题)【合集】
1. **相关排序和查找算法的原理**:介绍直接插入排序、直接选择排序、冒泡排序和顺序查找的基本原理及其实现代码。 2. **C++ 类与成员函数的定义**:讲解如何定义`Array`类,包括类的声明和实现,以及成员函数的定义与调用。 3. **数组作为类的成员变量的处理**:探讨内存管理和正确访问数组元素的方法,确保在类中正确使用动态分配的数组。 4. **函数参数传递与返回值处理**:解释排序和查找函数的参数传递方式及返回值处理,确保函数功能正确实现。 通过掌握这些知识,可以顺利地将排序和查找算法封装到`Array`类中,并进行测试验证。编程要求是在右侧编辑器补充代码以实现三种排序算法
41 5
【C++面向对象——类的多态性与虚函数】计算图像面积(头歌实践教学平台习题)【合集】
本任务要求设计一个矩形类、圆形类和图形基类,计算并输出相应图形面积。相关知识点包括纯虚函数和抽象类的使用。 **目录:** - 任务描述 - 相关知识 - 纯虚函数 - 特点 - 使用场景 - 作用 - 注意事项 - 相关概念对比 - 抽象类的使用 - 定义与概念 - 使用场景 - 编程要求 - 测试说明 - 通关代码 - 测试结果 **任务概述:** 1. **图形基类(Shape)**:包含纯虚函数 `void PrintArea()`。 2. **矩形类(Rectangle)**:继承 Shape 类,重写 `Print
48 4
【C++面向对象——类的多态性与虚函数】编写教学游戏:认识动物(头歌实践教学平台习题)【合集】
本项目旨在通过C++编程实现一个教学游戏,帮助小朋友认识动物。程序设计了一个动物园场景,包含Dog、Bird和Frog三种动物。每个动物都有move和shout行为,用于展示其特征。游戏随机挑选10个动物,前5个供学习,后5个用于测试。使用虚函数和多态实现不同动物的行为,确保代码灵活扩展。此外,通过typeid获取对象类型,并利用strstr辅助判断类型。相关头文件如&lt;string&gt;、&lt;cstdlib&gt;等确保程序正常运行。最终,根据小朋友的回答计算得分,提供互动学习体验。 - **任务描述**:编写教学游戏,随机挑选10个动物进行展示与测试。 - **类设计**:基类
33 3