当前位置: 首页 > news >正文

《C++ 手搓list容器底层》:从结构原理深度解析到功能实现(附源码版)

🔥个人主页:Cx330🌸

❄️个人专栏:《C语言》《LeetCode刷题集》《数据结构-初阶》《C++知识分享》

《优选算法指南-必刷经典100题》《Linux操作系统》:从入门到入魔

🌟心向往之行必能至


🎥Cx330🌸的简介:


目录

前言:

一. 底层原理:List 容器的 “骨架”—— 带头双向循环链表

1.1 结构组成与优势

二. 模块实现:bit命名空间下的 List 核心代码

2.1 模块 1:链表节点(list_node)—— 容器的 “基本单元”

2.1.1 代码实现

2.1.2 核心解析

2.2 模块 2:迭代器(list_iterator)—— 容器的 “导航工具”

2.2.1 代码实现

2.2.2 核心解析

2.3 模块3:容器类(list)——List 功能的 “中枢”

2.3.1 代码实现

2.3.2 核心解析

三. 功能测试:用test.cpp例子来验证 List 正确性

3.1 测试用例1:基础构造与遍历(push_back+迭代器/范围for)

3.1.1 尾插代码实现

3.1.2 测试结果

3.2 测试用例 2:头插、头删、尾删

3.2.1 代码实现

3.2.2 测试结果

3.3 测试用例 3:const 迭代器与拷贝构造

3.3.1 代码实现

3.3.2 测试结果

3.4 测试用例 4:insert、erase 与 clear

3.4.1 代码实现

3.4.2 测试结果

补充

核心特性(对比 vector)

总结:


前言:

在之前《数据结构初阶》的学习中,与大家分享了链表的相关内容,如果大家跟着学习了的话,相信大家可以很顺利的衔接上这篇博客的内容,如果没有学习过的话,也不影响我给大家深度解析

一. 底层原理:List 容器的 “骨架”—— 带头双向循环链表

要手写 List,先明确其底层结构 ——带头双向循环链表,这是所有接口高效实现的基础

1.1 结构组成与优势

结构部分功能说明
哨兵头节点不存储有效数据,仅作为操作锚点,统一空/非空链表的插入、删除逻辑,无需额外判断边界。例如:尾插时无需检查“是否为第一个节点”,直接通过头节点的前驱指针定位尾节点,简化代码逻辑
数据节点每个节点含_prev(前驱指针)、_next(后继指针)、_data(数据域),支持双向遍历。既可以从当前节点向前追溯前驱节点,也能向后访问后继节点,为迭代器的++/--操作提供底层支持
循环特性尾节点_next指向头节点,头节点_prev指向尾节点,形成闭环。例如:遍历到尾节点后,通过_next可直接回到头节点;获取尾节点无需遍历整个链表,只需访问_head->_prev,提升尾操作效率

通过这种结构设计,List容器实现了任意位置插入/删除O(1)效率遍历逻辑统一边界处理简化三大核心优势,是区别于vector动态数组的关键设计

二. 模块实现:bit命名空间下的 List 核心代码

2.1 模块 1:链表节点(list_node)—— 容器的 “基本单元”

节点是存储数据的载体,用模板类实现泛型支持,适配任意数据类型(如intstring

2.1.1 代码实现
#pragma once
#include<iostream>
#include<list>
using namespace std;namespace bit
{// 链表节点结构:存储数据与双向指针template<class T>struct list_node{list_node<T>* _prev;  // 前驱节点指针list_node<T>* _next;  // 后继节点指针T _data;              // 节点数据// 构造函数:默认值初始化,指针置空list_node(const T& x = T()): _prev(nullptr), _next(nullptr), _data(x){}};
}
2.1.2 核心解析
  • 模板参数 T:支持任意数据类型,例如bit::list<int>存储整数,bit::list<string>存储字符串,与 string 的泛型设计一致。
  • 默认构造参数T()确保内置类型(如int)默认初始化为 0,自定义类型自动调用其默认构造函数,兼容性强。
  • 指针初始化_prev_next初始化为nullptr,避免野指针风险,后续由容器类统一管理指针链接

2.2 模块 2:迭代器(list_iterator)—— 容器的 “导航工具”

List 的迭代器不是原生指针而是封装list_node*的类,通过运算符重载模拟指针行为,同时用 “三模板参数” 复用普通 /const 迭代器

2.2.1 代码实现
namespace bit
{// 迭代器类:T-数据类型,Ref-引用类型,Ptr-指针类型template<class T, class Ref, class Ptr>struct list_iterator{using Self = list_iterator<T, Ref, Ptr>;  // 简化自身类型名using Node = list_node<T>;                // 节点类型别名Node* _node;                              // 迭代器指向的节点指针// 迭代器构造:接收节点指针初始化list_iterator(Node* node): _node(node){}// 1. 解引用运算符:返回数据引用(普通迭代器可修改,const不可)Ref operator*(){return _node->_data;}// 2. 箭头运算符:支持复杂类型成员访问(如struct.field)Ptr operator->(){return &_node->_data;}// 3. 前置++:向后移动(指向后继节点)Self& operator++(){_node = _node->_next;return *this;}// 4. 后置++:先返回当前,再移动Self operator++(int){Self tmp(*this);_node = _node->_next;return tmp;}// 5. 前置--:向前移动(指向前驱节点)Self& operator--(){_node = _node->_prev;return *this;}// 6. 后置--:先返回当前,再移动Self operator--(int){Self tmp(*this);_node = _node->_prev;return tmp;}// 7. 相等判断:比较节点指针bool operator!=(const Self& s) const{return _node != s._node;}bool operator==(const Self& s) const{return _node == s._node;}};
}
2.2.2 核心解析
  • 三模板参数复用:参考 string 的 const 迭代器设计,RefT&时是普通迭代器(可修改数据),为const T&时是 const 迭代器(只读);Ptr同理,避免单独定义 const 迭代器的代码冗余(单独定义一个的话和普通迭代器区别不大,所以比较浪费)
  • 运算符重载:完全模拟指针行为,遍历(++/--)、访问数据(*/->)的用法与原生指针一致,无需改变使用习惯。
  • 无内存管理:迭代器仅作为 “导航工具”,不负责节点内存的创建与释放,避免与容器类的内存逻辑耦合

2.3 模块3:容器类(list)——List 功能的 “中枢”

容器类整合节点与迭代器,提供构造,插入,删除,遍历等核心接口,底层通过调整指针实现高效的操作(参考 string 的"接口复用"思想,如 push_back 复用 insert)

2.3.1 代码实现
namespace bit
{template<class T>class list{using Node = list_node<T>;  // 节点类型别名public:// 类型重定义:普通/const迭代器(复用list_iterator)using iterator = list_iterator<T, T&, T*>;using const_iterator = list_iterator<T, const T&, const T*>;// -------------------------- 迭代器接口 --------------------------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); }// -------------------------- 初始化接口 --------------------------// 空链表初始化:创建哨兵头节点,形成自环void empty_init(){_head = new Node;_head->_prev = _head;_head->_next = _head;}// 默认构造list() { empty_init(); }// 初始化列表构造(支持{1,2,3}形式)list(initializer_list<T> il){empty_init();for (auto& e : il) push_back(e);}// 范围构造(支持[first, last)区间)template <class InputIterator>list(InputIterator first, InputIterator last){empty_init();while (first != last){push_back(*first);++first;}}// 析构函数:释放所有节点,避免内存泄漏~list(){clear();          // 先删除所有数据节点delete _head;     // 再删除哨兵头节点_head = nullptr;  // 置空指针,避免野指针_size = 0;}// -------------------------- 插入删除接口 --------------------------// 尾插:复用insert,简化代码void push_back(const T& x) { insert(end(), x); }// 头插:复用insertvoid push_front(const T& x) { insert(begin(), x); }// 尾删:复用erasevoid pop_back() { erase(--end()); }// 头删:复用erasevoid pop_front() { erase(begin()); }// 任意位置插入:调整指针实现O(1)插入void insert(iterator pos, const T& x){Node* cur = pos._node;    // pos指向的当前节点Node* prev = cur->_prev;  // 当前节点的前驱Node* newnode = new Node(x);  // 新建数据节点// 调整指针:prev <-> newnode <-> curnewnode->_prev = prev;newnode->_next = cur;prev->_next = newnode;cur->_prev = newnode;++_size;  // 有效元素个数递增}// 任意位置删除:返回下一个有效迭代器,避免失效iterator erase(iterator pos){Node* cur = pos._node;    // 待删除节点Node* prev = cur->_prev;  // 前驱节点Node* next = cur->_next;  // 后继节点// 调整指针:跳过cur节点,连接prev和nextprev->_next = next;next->_prev = prev;delete cur;  // 释放待删除节点内存--_size;     // 有效元素个数递减return iterator(next);  // 返回下一个有效迭代器}// 清空容器:保留哨兵头节点,便于后续复用void clear(){iterator it = begin();while (it != end()) it = erase(it);}// -------------------------- 其他接口 --------------------------// 获取有效元素个数size_t size() const { return _size; }private:Node* _head;   // 哨兵头节点指针size_t _size = 0;  // 有效数据节点个数};
}
2.3.2 核心解析
  • 空初始化empty_init():参考string的reserve初始化逻辑,创建哨兵位头节点统一空链表和非空链表的操作逻辑,避免插入时还需额外判断是"否为头节点"
  • 接口复用push_back/push_front复用insertpop_back/pop_front复用erase,减少代码冗余,与 string 的+=复用push_back思路一致
  • 迭代器失效处理erase返回下一个有效迭代器,用户可通过it=erase(it)更新迭代器,避免访问失效节点,解决List迭代器失效的核心痛点
  • 内存管理:析构函数先clear()删除所有数据节点,再释放哨兵位头节点,确保无内存泄漏;clear()进删除数据节点,保留头节点,便于容器后续复用

三. 功能测试:用test.cpp例子来验证 List 正确性

参考 string 实现博客的 “测试用例 + 结果分析” 风格,用你提供的test.cpp代码,覆盖构造、遍历、插入、删除等核心场景,验证容器功能

3.1 测试用例1:基础构造与遍历(push_back+迭代器/范围for)

void test_list1()
{bit::list<int> lt;bit::list<int>::iterator it = lt.begin();while (it != lt.end()){cout << *it << ' ';++it;}cout << endl;for (auto e : lt){cout << e << ' ';}cout << endl;
}

因为这里还没有尾插数据,所以打印不出来结果

3.1.1 尾插代码实现
void test_list1()
{bit::list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);bit::list<int>::iterator it = lt.begin();while (it != lt.end()){cout << *it << ' ';++it;}cout << endl;for (auto e : lt){cout << e << ' ';}cout << endl;
}
int main()
{test_list1();return 0;
}
3.1.2 测试结果

3.2 测试用例 2:头插、头删、尾删

3.2.1 代码实现
void test_list2()
{bit::list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);lt.push_front(-1);lt.push_front(-2);for (auto e : lt){cout << e << ' ';}cout << endl;lt.pop_back();lt.pop_back();lt.pop_front();lt.pop_front();for (auto e : lt){cout << e << ' ';}cout << endl;cout << lt.size() << endl;
}
int main()
{//test_list1();test_list2();return 0;
}
3.2.2 测试结果

3.3 测试用例 3:const 迭代器与拷贝构造

3.3.1 代码实现
void Print(const bit::list<int>& lt)
{bit::list<int>::const_iterator it = lt.begin();while (it != lt.end()){//const迭代器不能修改//*it = 1;cout << *it << ' ';++it;}
}void test_list3()
{bit::list<int> lt1 = { 1,2,3,4 };for (auto e : lt1){cout << e << ' ';}cout << endl;bit::list<int> lt2(lt1);for (auto e : lt2){cout << e << ' ';}cout << endl;bit::list<int> lt3 = { 10,20,30 };lt1 = lt3;for (auto e : lt1){cout << e << ' ';}cout << endl;Print(lt1);
}
int main()
{//test_list1();//test_list2();test_list3();return 0;
}
3.3.2 测试结果

3.4 测试用例 4:insert、erase 与 clear

3.4.1 代码实现
void test_list4()
{bit::list<A> lt;lt.push_back({ 1,1 });lt.push_back({ 2,2 });lt.push_back({ 3,3 });bit::list<A>::iterator it = lt.begin();while (it != lt.end()){//cout << (*it)._a1 << (*it)._a2 << " ";cout << it->_a1 << it->_a2 << " ";cout << it.operator->()->_a1 << it.operator->()->_a2 << " ";++it;}cout << endl;
}
int main()
{//test_op2();//test_list3();//test_list1();//test_list2();test_list4();return 0;
}
3.4.2 测试结果


补充

核心特性(对比 vector)

特性List(双向链表)vector(动态数组)
插入删除效率任意位置 O(1)(仅需调整节点前驱/后继指针,无需移动其他元素)。例如:在链表中间插入新节点时,只需修改目标位置前后节点的_prev_next指针,操作耗时与链表长度无关中间位置 O(N)(插入/删除后需搬移后续所有元素);尾端操作(无扩容时)接近 O(1)。例如:在数组第 5 位插入元素,需将第 5 位及之后的所有元素向后移动 1 位,元素越多耗时越长
随机访问不支持(需从表头/表尾遍历,时间复杂度 O(N))。无法通过“容器名[下标]”直接访问元素,必须通过迭代器逐步移动(++/--)才能定位目标位置支持(基于原生指针偏移,时间复杂度 O(1))。可通过“容器名[下标]”或at(下标)直接访问元素,例如vec[3]能瞬间定位到第 4 个元素,无需遍历
迭代器失效被删除节点的迭代器失效,其他迭代器(指向未删除节点的)仍有效。例如:删除链表中第 3 个节点后,指向第 2、4 个节点的迭代器可正常使用插入元素时:若触发扩容(原有内存空间不足),所有迭代器、指针、引用均失效;若未触发扩容,仅插入位置之后的迭代器失效。删除元素时,删除位置之后的迭代器均失效
内存利用率按需分配节点(每个节点存储数据+2个指针),无冗余空间,但存在“指针开销”(每个节点额外占用 2 个指针的内存)。内存分配分散,可能存在内存碎片扩容时会预分配额外内存(通常为当前容量的 1.5 倍或 2 倍),可能产生冗余空间(例如容量为 10 但仅存储 5 个元素,剩余 5 个空间闲置)。内存分配连续,缓存命中率更高

通过对比可见:

  • 若场景以 频繁插入/删除(尤其是中间位置) 为主,优先选择 List
  • 若场景以 频繁随机访问、尾端插入 为主,优先选择 vector
  • 补充: List虽然封装了成员sort(底层类似归并排序),但是效率是不如算法库里的,所以如果需要排序还是vector比较好

总结:

往期回顾:

《C++ STL list详解指南》:从接口实现到容器性能对比,掌握你对链表容器的高效使用!

结尾:我们已经手搓出来了list容器,从哨兵节点简化边界逻辑,到双向指针支撑迭代器操作,每一步都印证了 “底层结构决定容器特性”—— 正是链表的设计

http://www.dtcms.com/a/485769.html

相关文章:

  • 成都那家做网站好注册网约车主需要什么条件
  • Wireshark:HTTP、MQTT、WebSocket 抓包详细教程
  • Linux内核架构浅谈36-Linux页帧描述:struct page数据结构的设计与关键成员
  • 道路车辆功能安全标准(FuSa)基础(七)
  • 【Linux系列】解码 Linux 内存地图:从虚拟到物理的寻宝之旅
  • vue+spring boot 利用ffmpeg实现大视频切片
  • 长沙手机网站建设公司wordpress 做笔记
  • Java基于Web3j调用智能智能合约案例
  • 关于联想ThinkCentre M950t-N000 M大师电脑恢复预装系统镜像遇到的一点问题
  • 有关优化网站建设的书籍深圳网络推广方法
  • 招聘网站做竞品分析南昌网站seo多少钱
  • 【实战总结】Docker部署MySQL完整教程:附docker-compose模板与常用命令大全
  • C++ string类的使用
  • 【数据结构】:C 语言常见排序算法的实现与特性解析
  • C语言数据结构:算法复杂度(1)
  • 16km无人机WiFi中继图传模块,高速传输画质高清不卡顿
  • Linux系统C++开发环境搭建工具(二)—— etcd 使用指南
  • AI+大数据时代:如何从架构到生态重构时序数据库的价值?
  • 小小 Postgres,何以替代 Redis、MongoDB 甚至 ES?
  • Win10正式谢幕!附最后更新版本
  • 前端自动翻译插件webpack-auto-i18n-plugin的使用
  • 山东官方网站建设沧州网络推广渠成网络
  • 贺州网站建设公司家装设计需要学什么软件
  • 网站在百度上搜索不到丽水山耕品牌建设网站
  • 漂亮的门户网站dedecms游戏门户网站源码
  • thinkphp2.1网站挂文件国有企业投资建设项目
  • 网站首页的动态视频怎么做的建网站的流程和费用
  • 一些可以做翻译的网站微信小程序制作文档
  • 东莞公司网站开发首页制作教程
  • 河北大名网站建设招聘深圳网站设计首选柚米