深入解析list:一个完整的C++双向链表实现
概述这是一个完整的模板类yyq::list的实现模仿 C 标准库中的std::list。作为STL中最经典的双向链表容器list的实现展示了C模板编程、迭代器设计、链表操作和内存管理的核心技术。本文将完整分析所有代码包括被注释的部分不遗漏任何细节。目录概述一、整体架构与设计1.1 命名空间与头文件保护1.2 链表节点设计二、迭代器设计核心部分2.1 第一阶段两个独立的迭代器类被注释的初始设计2.1.1 普通迭代器 list_iterator2.1.2 const迭代器 list_const_iterator2.1.3 第一阶段设计的详细分析2.2 第二阶段改进的单一模板迭代器2.2.1 第二阶段设计的详细分析2.3 迭代器操作符的详细分析2.3.1 解引用操作符 operator*()2.3.2 箭头操作符 operator-()2.3.3 自增操作符2.3.4 自减操作符2.3.5 比较操作符三、链表类实现3.1 成员变量3.2 迭代器访问函数3.2.1 begin() 和 end()3.2.2 const版本的begin()和end()3.3 初始化函数3.4 构造函数3.4.1 默认构造函数3.4.2 初始化列表构造函数3.4.3 拷贝构造函数3.5 赋值操作符3.5.1 交换函数3.5.2 赋值操作符现代实现3.6 析构函数3.7 清空链表四、链表操作实现4.1 插入操作4.1.1 通用插入函数4.1.2 被注释的push_back实现4.1.3 头部插入4.1.4 尾部插入4.2 删除操作4.2.1 通用删除函数4.2.2 被注释的erase实现4.2.3 头部删除4.2.4 尾部删除4.3 容量相关函数五、辅助函数5.1 通用容器打印函数六、设计模式与编程技巧6.1 哨兵节点模式6.2 拷贝并交换惯用法6.3 模板元编程6.4 RAII资源获取即初始化七、存在的问题与改进建议7.1 存在的问题7.2 改进建议7.2.1 异常安全改进7.2.2 添加移动语义支持7.2.3 添加反向迭代器八、教学价值与学习要点8.1 数据结构学习8.2 C模板编程8.3 STL设计思想8.4 现代C特性九、总结一、整体架构与设计1.1 命名空间与头文件保护cppnamespace yyq { // 链表实现 }使用自定义命名空间yyq避免与标准库冲突模板类设计支持任意类型的元素#pragma once防止头文件重复包含1.2 链表节点设计cpptemplate class T struct list_node { T _data; // 存储的数据 list_nodeT* _next; // 指向下一个节点 list_nodeT* _prev; // 指向前一个节点 // 构造函数 list_node(const T data T()) :_data(data) ,_next(nullptr) ,_prev(nullptr) {} };详细分析模板结构体支持任意类型的数据双向指针_next和_prev实现双向链接支持双向遍历默认构造函数使用T()作为默认值支持内置类型和自定义类型T()对于内置类型是零初始化对于自定义类型调用默认构造函数成员初始化列表高效初始化成员变量命名约定使用下划线前缀_data、_next、_prev常见于成员变量命名二、迭代器设计核心部分代码展示了迭代器设计的完整演进过程分为三个阶段2.1 第一阶段两个独立的迭代器类被注释的初始设计这部分代码被注释掉展示了最初的实现思路2.1.1 普通迭代器list_iteratorTcpptemplateclass T struct list_iterator { typedef list_nodeT Node; typedef list_iteratorT Self; // 类成员变量list迭代器本质是链表节点的指针 Node* _node; // 构造函数 list_iterator(Node* node) :_node(node) {} // 不用写拷贝构造浅拷贝足够了 // 析构也不用写了 T operator*() { return _node-_data; // 返回非常量引用 } T* operator-() { return _node-_data; // 返回非常量指针 } // 简化一下类型名字有点长 typedef list_iteratorT Self; // list_iteratorT operator() 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 s)const { return _node s._node; } bool operator!(const Self s)const { return _node ! s._node; } };2.1.2 const迭代器list_const_iteratorTcpptemplateclass T struct list_const_iterator { typedef list_nodeT Node; typedef list_const_iteratorT Self; // 类成员变量list迭代器本质是链表节点的指针 Node* _node; // 构造函数 list_const_iterator(Node* node) :_node(node) {} // 不用写拷贝构造浅拷贝足够了 // 析构也不用写了 const T operator*() { return _node-_data; // 返回常量引用 } const T* operator-() { return _node-_data; // 返回常量指针 } // 简化一下类型名字有点长 typedef list_iteratorT Self; // list_iteratorT operator() 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 s)const { return _node s._node; } bool operator!(const Self s)const { return _node ! s._node; } };2.1.3 第一阶段设计的详细分析代码重复问题两个类几乎完全相同只有operator*和operator-的返回类型不同list_iterator返回T和T*list_const_iterator返回const T和const T*其他所有代码构造函数、自增自减、比较操作符都完全相同设计思想迭代器本质上是指向链表节点的指针通过操作符重载提供类似指针的语法Self类型别名简化代码编写优缺点优点逻辑清晰分别处理常量和非常量迭代器缺点代码重复严重违反DRY原则Dont Repeat Yourself2.2 第二阶段改进的单一模板迭代器这是当前使用的实现解决了代码重复问题cpptemplateclass T, class Ref, class Ptr struct list_iterator { typedef list_nodeT Node; typedef list_iteratorT, Ref, Ptr Self; // 类成员变量list迭代器本质是链表节点的指针 Node* _node; // 构造函数 list_iterator(Node* node) :_node(node) {} // 不用写拷贝构造浅拷贝足够了 // 析构也不用写了 Ref operator*() { return _node-_data; // Ref可能是T或const T } Ptr operator-() { return _node-_data; // Ptr可能是T*或const T* } // 简化一下类型名字有点长 typedef list_iteratorT Self; // list_iteratorT operator() 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 s)const { return _node s._node; } bool operator!(const Self s)const { return _node ! s._node; } };2.2.1 第二阶段设计的详细分析模板参数设计T元素类型Ref解引用操作符的返回类型引用Ptr箭头操作符的返回类型指针类型别名系统cpp// 在list类中的定义 typedef list_iteratorT, T, T* iterator; typedef list_iteratorT, const T, const T* const_iterator;iterator传递T和T*返回非常量引用和指针const_iterator传递const T和const T*返回常量引用和指针优势代码复用一个模板实现两种迭代器编译时多态通过模板参数决定行为类型安全编译时检查类型一致性设计模式这是模板元编程的典型应用通过模板参数实现策略模式2.3 迭代器操作符的详细分析2.3.1 解引用操作符operator*()cppRef operator*() { return _node-_data; }返回节点数据的引用Ref模板参数决定是常量引用还是非常量引用使得*it可以访问或修改元素值2.3.2 箭头操作符operator-()cppPtr operator-() { return _node-_data; }返回指向节点数据的指针Ptr模板参数决定是常量指针还是非常量指针使得it-member可以访问结构体/类成员2.3.3 自增操作符前置自增cppSelf operator() { _node _node-_next; // 移动到下一个节点 return *this; // 返回当前对象的引用 }修改自身状态返回引用支持链式调用后置自增cppSelf operator(int) { Self temp *this; // 保存当前状态 _node _node-_next; // 移动到下一个节点 return temp; // 返回原来的状态 }参数int仅用于区分前置和后置版本返回临时对象不能链式调用性能略低于前置版本2.3.4 自减操作符实现原理与自增类似只是移动方向相反。2.3.5 比较操作符cppbool operator(const Self s)const { return _node s._node; } bool operator!(const Self s)const { return _node ! s._node; }比较底层节点指针都是const成员函数支持const对象比较简单高效时间复杂度O(1)三、链表类实现3.1 成员变量cppprivate: Node* _head; // 指向哨兵节点 size_t _size; // 链表大小详细分析哨兵节点设计_head指向一个不存储有效数据的节点链表为空时_head-_next _head且_head-_prev _head简化边界条件处理避免空指针检查大小维护维护_size变量使size()操作为O(1)否则需要遍历链表计算大小时间复杂度O(n)3.2 迭代器访问函数3.2.1 begin() 和 end()cppiterator begin() { iterator it(_head-_next); // 第一个有效元素 return it; } iterator end() { iterator it(_head); // 哨兵节点作为结束位置 return it; }代码中的三种等价写法注释中展示cpp// 写法1显式创建临时对象当前使用的写法 iterator it(_head-_next); return it; // 写法2返回匿名对象 return iterator(_head-_next); // 写法3依赖隐式类型转换单参数构造函数 return _head-_next; // 编译器自动转换为iterator3.2.2 const版本的begin()和end()cppconst_iterator begin()const { const_iterator it(_head-_next); return it; } const_iterator end()const { const_iterator it(_head); return it; }用于const对象返回const_iterator只读访问3.3 初始化函数cppvoid empty_init() { _head new Node; // 创建哨兵节点 _head-_next _head; // 指向自己 _head-_prev _head; // 指向自己 _size 0; // 大小为0 }所有构造函数的基础创建自循环的哨兵节点注意哨兵节点的_data使用默认构造函数初始化3.4 构造函数3.4.1 默认构造函数cpplist() { empty_init(); }创建一个空链表只有哨兵节点3.4.2 初始化列表构造函数cpplist(initializer_listT il) { empty_init(); for (auto e : il) { push_back(e); // 逐个添加元素 } }支持现代C语法cppyyq::listint lst {1, 2, 3, 4, 5}; // 初始化列表3.4.3 拷贝构造函数cpplist(const listT lt) { empty_init(); for (auto e : lt) { push_back(e); // 深拷贝每个元素 } }详细分析深拷贝创建新节点复制数据使用范围for循环依赖迭代器支持时间复杂度O(n)n为源链表大小异常安全如果push_back抛出异常已分配的资源需要清理3.5 赋值操作符3.5.1 交换函数cppvoid swap(listT lt) { std::swap(_head, lt._head); std::swap(_size, lt._size); }使用std::swap交换指针和大小效率高不涉及元素复制3.5.2 赋值操作符现代实现cpplistT operator(listT lt) { swap(lt); return *this; }拷贝并交换技法详细分析参数lt是值传递自动调用拷贝构造函数交换当前对象和lt的资源lt离开作用域时自动析构释放原资源异常安全如果拷贝构造失败不会修改当前对象自赋值安全参数是值传递不会影响自赋值3.6 析构函数cpp~list() { clear(); // 删除所有有效节点 delete _head; // 删除哨兵节点 _head nullptr; }详细分析先调用clear()删除所有数据节点再删除哨兵节点将指针置为nullptr避免悬空指针注意对于自定义类型会调用每个元素的析构函数3.7 清空链表cppvoid clear() { // iterator it begin(); auto it begin(); while (it ! end()) { it erase(it); // 返回下一个迭代器 } }详细分析使用auto简化类型声明安全删除方式使用erase的返回值更新迭代器避免迭代器失效直接使用erase(it)会使it失效时间复杂度O(n)n为链表大小四、链表操作实现4.1 插入操作4.1.1 通用插入函数cppiterator insert(iterator pos, const T x) { Node* newnode new Node(x); // 创建新节点 Node* cur pos._node; // 当前位置节点 Node* prev cur-_prev; // 前一个节点 // 连接新节点 newnode-_next cur; cur-_prev newnode; newnode-_prev prev; prev-_next newnode; _size; // 更新大小 return newnode; // 返回指向新节点的迭代器 }连接步骤详细分析newnode-_next cur新节点的next指向当前位置cur-_prev newnode当前位置的prev指向新节点newnode-_prev prev新节点的prev指向前一个节点prev-_next newnode前一个节点的next指向新节点返回值返回指向新插入元素的迭代器符合STL规范4.1.2 被注释的push_back实现cpp//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; // // _size; //}详细分析直接实现不依赖insert效率可能比insert(end(), x)略高少一次函数调用代码复用当前实现使用insert(end(), x)更简洁逻辑在哨兵节点前插入即链表尾部4.1.3 头部插入cppvoid push_front(const T x) { insert(begin(), x); // 在开始位置插入 }4.1.4 尾部插入cppvoid push_back(const T x) { insert(end(), x); // 在结束位置前插入 }注意插入到end()位置实际上是在哨兵节点前插入即链表尾部4.2 删除操作4.2.1 通用删除函数cppiterator erase(iterator pos) { assert(pos ! end()); // 不能删除哨兵节点 Node* next pos._node-_next; Node* prev pos._node-_prev; // 跳过要删除的节点 prev-_next next; next-_prev prev; delete pos._node; // 释放节点内存 --_size; // 更新大小 return next; // 返回下一个有效迭代器 }详细分析断言检查确保不删除哨兵节点连接调整prev-_next next前一个节点跳过当前节点next-_prev prev后一个节点跳过当前节点内存释放delete节点调用元素析构函数返回值返回下一个有效迭代器避免迭代器失效4.2.2 被注释的erase实现cpp//void erase(iterator pos) //{ // Node* cur pos._node; // Node* prev cur-_prev; // Node* next cur-_next; // // prev-_next next; // next-_prev prev; // // delete cur; // cur nullptr; // // --_size; //}对比分析没有返回值调用者无法知道删除后的下一个有效位置迭代器失效调用后pos失效但调用者不知道当前实现更好返回下一个迭代器更安全4.2.3 头部删除cppvoid pop_front() { erase(begin()); // 删除第一个元素 }4.2.4 尾部删除cppvoid pop_back() { // erase(end()._node-_prev); erase(--end()); // 删除最后一个元素 }两种写法erase(--end())使用迭代器操作erase(end()._node-_prev)直接访问节点指针4.3 容量相关函数cppsize_t size()const { return _size; // 直接返回存储的大小 } bool empty()const { return _size 0; // 检查大小是否为0 // 或者: return _head-_next _head; }详细分析O(1)复杂度维护_size变量注释中的替代方案_head-_next _head检查链表是否为空const成员函数可以用于const对象五、辅助函数5.1 通用容器打印函数cpptemplateclass Container void print_container(const Container con) { // 方法1使用迭代器 typename Container::const_iterator it con.begin(); while (it ! con.end()) { cout *it ; it; // 注意原代码这里缺少it是bug } cout endl; // 方法2使用范围for循环 for (auto e : con) { cout e ; } cout endl; }详细分析模板函数可以打印任何支持迭代器的容器typename关键字必须使用告诉编译器Container::const_iterator是一个类型因为Container是模板参数编译器不知道const_iterator是类型还是静态成员Bug原代码while循环中缺少it会导致死循环两种遍历方式传统迭代器遍历范围for循环依赖容器的begin()和end()通用性可以用于yyq::list、yyq::vector等任何STL兼容容器六、设计模式与编程技巧6.1 哨兵节点模式目的简化边界条件处理实现创建不存储数据的头节点优点统一插入删除逻辑避免空指针检查6.2 拷贝并交换惯用法目的实现异常安全的赋值操作实现参数值传递 swap优点自动处理自赋值异常安全6.3 模板元编程目的代码复用编译时多态实现迭代器模板参数化优点消除代码重复类型安全6.4 RAII资源获取即初始化目的自动管理资源实现构造函数分配析构函数释放优点避免资源泄漏七、存在的问题与改进建议7.1 存在的问题异常安全性不足new可能失败抛出std::bad_alloc插入操作中如果new失败应保持链表不变缺少移动语义支持C11移动构造函数移动赋值运算符emplace操作迭代器安全性缺少迭代器有效性验证多线程环境不安全功能不完整缺少反向迭代器缺少sort、merge、unique等算法缺少resize、assign等方法7.2 改进建议7.2.1 异常安全改进cppiterator insert(iterator pos, const T x) { Node* newnode nullptr; try { newnode new Node(x); // 可能抛出异常 } catch (...) { // 如果new失败链表保持不变 throw; // 重新抛出异常 } // ... 连接操作不会抛出异常 return iterator(newnode); }7.2.2 添加移动语义支持cpp// 移动构造函数 list(list other) noexcept : _head(other._head) , _size(other._size) { other._head nullptr; other._size 0; } // 移动赋值运算符 list operator(list other) noexcept { if (this ! other) { clear(); delete _head; _head other._head; _size other._size; other._head nullptr; other._size 0; } return *this; } // emplace操作 template typename... Args iterator emplace(iterator pos, Args... args) { Node* newnode new Node(T(std::forwardArgs(args)...)); // ... 连接操作 return iterator(newnode); }7.2.3 添加反向迭代器cpptypedef std::reverse_iteratoriterator reverse_iterator; typedef std::reverse_iteratorconst_iterator const_reverse_iterator; reverse_iterator rbegin() { return reverse_iterator(end()); } reverse_iterator rend() { return reverse_iterator(begin()); } const_reverse_iterator rbegin() const { return const_reverse_iterator(end()); } const_reverse_iterator rend() const { return const_reverse_iterator(begin()); }八、教学价值与学习要点8.1 数据结构学习双向链表的基本操作哨兵节点的优势与应用链表与数组的性能对比8.2 C模板编程类模板与函数模板模板参数推导模板元编程技巧8.3 STL设计思想迭代器模式容器与算法的分离泛型编程哲学8.4 现代C特性异常安全编程RAII原则移动语义九、总结这个yyq::list实现是一个优秀的教学范例它完整展示了从简单到复杂的演进过程初始的两个独立迭代器类改进的模板化迭代器展示了代码重构和优化的思路完整的功能实现链表节点的创建与连接迭代器的完整实现基本操作的实现拷贝控制和资源管理现代C编程实践模板编程异常安全考虑代码复用技巧STL兼容性设计标准的迭代器接口容器约定的方法与算法协同工作虽然与标准库的std::list相比还有差距但作为一个教学实现它完美展示了双向链表和迭代器的核心原理是学习C容器设计和模板编程的优秀范例。通过分析这个实现可以深入理解STL的设计哲学和实现细节。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2423625.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!