news 2026/10/11 1:48:09

C++模拟实现list保姆级教程,带你避开所有坑!

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++模拟实现list保姆级教程,带你避开所有坑!

注意事项

list是需要模版来实现的容器,所以申明与定义必须处于同一文件下,否则会出现:

“error: undefined reference to `...`”错误

原因:模版函数调用需要实例化,而实例化需要看到定义,但如果定义放在其它文件(比如.cpp),在其它文件中,这个函数仍然是未实例化的模版,既然没有实例化,就没有机器码,没有机器码就会链接失败。

list的节点类

(注意:我们这里实现的是双向循环链表。)

list的节点类我们使用struct来封装,而非class,好处在于struct默认public。

list的节点类中包含前驱指针,后驱指针这些后面会频繁访问的成员,由于class默认是private,而struct的特点就能省去很多麻烦,这也是二者唯一的区别。

构造函数我们需要写两个版本,一个数据构造和无参构造,原因在于list是有头节点的,而头结点通常不存储数据。

为了不与标准库冲突,建议使用自己的命名域封装一下,具体代码如下:

//list的节点类 template<class T> struct ListNode { //数据节点构造 ListNode(const T& x) :_pPre(nullptr) ,_pNext(nullptr) ,_val(x) { } //头节点构造 ListNode() :_pPre(nullptr) , _pNext(nullptr) { } ListNode<T>* _pPre; //前驱指针 ListNode<T>* _pNext; //后驱指针 T _val; };

list的迭代器类

list的迭代器实现起来与vector有很大不同,我们先把申明贴过来,再一一说明。

template<class T, class Ref, class Ptr> struct ListIterator { typedef ListNode<T>* PNode; typedef ListIterator<T, Ref, Ptr> Self; PNode _pNode;//指向节点的指针 ListIterator(PNode pNode = nullptr);//构造 ListIterator(const Self& l);//拷贝构造 Ref operator*();//取值 Ptr operator->();//访问成员 Self& operator++();//前置++ Self operator++(int);//后置++ Self& operator--();//前置-- Self operator--(int);//后置-- bool operator==(const Self& l);//等于 bool operator!=(const Self& l);//不等于 };

Ref与Ptr

首先,先说明参数中,Ref和Ptr的含义。

简单来说,如果T是int,那么Ref就是int&,Ptr就是int*,它们分别代表一种返回类型,即引用类型和指针类型。

那为何不直接使用T&和T*?

原因在于,T&和T*无法传递const属性,Ref和Ptr可以。比如,如果你想使用一个只读的迭代器:

const list<int> cv = {1,2,3}; auto it = cv.begin(); // 是 const_iterator,只读 *it = 5;

但T*这种写法无法传递const属性,到最后begin仍然被修改为5,但Ref就不会发生这种错误。

有人可能会觉得,我另外写一个const T& operator*()和const T* operator->()的版本,重载*和->不就行了?

但问题是这两个版本的重载签名完全相同,你不能塞到同一个struct里,如果你执意这么写,最后你就要写两份迭代器struct,而二者大多功能完全相同,没必要这样。

operator*和operator->

在具体实现中,重载*和->的函数必须是const修饰的成员函数。

原因在于我们需要适应一个场景:迭代器对象本身是const类型,需要const类型的operator来接收。

const cIt = l.begin() //const类型的迭代器 *cIt;//需要const类型的operator*

具体来说:

我们说的const_iterator 和 const iteraator不是同一个东西。

前者的迭代器类型是非const,对operator没要求,它只是指向的list的元素不可修改,这个情况通过Ref和Ptr已经解决,而后一个是迭代器本身就是const,本身就不可修改,所以需要const类型operator才能适应。

operator!=和operator==

重载等于和不等于时,也有需要注意的地方。

具体而言这两个需要除了也是const成员,还有一个要求就是,它们需要再套一个模版。

原因在于,C++标准库中,这两个允许不同类型的迭代器进行比较,为了适应这个场景,我们必须单独给这两个函数的第二个操作数套一个模版。

具体代码

其它的功能基本上没什么坑,大家自己看看代码就行。

template<class T, class Ref, class Ptr> struct ListIterator { typedef ListNode<T>* PNode; typedef ListIterator<T, Ref, Ptr> Self; PNode _pNode;//指向节点的指针 ListIterator(PNode pNode = nullptr) : _pNode(pNode) {}//构造 ListIterator(const Self& l) : _pNode(l._pNode) {}//拷贝构造 Ref operator*() const { return _pNode->_val; }//取值 Ptr operator->() const { return &_pNode->_val; }//访问成员 Self& operator++() { _pNode = _pNode->_pNext; return *this; }//前置++ Self operator++(int) { Self tmp = *this; _pNode = _pNode->_pNext; return tmp; }//后置++ Self& operator--() { _pNode = _pNode->_pPre; return *this; }//前置-- Self operator--(int) { Self tmp = *this; _pNode = _pNode->_pPre; return tmp; }//后置-- template <class Ref2, class Ptr2> bool operator==(const ListIterator<T, Ref2, Ptr2>& l) const { return _pNode == l._pNode; }//等于 template <class Ref2, class Ptr2> bool operator!=(const ListIterator<T, Ref2, Ptr2>& l) const { return _pNode != l._pNode; }//不等于 };

list类

基本框架

首先需要写好两种迭代器的两个版本。

_pHead指向哨兵节点,_size是节点个数。

template<class T> class list { typedef ListNode<T> Node; typedef Node* PNode; public: typedef ListIterator<T, T&, T*> iterator; typedef ListIterator<T, const T&, const T*> const_iterator; iterator begin() { return iterator(_pHead->_pNext); } iterator end() { return iterator(_pHead); } const_iterator begin() const { return const_iterator( _pHead->_pNext); } const_iterator end() const { return const_iterator(_pHead); } private: PNode _pHead; size_t _size; }

注意迭代器同样遵循“左闭右开”的原则,所以end()是返回哨兵节点。

构造,拷贝构造,析构,赋值重载

双向循环链表的默认构造需要产生一个自环的哨兵节点,析构则是要先实现clear()清空list,最后delete哨兵节点。注意clear清空list后要让剩下的哨兵节点自环。

赋值重载和拷贝构造一般都是先实现了push_back后再回来复用实现会比较好,但是赋值拷贝最好的写法是使用copy-and-swap办法。

我们先实现一个成员swap,交换两个list的_pHead和_size,再将operator=的参数改为传值拷贝,把参数list和this交换,最后返回this即可。

void swap(list& l) { std::swap(_pHead, l._pHead); std::swap(_size, l._size); }
//构造 list() :_pHead(new Node) ,_size(0) { _pHead->_pNext = _pHead->_pPre = _pHead; } list(const list& l) :_pHead(new Node) ,_size(0) { _pHead->_pNext = _pHead->_pPre = _pHead; for (auto& x : l) { push_back(x); } } void clear() { PNode cur = _pHead->_pNext; while (cur != _pHead) { PNode next = cur->_pNext; delete cur; cur = next; } _pHead->_pNext = _pHead->_pPre = _pHead; _size = 0; } //析构 ~list() { clear(); delete _pHead; } //赋值拷贝 list& operator=(list l) { swap(l); return *this; }

增删查改

这里主要包括insert,push_back, push_front, erase,pop_back, pop_front。

我们可以先实现insert和erase,其它的只需复用即可。

insert需要传入插入位置的迭代器和需插入的数据,然后返回插入后该节点的迭代器,逻辑就是普通的双向链表插入。

iterator insert(iterator pos,const T& x) { PNode n = new Node(x); n->_pNext = pos._pNode; n->_pPre = pos._pNode->_pPre; pos._pNode->_pPre->_pNext = n; pos._pNode->_pPre = n; _size++; return iterator(n); }
void push_back(const T& x) { insert(end(), x); } void push_front(const T& x) { insert(begin(), x); }

然后是erase。

iterator erase(iterator pos) { PNode prev = pos._pNode->_pPre, next = pos._pNode->_pNext; prev->_pNext = next; next->_pPre = prev; delete pos._pNode; _size--; return iterator(next); } iterator pop_back() { return erase(--end()); } iterator pop_front() { return erase(begin()); }

还有各种访问器,例如size()返回有效节点个数,front()和back()分别返回第一个与最后一个节点的引用, 以及判空empty()。

这些的逻辑都比较简单。不过需要注意的是,front和back需要实现const的版本。

size_t size() const { return _size; } bool empty() const { return _size == 0; } T& front() { return begin()._pNode->_val; } const T& front() const { return begin()._pNode->_val; } T& back() { return (--end())._pNode->_val; } const T& back() const { return (--end())._pNode->_val; }

list与vector的比较

从使用场景的角度来看,list适用于需要频繁插入删除的场景,而vector适用于需要高效存储,支持随机访问的场景。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/11 1:48:06

医疗文本分类实战:MIMIC-IV上word2vec+轻量Transformer端到端落地

简介&#xff1a;本资源是一套面向自然语言处理初学者与医疗AI实践者的PyTorch实战项目&#xff0c;聚焦英文医学影像报告文本分类任务&#xff0c;适用于高校学生、NLP入门开发者及医疗信息化方向研究者。项目基于真实临床数据集MIMIC-IV&#xff0c;完整实现从Word2Vec词向量…

作者头像 李华
网站建设 2026/10/11 1:47:30

DLP和沙箱检测

一、定义 1. DLP 规则&#xff08;数据防泄露规则&#xff09; DLP&#xff08;Data Loss Prevention&#xff09;&#xff0c;俗称“数据防泄露”系统。它的核心目的是防止公司的敏感数据被内部人员有意或无意地传出去。 2. 沙箱规则&#xff08;虚拟环境运行&#xff09; 沙…

作者头像 李华
网站建设 2026/10/11 1:47:14

数字工厂规划蓝图:69页PPT背后的可执行技术契约

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/11 1:46:49

精选 21道 Redis 最常问面试题!

Redis 作为后端开发面试中的高频考点&#xff0c;几乎每一次 Java、Go、Python、Node.js 岗位面试都会涉及。它不仅是缓存组件&#xff0c;更在分布式锁、消息队列、排行榜、计数器、限流等场景中发挥着重要作用。本文精选了 21 道 Redis 最常问的面试题&#xff0c;覆盖基础概…

作者头像 李华