C++ list容器模拟实现

目录

一.什么是List

二.List的模拟实现

2.1List的基本结构

2.2构造+尾插

2.3迭代器

2.4插入+删除+头插

2.5->运算符重载

2.6const类型的迭代器

const的用法:

2.7迭代器失效问题

[2.7.1 insert插入](#2.7.1 insert插入)

2.7.2erase删除


一.什么是List

二.List的模拟实现

2.1List的基本结构

问题一:为什么要分成两个类来写

1)ListNode(节点类)

只负责 一个节点自己的事

  • 存数据 _data
  • 存前驱指针 _prev
  • 存后继指针 _next
  • 只关心:我是谁、我前后是谁

2)List(链表管理类)

负责 整个链表的事

  • 维护头结点 _head
  • 维护长度 _size
  • 提供接口:push_backinserterase、遍历......
  • 只关心:怎么管理一群节点、怎么对外提供服务

如果把节点和链表写在同一个类里:

  • 数据、prevnextheadsize、所有成员函数 全部挤在一起
  • 分不清 "节点本身" 和 "整个链表"
  • 代码混乱、难读、难改、复用性差

对比(一眼看出区别)

  • 分开写 :Node 只管节点;List 只管链表 → 清晰、各司其职
  • 写一起 :什么都混在一起 → 一坨面条代码

问题二:ListNode<T>* _next,这里为什么要加T

2.2构造+尾插

这里运行不了,运行不了的原因是

list_node 缺少无参构造函数

在 List.h 中,list_node 结构体的构造函数只有带参数的版本(list_node(const T& x)),但没有无参构造函数。

而在 list 类的构造函数中,执行了 _head = new Node; ------ 这里尝试调用 list_node 的无参构造函数,但该构造函数并不存在,因此会触发编译错误。

方法 1:利用list_node的带参构造显式传入默认值

在 list 类的构造函数中,创建头结点时,显式调用 list_node 的带参构造,并传入 T 类型的默认值(通过 T() 触发 T 的默认构造,也就是使用匿名对象,T有可能是任意类型)。这样无需为 list_node 新增无参构造函数。

cpp 复制代码
template<class T>
class list
{
public:
    list()
    {
        // 调用list_node的带参构造,传入T的默认值
        _head = new Node(T()); 
        _head->_prev = _head;
        _head->_next = _head;
    }
    // 其余成员保持不变...
};

方法2:使用委托构造函数(C++11 及以后)

让带参数的构造函数也能处理无参数的情况,通过委托给自己并提供一个默认值

cpp 复制代码
template<class T>
struct list_node
{
    list_node<T>* _prev;
    list_node<T>* _next;
    T _data;

    // 带参数的构造函数,并提供一个默认参数 T()
    list_node(const T& x = T())
        : _prev(nullptr)
        , _next(nullptr)
        , _data(x)
    {}
};

2.3迭代器

单独写一个迭代器的类是因为他不能像vector那样++,*等,List的地址是不连续的,++的话只能到它相邻的下一个,但是链表不是连续的地址,vector是连续的,可以支持++,所以我们需要创建一个单独的类来实现这种相同的++效果

这个 List_iterator 类,就是为了给你的双向链表提供「迭代器」功能,让链表也能像 vector 一样,用统一的方式遍历元素。

1. 封装底层细节,隐藏链表的实现

用户用迭代器时,只需要关心 ++it*it不需要知道底层是 _next 还是 _prev 指针

  • 迭代器类把 _node->_next 封装在 operator++() 里,用户只需要写 ++it 就行。
  • 这样就算你以后改了链表的底层实现(比如改成单向链表),只要迭代器接口不变,用户的代码完全不用改。

2. 实现双向迭代器的完整功能

你的 List 是双向链表,迭代器需要支持:

  • ++it 前进(你已经实现了前置 ++)
  • --it 后退(你还没写,但后续可以加)
  • *it 访问数据
  • it != it2it == it2 比较
  • it-> 访问成员(需要额外实现 operator->

如果直接用裸指针,这些功能需要用户自己手动实现,既麻烦又容易出错;而封装成迭代器类后,这些操作都被标准化了。

函数 作用
List_iterator(Node* node) 构造函数:把迭代器绑定到一个链表节点上
iterator& operator++() 前置 ++:让迭代器移动到下一个节点(通过 _node->_next
* T& operator*() 解引用:返回节点存储的数据 _data
bool operator!=(...) 比较两个迭代器是否指向不同节点
bool operator==(...) 比较两个迭代器是否指向同一个节点

this 指针和 *this 是什么?

在 C++ 的非静态成员函数中,编译器会自动给函数传递一个隐含的指针参数 ------this,它的作用是:指向调用这个成员函数的「对象本身」。

举个具体例子:假设你定义了迭代器对象 it,然后调用 ++it,此时 it 就是调用 operator++() 函数的对象,函数里的 this 指针就指向 it 这个对象(this 的类型是 list_iterator< T >*)。

而 *this 就是对 this 指针做「解引用」,得到的是:调用该函数的那个迭代器对象本身(类型是 list_iterator< T >)。

2.4插入+删除+头插

2.5->运算符重载

如果链表中存放的是结构体,要想获取里面的数据就要这样写,先解引用获取,然后再获取里面的数据,能不能直接用操作符直接获取,这是我们就需要重载运算符->

2.6const类型的迭代器

为什么这里会报错呢?

这是因为在Print_Container的形参是const类型修饰的,但这里传入的是非const类型的,所以还需是现const的迭代器

这里的typename是什么意思

const的用法:

我们发现const迭代器和非const迭代器的相似度很高,写两个显得冗余,是否能创建一个模板来实现两个迭代器通用?

2.7迭代器失效问题

2.7.1 insert插入

我们可以发现插入的时候迭代器未失效

2.7.2erase删除

为什么这里的迭代器失效了

这时就需要更新迭代器的指向了