【C++list】底层结构、迭代器核心原理与常用接口实现全解析

作者:m0_74823364日期:2025/10/27

一、官方源码的探究

在实现list的底层前,我们先看下官方的核心成员变量,link_type node,其中link_type是list_node*,也就是说是节点的指针

在这里插入图片描述

在这里插入图片描述

下面我们看下其的初始化,在空初始化中,链表为空并不是把节点的指针给成空,而是给了个节点,让其的前驱指针和后继指针均指向自己,在C语言阶段的数据结构中我们便知道这个节点是哨兵位头节点

注意: 这里创捷新的节点不是new的,而是使用get_node出来的,这里是由于内存池的原因,后续再介绍

在这里插入图片描述

在这里插入图片描述

二、list底层的构建及其尾插

2.1 list底层探索

实现链表首先需要实现节点,即其的前驱指针,后继指针,和其保存的数据

代码语言:javascript

代码运行次数:0

运行

AI代码解释

1namespace hxx
2{
3   template<class T>
4   class list_node
5   {
6         T _data;
7         list_node* _prev;
8         list_node* _next;
9list_node(const T& data)
10:_data(data)
11,_next(nuppptr)
12,_prev(nullptr)
13{}
14    }:
15    }

链表是一个个节点组成的结构,在实现完节点后,我们便可以轻松实现list了的初始化结构,我们仿照官方实现的结构来实现,由于我们并没有实现内存池,因此这里创建新节点依旧是new

注意: 这里我们多定义了个size变量是为了方便计算节点的数量,每次增加节点或者删除节点,直接让size++/- - 更加方便,不需要在遍历链表

为什么官方在初始化list时,使用的方法是头结点的next和prev指针均指向自己,是为了防止后面实现的end和begin接口在空节点的特殊情况下的处理

代码语言:javascript

代码运行次数:0

运行

AI代码解释

1template <class T>
2class list
3{
4   typedef list_node<T> Node;
5public:
6    list()
7    {
8      _head=new Node;
9      _head->next=_head;
10      _head->prev=_head;
11      _size=0;
12    }
13
14private:
15    Node*_head;
16    size_t size;
17} 
2.2 push_back

在实现尾插前我们需要找到链表的尾部, 哨兵位头结点的前驱指针便是尾节点tail,再让tail的后继指针指向要尾插的新节点newnode,newnode的前驱指针指向tail,最后别忘让newtail成为新的尾节点,因此需要让newtail的后继指针指向哨兵位头结点,哨兵位头结点指向newnode即可

代码语言:javascript

代码运行次数:0

运行

AI代码解释

1void push_back()
2{
3  //newd对象后需要调用构造函数,因此还需要再节点类型里定义构造函数
4    Node*newnode=new Node(x);
5    Node*tail=_head->prev;
6    tail->next=newnode;
7    newnode->prev=tail;
8    newnode->next=_head;
9    _head->prev=newnode;
10    ++size;
11   }

三、实现普通迭代器的遍历

list相对于vector和string来说,难实现的是迭代器,因为vector和string的结构有着先天的优势——地址是连续的,使用原生指针可以直接访问。因此list的迭代器需要类型对节点的指针进行封装

在上面我们完成了list的基本底层结构和尾插,为了判断我们实现的代码是否有问题,那么我们打印一遍就知道了,这里必然要用到迭代器去遍历链表,因此实现list版本的迭代器是不可或缺的重点

我们只需要将如下的代码跑通便代表迭代器实现了

代码语言:javascript

代码运行次数:0

运行

AI代码解释

1void test()
2{
3  list<int> lt;,
4  lt.push_back(1);
5  lt.push_back(2);
6  lt.push_back(3);
7  lt.push_back(4);
8  
9  list<int>::iterator it=lt.begin();
10  while(it!=lt.end())
11  {
12     cout<<*it<<" ";
13     ++it;
14   }
15   cout<<endl;
16}
3.1运算符*/++/–的重载

那么这里的迭代器我们需要如何实现其底层结构?我们要去遍历节点,我们使用节点的指针是搞不定的,因为list的地址是不连续的,++指针是不能找到下一个节点。但是节点力存放了下一个节点的地址,因此我们考虑用类进行封装通过重载运算符实现迭代器,由于直接解引用得到的不是data数据,因此我们可以重载operator*来实现,同理也可以重载++来找到下一个节点

代码语言:javascript

代码运行次数:0

运行

AI代码解释

1//由于链表的类型不确定,因此需要使用模板
2template <class T>
3//这里使用struct是因为其默认是公有,可以直接给list提供公共接口
4struct list_iterator
5{
6   typedef list_node<T>  Node;
7   //重载++就是迭代器++,也就是迭代器的类型,这里可以typedef下
8   typedef list_iterator<T> Self;
9    Node* _node;
10   
11   //构造函数
12   list_iterator(Node* node)
13   :_node(node)
14   {
15   }
16   T&  operator*()
17   {  
18    
19    return _node->_data;
20   }
21   
22  Self& operator++()
23   {
24    _node=node->_next;
25    return *this;
26   }
27    
28    //两个迭代器的比较
29   bool operator!=(const Self& s)
30    {
31        return _node!=s._node;
32    }
33
34};

实现完前置++/- -,我们再实现下后置++/- -,后置是返回++/- -之前的值,但是其在实现的时候还是需要++/- -

代码语言:javascript

代码运行次数:0

运行

AI代码解释

1//后置++
2Self& operator++(int)
3{
4    Self tmp(*this);
5    _node=_node->next;   
6    return *this;
7}
8
9//后置- -
10Self& operator--(int)
11{   
12   //在这里我们用到了拷贝,但是我们自己却没有实现拷贝构造函数,而是让编译器自己生成的,这里仅是值拷贝,因为我们希望迭代器    
13   //iterator也指向该节点,这里就是浅拷贝,而不是深拷贝,注意并不是有指针就是深拷贝,而是要看指针所指向的资源是否属于自己,属于自己需要深拷贝,而迭代器中的指针指向的资源不属于它的,因此仅为浅拷贝
14
15    Self tmp(*this);
16    _node=_node->prev;   
17    return *this;
18}

总结:资源不属于当前对象,只是 “借用” 或 “引用” 资源,资源的生命周期由其他对象管理,那么拷贝时只需要浅拷贝,因为即使多个指针指向同一份资源,也不会有冲突(资源的释放由真正的所有者负责)

,我们解引用不再使用对象+点来访问,而是通过->l来进行,因c还需要运算符重载下->

3.2 自定义类型下的运算符重载

如若list的类型不再是内置类型而是自定义类型如struct时,下面我们来看个struct类型的例子,看是否可以遍历list

代码语言:javascript

代码运行次数:0

运行

AI代码解释

1struct AA
2{
3   int a1=1;
4   int a2=1;
5
6
7};
8
9
10void test1()
11{
12   list<AA> lt;
13   lt.push_back(AA());
14   lt.push_back(AA());
15   lt.push_back(AA());
16   lt.push_back(AA());
17   
18   list<AA>::iteerator it=lt.begin();
19   while(it!=lt.end())
20   {   
21   cout<<*it<<" ";
22   ++it;
23
24   }
25   cout<<endl;
26}

结果演示:

在这里插入图片描述

在这里插入图片描述

很显然我们运行错误,因为lt解引用后是自定义类型,<<不支持自定义类型的使用,因此在这里有三种方法

1.在struct里面重载流插入 2.通过(*lt)._a1,(*lt)_a2来访问 3.通过重载operator->

这里我们按照 编译器 所推荐的->的重载进行解决

代码语言:javascript

代码运行次数:0

运行

AI代码解释

1T* operator->()
2{
3     return &_node->_data;
4}
5
6list<AA>::iterator it=lt.begin();
7while(it!=lt.end())
8{
9   //lt调用operator->,operator->返回的是T*,_data是AA类型的,返回的便是AA*,那么AA*是怎么访问_a1的?
10   //实际上这里应该是两个箭头:it->->_a1,但是在编译器中不支持两个箭头,第一个箭头是运算符重载(lt.operator->())返回底层的指针AA*,第二个箭头便是对AA*的_a1的解引用(原生指针)
11   cout<<lt->_a1<<":"<<lt->_a2<<endl;
12   ++it;
13}
14
15cout<<endl;
3.3 迭代器遍历

最后我们在list类的public typedef下迭代器

代码语言:javascript

代码运行次数:0

运行

AI代码解释

1typedef list_iterator<T> iterator;
2
3iterator begin()
4{
5  
6 //  return iterator (_head->_next);
7   //当然也可以如下写法,因为单参数构造函数支持隐式类型转换
8   return _head->_next;
9}
10
11
12iterator end()
13{
14
15   //隐式类型转换,这里需要注意end返回的尾节点的后一个节点,尾节点是_head->_prev,因此后一个节点是_head
16    iterator _head;
17
18}

经过上面的代码实现迭代器便已经可以跑了,下面我们在测试代码试下

代码语言:javascript

代码运行次数:0

运行

AI代码解释

1#include"list.h"
2
3hxx::test1();

在这里插入图片描述

在这里插入图片描述

因为只要new对象了必然会调用构造函数,node类中我们没有写构造函数因此导致报错,在写构造函数时我们需要注意push_back new的对象是带参的构造,因此需要给list类中的node传参,这里需要注意的是只能传匿名对象T(),或者可以让list_node类中的构造函数传匿名对象T()

代码语言:javascript

代码运行次数:0

运行

AI代码解释

1template<class T>
2struct list_node
3{
4	T _data;
5	list_node<T>* _next;
6	list_node<T>* _prev;
7
8	list_node(const T& data = T())
9		:_data(data)
10		, _next(nullptr)
11		, _prev(nullptr)
12	{
13	}
14};

结果演示

在这里插入图片描述

在这里插入图片描述

总结:最后我们也实现了list的迭代器的接口,虽然list和string,vector的实现不同,但是最后的效果都一样,举个很简单的例子,每个人前往相同目的地,有的人是走路,有的人骑车,有的人开车,方式各不相同,但是最后都到达了终点。这里是一样的,上层调用接口都是一样的方法,底层各不相同

四、实现const迭代器的遍历

4.1 print_container的迭代器问题

在模拟vector的底层时我们实现了print_container的打印,我们直接把代码CV过来·,并用其打印测试下list

代码语言:javascript

代码运行次数:0

运行

AI代码解释

1template<class Container>
2void print_container(const Container& v)
3{
4  for(auto e:v)
5  {
6  cout<<e<<" ";
7  }
8   cout<<endl;
9}
10void test1()
11{
12list<AA> lt;
13lt.push_back(A());
14lt.push_back(A());
15lt.push_back(A());
16lt.push_back(A());
17
18for(auto e:lt)
19{
20  cout<<e<<" ";
21}
22cout<<endl;
23print_container(lt);
24}

编译器报错说范围for出了问题,那么我们在test1()中屏蔽print_container()并调用范围for来进行测试,但是奇怪的是我们在test1函数里面实现的范围for并没有问题

这是因为在test1中使用的是普通容器,因此将范围for转换成普通的迭代器,而print_contaier()中实现的是const迭代器,对应的便是const容器,但是print_container()中并没有实现const迭代器,因此其中的范围for跑不了

注意:为什么这里const迭代器的使用是const_iterator,而不是const iterator,这里的原理和C语言中T_const ptr指的是指针本身不能被修改(const在_之后修饰的是指针本身),const T* ptr(const在*之前修饰的是指针指向的内容)。

因此const iterator,const在iterator之前修饰的是iterator即迭代器本身不能被修改,那么便无法++,也就不能遍历,因此我们使用const_iterator即指向的内容不能被修改,而list访问数据是通过迭代器中的*/->进行的,因此想要让const迭代器实现,必须让它们变为const函数才可以。

4.2 按需实例化

代码语言:javascript

代码运行次数:0

运行

AI代码解释

1// 按需实例化
2template<class Container>
3void print_container(const Container& con)
4{
5	// const iterator -> 迭代器本身不能修改
6	// const_iterator -> 指向内容不能修改
7   list<int>::const_iterator it=con,begin();
8	while (it != con.end())
9	{
10		*it += 10;
11    cout << *it << " ";
12		++it;
13	}
14	cout << endl;
15
16	for (auto e : con)
17	{
18		cout << e << " ";
19	}
20	cout << endl;
21}
22void test_list1()
23{
24	list<int> lt;
25	lt.push_back(1);
26	lt.push_back(2);
27	lt.push_back(3);
28	lt.push_back(4);
29
30	list<int>::iterator it = lt.begin();
31	while (it != lt.end())
32	{
33		*it += 10;
34
35		cout << *it << " ";
36		++it;
37	}
38	cout << endl;
39
40	for (auto e : lt)
41	{
42		cout << e << " ";
43	}
44	cout << endl;
45	//print_container(lt);
46}

该段代码在编译器上可以跑,但是我们仔细看下,这里使用的是const迭代器,但是我们在while循环中对迭代器指向的内容进行了修改,这里便是因为模板走的是按需实例化(不能直接调用生成,只有实例化后才能调用生成对应的代码),函数模板在这里没有进行调用,编译器只会对其进行基础的扫描查看模板中有无明显的错误(多写一个;少个[),但不会检查细节的错误(调用才实例化),因此如若将print_contaier放出来则报错

4.3 模板解决迭代器代码冗余

为了实现各个场景下list的使用,const迭代器中所有接口均要再从普通迭代器中拷贝一份,但是这样便会导致代码冗余,这里我们看下官方是通过同一个类模板传三个模板参数实现的T,T&,T*实现的,这样我们就不需要写两个类便可以实现

在这里插入图片描述

在这里插入图片描述

代码实现:

代码语言:javascript

代码运行次数:0

运行

AI代码解释

1template<class T,class Ref,class Ptr>
2struct list_iterator
3{
4   typedef list_node<T,Ref,Ptr> Node;
5   typedef list_iterator<T,Ref,Ptr>Self;
6   Node*_node;
7   list_iterator(Node* node)
8   :_node(node);
9   {}
10   
11   Ref operator*()
12   {
13      return _node->data;
14    }
15    Ptr opertor->()
16    {
17    //箭头返回对象类型的指针,指针解引用找到其存储的数据
18    	return &_node->_data;
19    }
20}

总结:官方实现的类模板给给编译器,因为给了不同的模板参数,编译器实例化了两个不同的类

五、迭代器失效

在链表中的迭代器失效与string和vector不同,因为在链表中插入数据不再导致迭代器失效,因为list的地址不连续,在目标节点前插入数据,不需要挪动其他数据,只需要让目标结点和插入节点的指针进行链接即可,迭代器便不会失效

代码语言:javascript

代码运行次数:0

运行

AI代码解释

1void test_list()
2{
3   list<int> lt;
4   lt.push_back(1);
5   lt.push_back(2);
6   lt.push_back(3);
7   lt.push_back(4);
8   
9   list<int>::iterator it=lt.begin();
10   lt.insert(it,10);
11   *it+=100;
12   
13   print_container(lt);
14
15}

但是删除节点便会导致迭代器失效,因为删除目标节点会导致指向目标节点的迭代器成为野指针

代码语言:javascript

代码运行次数:0

运行

AI代码解释

1auto it=lt.begin();
2while(it!=lt.end())
3{
4 //删除所有的偶数
5   if(*it%2==0)
6   {
7       lt.erase(it);
8
9    }
10    else
11    {
12    ++it;
13    }
14}

因此我们这里同样需要让迭代器去接受erase的返回值(返回下一个位置的迭代器)

六、常见接口的实现及其源码

5.1 insert 插入

在pos位置之前插入数据,我们需要知道pos位置的前驱指针,并将prev newnode 和pos节点三者重新进行连接,注意在插入完数据后还需要让_size++

代码语言:javascript

代码运行次数:0

运行

AI代码解释

1iterator insert(iterator pos, const T& x)
2{
3	Node* cur = pos._node;
4	Node* prev = cur->_prev;
5	Node* newnode = new Node(x);
6
7	// prev newnode cur
8	newnode->_next = cur;
9	cur->_prev = newnode;
10	newnode->_prev = prev;
11	prev->_next = newnode;
12
13	++_size;
14
15	return newnode;
16}
5.2 push_back /push_front

我们在前面实现了insert插入后,发现push_back /push_front不再需要自己实现,直接赋用insert即可,因为头插和尾插的底层依旧是insert

代码语言:javascript

代码运行次数:0

运行

AI代码解释

1void push_front(const T& x)
2{
3   insert(begin(),x);
4
5}
6
7//在哨兵位头结点之前插入数据相当于尾插
8void push_back()
9{
10    insert(end(),x);
11}
5.3 erase/pop_back/pop_front

删除pos位置节点,只需要找到pos位置的前驱和后继节点,再进行链接即可,最后不要忘记- -size

注意: erase不能删除哨兵位头结点

代码语言:javascript

代码运行次数:0

运行

AI代码解释

1iterator erase(pos)
2{   
3
4     assert(pos!=end());
5     Node*prev=pos._node->prev;
6     Node*next=pos._node->next;
7
8     prev->_next=next;
9     next->_prev=prev;
10    
11    delete pos._node;
12    --size;
13    
14    return next;
15}

实现完erase后,和insert一样,pop_back(尾删)也可以赋用erase

代码语言:javascript

代码运行次数:0

运行

AI代码解释

1void pop_back()
2{   
3//因为end指向的是最后一个有效节点的下一个位置,需要让end--走到最后一个有效位置
4     erase(--end());
5}

同理pop_front(头删)也是如此

代码语言:javascript

代码运行次数:0

运行

AI代码解释

1void pop_front()
2{
3//begin指的就是第一个有效节点,无需- -
4   erase(--begin());
5}

总结

学完了list的底层实现后我们必须要知道const迭代器和普通迭代器如何实现遍历链表及按需实例化和链表核心接口的实现,最后感谢各位大佬的支持,你们的支持就是我前进的动力


【C++list】底层结构、迭代器核心原理与常用接口实现全解析》 是转载文章,点击查看原文


相关推荐


从复杂到高效:QtitanNavigation助力金融系统界面优化升级
Aevget2025/10/24

QtitanNavigation 组件模拟Microsoft Dynamics CRM-2016 / Office 365导航界面和一组控件,来改善Qt.C ++应用程序的用户体验。QtitanNavigation结合用户界面构建“Ribbon UI”和“Side Bar”的各种示例,可以更好地在您的应用程序中导航,使用户更直观地访问应用程序的某些部分。因此,它允许同时显示更多的信息,并让您高效地查看所有实体(工作区域,网格或其他项目),滚动次数更少,点击次数更少。与我们的其他解决方案一样,Qt


Rust 与 Go – 比较以及每个如何满足您的需求
std78792025/10/22

Rust 和 Go 是新的编程语言。每个都解决了以前编程语言(例如 C 和 C++)固有的问题。 如果您不确定哪一个适合您的项目,请查看这篇比较文章,我们将在其中更深入地研究 Rust 与 Go。 在比较结束时,您将清楚地了解 Rust 和 Go 提供的功能。我们将介绍它们的主要特点、优缺点、异同,并根据您的要求讨论正确的选择。 除此之外,我们还将争辩说,大多数团队可能能够同时使用这两种语言来支持他们的应用程序,并且比只坚持使用一种编程语言获得好处。 那么,为什么还要等呢?让我们


node.js上传图片接口
郏国上2025/10/21

node.js需要使用koa-multer库来实现上传图片接口。 实际上先通过koa-multer下载到本地指定目录,然后上传到阿里云(部分格式图片需要转换成网络格式图片jgp再上传)。 首先在系统启动文件引入注册路由: app.use(BodyParser({ 'formLimit':'3mb', 'jsonLimit':'3mb', 'textLimit':'3mb' })); // 注意顺序,必须body parser在前, router在后 app.use(rou


SpringBoot的学习
ʚ希希ɞ ྀ2025/10/20

学习目标: 1.掌握基于SpringBoot框架的程序开发步骤 2.熟练使用SpringBoot配置信息修改服务器配置 3.基于SpringBoot的完成SSM整合项目开发 下图创建一个Spring模块: 下图是一个SpringBoot程序最基本的架子: 如下一个SpringBoot程序就开发好了。 SpringBoot程序之所以好用是因为pom文件中的继承和一个dependency: 最后运行Application类: Spring程序和S


小杰深度学习(sixteen)——视觉-经典神经网络——MobileNetV2
jie*2025/10/19

7.MobileNetV2 1. 网络的背景 MobileNetV1 还不够轻量和高性能,为了让移动设备有更好的体验,Google 团队提出了 MobileNetV2 架构 MobileNetV2网络是由谷歌团队在2018年提出的,它相对于MobileNetV1而言,有着更高的准确率和更小的网络模型。 论文地址:https://arxiv.org/abs/1801.04381 Inverted Residuals and Linear Bottlenecks.pdf 2. 网络的


C#:函数默认参数
曹牧2025/10/17

C#函数默认参数允许在方法定义时为参数指定默认值,当调用时未提供该参数值则自动使用默认值:    ‌1、基本语法‌     在方法声明中通过参数名=默认值形式定义,例如void Print(string msg="default")。调用时可省略有默认值的参数Print(),此时msg取值为"default"。    ‌2、使用规则‌         默认参数必须从右向左连续定义,即某个参数有默认值后,其右侧所有参数必须都有默认值        默认值必须是编译时常量,不支持运行时动态赋值


【项目实战 Day12】springboot + vue 苍穹外卖系统(Apache POI + 工作台模块 + Excel表格导出 完结)
Roye_ack2025/10/16

目录 一、工作台模块 1、查询今日运营数据 - GET接口 (1)需求分析 (2)代码开发 2、查询今日运营数据 - GET接口 (1)需求分析 (2)代码开发 3、查询菜品总览 - GET接口 (1)需求分析 (2)代码开发 4、查询套餐总览 - GET接口 (1)需求分析 (2)代码开发 二、Excel表格导出 1、Apache POI (1)入门案例 2、导出Excel表格模块 (1)需求分析 (2)代码开发 【1】导入excel模板文件 【2】c


【HarmonyOS Bug踩坑】主窗口调用的接口,UI在子窗口异常显示
GeorgeGcs2025/10/15

【HarmonyOS Bug踩坑】主窗口调用的UI表现在子窗口异常显示 一、问题现象: 这个问题的标题略显抽象,毕竟涉及到的异常表现形式太多,标题是临时拟定的。 说白了,这个问题是鸿蒙里经典的上下文指定问题。 异常的业务场景是,在主窗口之上,添加一个子窗口。当在主窗口里调用某些UI表现,例如:气泡,弹窗,模态窗口,自定义安全键盘,自定义loading等,你会发现,有时候都异常加载到子窗口中了,并没有在主窗口显示。如下图所示: import { window } from '@kit.ArkUI


k8s-pod的启动
Code Rhythm2025/10/13

k8s-pod的启动 一、命令行启动nginx的pod创建deployment访问节点中的nginx查看部署控制器和副本控制器模拟高可用,将k8s-3关机手动触发重建删除rs会重新启新的rs删除deploy,所管理的rs也会被删除 二、yaml文件启podkubectl apply 启动podkubectl apply 使用部署控制器启动pod 三、pod的启动流程四、pod的终止过程 官方文档:https://kubernetes.io/zh-cn/docs/concept


敏捷开发流程-精简版
暖阳_2025/10/12

敏捷开发流程 - 精简版(实战版) 版本: v1.0 更新日期: 2025-10-11 适用场景: 中小型团队快速迭代开发 🎯 流程全景图 📋 5大阶段详解 阶段1:需求收集 → 业务需求文档 负责人: 项目经理 输入: 用户原始需求(口头/邮件/会议记录) 核心工作: 收集用户需求 整理为层级结构(一级-二级-三级) 组织需求评审会①(内部简单评审) 产出物: ✅ 业务需求文档(层级结构) 评审点:需求评审会① 参与人:项目经理、产品经理 目的:快速确认需求方向和范围 输

首页编辑器站点地图

Copyright © 2025 聚合阅读

License: CC BY-SA 4.0