深入解析C++list:从0到1实现一个完整的链表类

文章目录

  • 1.list的构造
    • 1.1定义结点结构
    • 1.2模拟迭代器行为
      • [1.2.1 第一阶段迭代器:初步构造迭代器](#1.2.1 第一阶段迭代器:初步构造迭代器)
      • [1.2.2 第二阶段迭代器:解决const迭代器的问题](#1.2.2 第二阶段迭代器:解决const迭代器的问题)
      • [1.2.3 第三阶段迭代器:添加模板参数合并两个迭代器](#1.2.3 第三阶段迭代器:添加模板参数合并两个迭代器)
    • 1.3将结点封装成list
  • 2.默认成员函数
    • [2.1 构造函数](#2.1 构造函数)
    • 2.1析构函数
    • [2.2 拷贝构造](#2.2 拷贝构造)
    • [2.3 赋值重载](#2.3 赋值重载)
  • 3.修改器
    • [3.1 insert任意位置插入函数](#3.1 insert任意位置插入函数)
    • [3.2 erase删除函数](#3.2 erase删除函数)
    • [3.3 push插入函数](#3.3 push插入函数)
    • [3.4 pop弹出函数](#3.4 pop弹出函数)
    • [3.5 clear清除函数](#3.5 clear清除函数)
  • 总结

list 是 C++ 标准模板库(STL)中一种基于带头结点双向循环链表实现的序列式容器。它最大的优势在于极高的动态修改效率,能够在任意位置以 O(1) 的时间复杂度完成元素的插入和删除,且完全不需要移动其他元素;不过,由于其节点在内存中采用离散存储,list 不支持通过下标进行随机访问,查找任意元素的时间复杂度为 O(N)。因此,list 非常适合应用于需要频繁增删节点,而对随机读取要求不高的编程场景。

1.list的构造

1.1定义结点结构

C++ 复制代码
template<class T>
struct list_node {//是对list_node的封装
    T _data;
    list_node<T>* _next;
    list_node<T>* _prev;

    list_node(const T& x = T())
        : _data(x)
        , _next(nullptr)
        , _prev(nullptr)
    {
    }
};

1.2模拟迭代器行为

在list结构中,想要实现迭代器并没有string和vector一样那么简单,上面二者的底层都是数组,首元素地址++即可依次遍历,而list就不一样了,list的底层是链表,在内存中的存储就是不连续的,遍历元素不能单靠简单的++来完成,所以对于list我们需要模拟迭代器的行为,对其进行封装来实现出迭代器的效果。

1.2.1 第一阶段迭代器:初步构造迭代器

迭代器的初步实现:

C++ 复制代码
template<class T>
struct __list_iterator {
    typedef list_node<T> Node;
    Node* _node;

    __list_iterator(Node* node) : _node(node) {}

    T& operator*() { return _node->_data; }

    // 前置++
    __list_iterator<T>& operator++() {
        _node = _node->_next;
        return *this;
    }
    // 后置++
    __list_iterator<T> operator++(int) {
        __list_iterator<T> tmp(*this);
        _node = _node->_next;
        return tmp;
    }
    // 前置--
    __list_iterator<T>& operator--() {
        _node = _node->_prev;
        return *this;
    }
    // 后置--
    __list_iterator<T> operator--(int) {
        __list_iterator<T> tmp(*this);
        _node = _node->_prev;
        return tmp;
    }

    bool operator!=(const __list_iterator<T>& it) const {
        return _node != it._node;
    }
    bool operator==(const __list_iterator<T>& it) const {
        return _node == it._node;
    }
};

大家可能会问:迭代器里有一个指针,那需不需要为它写析构函数呢?答案是不需要。因为这个节点并不属于迭代器,它真正的管理者是 list 容器,迭代器只是借用了这个指针来遍历。既然迭代器不负责资源的释放,我们就直接使用编译器默认生成的浅拷贝(拷贝构造和赋值重载)即可。这里大家要记住一个核心点:并不是所有包含指针的类都需要写析构,关键要看资源的所有权在谁手里。

1.2.2 第二阶段迭代器:解决const迭代器的问题

迭代器修改是通过operate*来修改我们需要,重新定义const迭代器。让其指向内容的数据不可修改。这里本质上就只是修改了解引用的重载。(但这个时候两种方式还不能合并)。

C++ 复制代码
template<class T>
struct __list_const_iterator {
    typedef list_node<T> Node;
    Node* _node;

    __list_const_iterator(Node* node) : _node(node) {}

    //控制operate*
    const T& operator*() { return _node->_data; }
    // 前置++
    __list_const_iterator<T>& operator++() {
        _node = _node->_next;
        return *this;
    }
    // 后置++
    __list_const_iterator<T> operator++(int) {
        __list_const_iterator<T> tmp(*this);
        _node = _node->_next;
        return tmp;
    }
    // 前置--
    __list_const_iterator<T>& operator--() {
        _node = _node->_prev;
        return *this;
    }
    // 后置--
    __list_const_iterator<T> operator--(int) {
        __list_const_iterator<T> tmp(*this);
        _node = _node->_prev;
        return tmp;
    }

    bool operator!=(const __list_const_iterator<T>& it) const {
        return _node != it._node;
    }
    bool operator==(const __list_const_iterator<T>& it) const {
        return _node == it._node;
    }
};

这里不能和vector一样重载,

不能实现,因为const迭代器本身导致迭代器不能++

C++ 复制代码
//不能实现,因为const迭代器本身,导致迭代器不能++
//typedef const __list_iterator<T> iterator;

也不能重载:

下述重载会导致调用的迭代器本身无法修改,因为不得不在前面加上const,该对象不能调用++。

C++ 复制代码
 const T& operator*() const
 { return _node->_data; }

1.2.3 第三阶段迭代器:添加模板参数合并两个迭代器

但是有点太啰嗦了,那这里我们就可以进行修改,将两个重复的定义的迭代器合并到一起。

C++ 复制代码
 template<class T,class Ref,class Ptr>//这里还要一个参数
 struct __list_iterator {
     typedef list_node<T> Node;
     //为了避免将下面的所有模板参数都进行修改,我们直接做定义
     typedef __list_iterator<T, Ref,Ptr> Self;
     Node* _node;
     __list_iterator(Node* node) : _node(node) {}

     //控制operate*
     Ref operator*() { return _node->_data; }//本质上是控制返回参数不一样
     //增加一个模板参数
     // 前置++
     //结合下面的需求,这里对->功能进行了更新
     Ptr operator->() {
         return &_node->_data;//这里就是把data的地址取出来
         //就是it->->_col实际要这样写
     }
     Self& operator++() {
         _node = _node->_next;
         return *this;
     }
     // 后置++
     Self operator++(int) {
         Self tmp(*this);
         _node = _node->_next;
         return tmp;
     }
     // 前置--
     Self& operator--() {
         _node = _node->_prev;
         return *this;
     }
     // 后置--
     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;
     }
 };

1.3将结点封装成list

C++ 复制代码
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;
    //同一个类模板实例化的两个模板参数
    //不能实现,因为const迭代器本身,导致迭代器不能++
    //typedef const __list_iterator<T> iterator;
    //typedef __list_const_iterator<T> const_iterator;//二者是两个不同的对象
    //普通迭代器
    iterator begin() {
        return iterator(_head->_next);
    }
    iterator end() {
        return iterator(_head);
    }
    //const迭代器
    const_iterator begin() const {
        return const_iterator(_head->_next);
    }

    const_iterator end() const {
        return const_iterator(_head);//返回const迭代器
    }
   void empty_init() {
        _head = new Node;
        _head->_next = _head;
        _head->_prev = _head;
    }
    list() {
        empty_init();
    }
    private:
        Node* _head;
		size_t _size = 0;//否则遍历计数也行
    };
}

测试用例:

C++ 复制代码
namespace ZL {
	void text_list01() {
		ZL::list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);

		list<int>::iterator it = lt.begin();
		while (it != lt.end()) {
			cout << *it << " ";
			++it;
		}
		cout << endl;
	}
}

2.默认成员函数

2.1 构造函数

普通构造上面已经说过了,接下来我们说:

多参数构造:

C++ 复制代码
list(std::initializer_list<T> il) {
    empty_init();
    for (const auto& e : il) {
        push_back(e);
    }
}

2.1析构函数

C++ 复制代码
//析构
~list() {
    clear();
    delete _head;
    _head = nullptr;
}

2.2 拷贝构造

C++ 复制代码
//拷贝构造
list(const list<T>& lt) {
    empty_init();
    for (const auto& e : lt) {
        push_back(e);
    }
}

2.3 赋值重载

赋值重载的传统写法:

C++ 复制代码
list<T>& operator=(const list<T>& lt) {
  if (this != &lt) {
    clear();
    for (const auto& e : lt) {
        push_back(e);
    }
}
    return *this;
}

赋值重载的现代写法:

C++ 复制代码
 void swap(list<T>& lt) {
     std::swap(_head, lt._head);
     std::swap(_size, lt._size);
 }
 //lt1(lt2);
list<T>& operator=(list<T> lt) {
    swap(lt);//将lt2临时对象的值交换给lt1,让lt1具有和lt2一样的成员函数与变量
    //出了作用域后临时对象销毁对lt2没有影响
    return *this;
}

3.修改器

3.1 insert任意位置插入函数

这里就是和双向链表的插入删除相似了。

C++ 复制代码
iterator insert(iterator pos,const T& val) {//用迭代器屏蔽底层的细节
    Node* cur = pos._node;//体现出iterator封装的便利性
    Node* newNode = new Node(val);
    Node* prev = cur->_prev;
    prev->_next = newNode;
    newNode->_prev = prev;
    newNode->_next = cur;
    cur->_prev = newNode;
    _size++;
    return (newNode);
}

3.2 erase删除函数

C++ 复制代码
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 next;构造隐式类型转化 
    _size--;
    return (next);//构造匿名对象
}

3.3 push插入函数

正常应该这么写:

C++ 复制代码
 void push_back(const T& x) {
     Node* newnode = new Node(x);
     Node* tail = _head->_prev;
     tail->_next = newnode;
     newnode->_prev = tail;
     newnode->_next = _head;
     _head->_prev = newnode;
 }

但是实际上我们可以对insert进行复用:

C++ 复制代码
void push_back(const T& x) {
    insert(end(), x);
}
void push_front(const T& x) {
    insert(begin(), x);
}

3.4 pop弹出函数

C++ 复制代码
void pop_back( ) {
	assert(!empty());
    erase(--end());
}
void pop_front( ) {
    assert(!empty());
    erase(begin());
}

3.5 clear清除函数

C++ 复制代码
void clear() {
    iterator it = begin();//用迭代器拷贝it,浅拷贝
    while (it != end()) {
        it = erase(it);
    }
    _size=0;
}

总结

分类 核心内容 关键点/代码
底层结构 带头结点的双向循环链表 插入删除 O(1),不支持随机访问
节点定义 list_node<T> _data, _next, _prev,构造函数支持默认值
迭代器设计 三个阶段演进 普通迭代器 → const迭代器 → 模板参数合并
迭代器核心操作 operator*, operator->, ++, --, !=, == 通过 RefPtr 模板参数区分 const 版本
list 类成员 _head (头结点), _size (元素个数) 哨兵位头结点,双向循环
构造/析构 list(), ~list(), empty_init() 头结点自循环,clear() + 释放头结点
拷贝构造 list(const list& lt) 复用 empty_init() + push_back
赋值重载 传统写法 + 现代写法 (copy-and-swap) 现代写法:传值参数 + swap
插入操作 insert, push_back, push_front insert 返回新节点迭代器,其他复用 insert
删除操作 erase, pop_back, pop_front erase 返回下一节点迭代器,防止迭代器失效
清空操作 clear() 遍历 erase,重置 _size = 0
多参数构造 initializer_list<T> 支持 list<int> lt = {1,2,3}
迭代器接口 begin(), end(),及 const 版本 普通返回 iterator,const 返回 const_iterator

欢迎大家批评指正!!!

相关推荐
王老师青少年编程5 小时前
csp信奥赛C++高频考点专项训练之字符串 --【字符串综合】:[NOIP 2015 提高组] 子串
c++·字符串·csp·高频考点·子串·信奥赛
June`5 小时前
redis项目之命令解析器
数据库·c++·redis
rGzywSmDg5 小时前
如何在Dev-C++中设置TDM-GCC为默认编译器
开发语言·c++
csdn_aspnet6 小时前
C++ Lomuto分区算法(Lomuto Partition Algorithm)
开发语言·c++·算法
会周易的程序员7 小时前
aiDgeScanner:工业设备扫描与管理的一体化利器——深度解析上位机与扫描端的无缝协作
c++·物联网·typescript·electron·vue·iot·aiot
BirdenT7 小时前
20260518紫题训练
c++·算法
lingzhilab9 小时前
零知派ESP32——BLE Mesh蓝牙组网智能灯控系统(PIR感应+W2812三档调色)
c++·mfc
计算机安禾9 小时前
【c++面向对象编程】第29篇:定位new(placement new):在指定内存上构造对象
开发语言·c++·算法