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

news/2025/9/28 16:12:17/文章来源:https://www.cnblogs.com/ljbguanli/p/19116996

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

封面

个人主页:爱和冰阔乐
专栏传送门:《数据结构与算法》 、C++
学习方向:C++方向学习爱好者
⭐人生格言:得知坦然 ,失之淡然

在这里插入图片描述


博主简介
在这里插入图片描述

文章目录

  • 前言
  • 一、官方源码的探究
  • 二、list底层的构建及其尾插
    • 2.1 list底层探索
    • 2.2 push_back
  • 三、实现普通迭代器的遍历
    • 3.1运算符*/++/--的重载
    • 3.2 自定义类型下的运算符重载
    • 3.3 迭代器遍历
  • 四、实现const迭代器的遍历
    • 4.1 print_container的迭代器问题
    • 4.2 按需实例化
    • 4.3 模板解决迭代器代码冗余
  • 五、迭代器失效
  • 六、常见接口的实现及其源码
    • 5.1 insert 插入
    • 5.2 push_back /push_front
    • 5.3 erase/pop_back/pop_front
    • 5.4 核心接口源码
  • 总结


前言

在前一篇文章我们介绍了list的各种接口的实现,今天我们将通过模拟实现list底层的各个接口,让大家对list的使用与逻辑更加清楚,通过迭代器的各类问题的拆解和运算符重载让我们对STL各个容器的不同特性与共同点更加清晰

一、官方源码的探究

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

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

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

在这里插入图片描述

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

2.1 list底层探索

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

namespace hxx
{
template<class T>class list_node{T _data;list_node* _prev;list_node* _next;list_node(const T& data):_data(data),_next(nuppptr),_prev(nullptr){}}:}

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

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

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

template <class T>class list{typedef list_node<T> Node;public:list(){_head=new Node;_head->next=_head;_head->prev=_head;_size=0;}private:Node*_head;size_t size;}

2.2 push_back

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

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

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

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

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

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

void test()
{
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;}

3.1运算符*/++/–的重载

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

//由于链表的类型不确定,因此需要使用模板
template <class T>//这里使用struct是因为其默认是公有,可以直接给list提供公共接口struct list_iterator{typedef list_node<T>  Node;//重载++就是迭代器++,也就是迭代器的类型,这里可以typedef下typedef list_iterator<T> Self;Node* _node;//构造函数list_iterator(Node* node):_node(node){}T&  operator*(){return _node->_data;}Self& operator++(){_node=node->_next;return *this;}//两个迭代器的比较bool operator!=(const Self& s){return _node!=s._node;}};

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

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

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

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

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

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

struct AA
{
int a1=1;
int a2=1;
};
void test1()
{
list<AA> lt;lt.push_back(AA());lt.push_back(AA());lt.push_back(AA());lt.push_back(AA());list<AA>::iteerator it=lt.begin();while(it!=lt.end()){cout<<*it<<" ";++it;}cout<<endl;}

结果演示:
在这里插入图片描述
很显然我们运行错误,因为lt解引用后是自定义类型,<<不支持自定义类型的使用,因此在这里有三种方法

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

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

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

3.3 迭代器遍历

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

typedef list_iterator<T> iterator;iterator begin(){//  return iterator (_head->_next);//当然也可以如下写法,因为单参数构造函数支持隐式类型转换return _head->_next;}iterator end(){//隐式类型转换,这里需要注意end返回的尾节点的后一个节点,尾节点是_head->_prev,因此后一个节点是_headiterator _head;}

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

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

在这里插入图片描述

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

template<class T>struct list_node{T _data;list_node<T>* _next;list_node<T>* _prev;list_node(const T& data = T()):_data(data), _next(nullptr), _prev(nullptr){}};

结果演示
在这里插入图片描述

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

四、实现const迭代器的遍历

4.1 print_container的迭代器问题

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

template<class Container>void print_container(const Container& v){for(auto e:v){cout<<e<<" ";}cout<<endl;}void test1(){list<AA> lt;lt.push_back(A());lt.push_back(A());lt.push_back(A());lt.push_back(A());for(auto e:lt){cout<<e<<" ";}cout<<endl;print_container(lt);}

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

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

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

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

4.2 按需实例化

// 按需实例化
template<class Container>void print_container(const Container& con){// const iterator -> 迭代器本身不能修改// const_iterator -> 指向内容不能修改list<int>::const_iterator it=con,begin();while (it != con.end()){*it += 10;cout << *it << " ";++it;}cout << endl;for (auto e : con){cout << e << " ";}cout << endl;}void test_list1(){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()){*it += 10;cout << *it << " ";++it;}cout << endl;for (auto e : lt){cout << e << " ";}cout << endl;//print_container(lt);}

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

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

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

在这里插入图片描述
代码实现:

template<class T,class Ref,class Ptr>struct list_iterator{typedef list_node<T,Ref,Ptr> Node;typedef list_iterator<T,Ref,Ptr>Self;Node*_node;list_iterator(Node* node):_node(node);{}Ref operator*(){return _node->data;}Ptr opertor->(){//箭头返回对象类型的指针,指针解引用找到其存储的数据return &_node->_data;}}

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

五、迭代器失效

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

void test_list()
{
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();lt.insert(it,10);*it+=100;print_container(lt);}

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

auto it=lt.begin();
while(it!=lt.end())
{
//删除所有的偶数
if(*it%2==0)
{
lt.erase(it);
}
else
{
++it;
}
}

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

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

5.1 insert 插入

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

iterator insert(iterator pos, const T& x)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(x);
// prev newnode cur
newnode->_next = cur;
cur->_prev = newnode;
newnode->_prev = prev;
prev->_next = newnode;
++_size;
return newnode;
}

5.2 push_back /push_front

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

void push_front(const T& x)
{
insert(begin(),x);
}
//在哨兵位头结点之前插入数据相当于尾插
void push_back()
{
insert(end(),x);
}

5.3 erase/pop_back/pop_front

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

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

iterator erase(pos)
{
assert(pos!=end());
Node*prev=pos._node->prev;
Node*next=pos._node->next;
prev->_next=next;
next->_prev=prev;
delete pos._node;
--size;
return next;
}

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

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

同理pop_front(头删)也是如此

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

5.4 核心接口源码

list核心接口源码实现:https://gitee.com/zero-point-civic/c-initial-stage/tree/master/list/list


总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/920801.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

完整教程:Flink Watermark机制解析

完整教程:Flink Watermark机制解析pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco…

2025 年北京湖南菜餐厅推荐:小湖南岸以湖湘本味与匠心服务,成京城湘菜口碑之选

2025 年的北京餐饮市场呈现 “冰火两重天” 的格局:一边是同质化竞争加剧、成本高企导致部分品牌陷入经营困境,另一边是坚守品质与特色的餐饮品牌凭借精准定位实现逆势增长。作为全国美食汇聚地,北京的湘菜赛道尤为…

完整教程:龙芯在启动参数里添加串口信息

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

ABC425题解

A. Sigma Cubescode #include<bits/stdc++.h> using namespace std; int n; int main(){cin >> n;int ans = 0;for(int i = 1; i <= n; ++i){ans += ((i&1)?-1:1) * (i * i * i); }cout << …

技术分享 | Dify智能体案例分享:智能导诊助手 - 详解

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

STM32中的Flash、ROM与RAM全解析 - 指南

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

Functions

FunctionsVector std:ranges::sort() -std=c++20 是一个C++20引入的排序函数,收录于#include<algorithm> ()里的内容通常是 vector 和 array(满足随机访问迭代器要求),也可以是其它符合范围 概念 的序列。 使…

科技网站推荐台州找人做网站

1、FACTORY—追MM少不了请吃饭了&#xff0c;麦当劳的鸡翅和肯德基的鸡翅都是MM爱吃的东西&#xff0c;虽然口味有所不同&#xff0c;但不管你带MM去麦当劳或肯德基&#xff0c;只管向服务员说“来四个鸡翅”就行了。麦当劳和肯德基就是生产鸡翅的Factory (下载源码就到源码网:…

QOJ #5421. Factories Once More 题解

Description 有一个王国,共有 \(n\) 座城市,这些城市编号为 \(1\) 到 \(n\)(包含两端)。 王国中有 \(n-1\) 条双向道路将这些城市相连,并且保证任意两座城市之间都可以通过这些道路到达。 女王最近决定新建 \(k\)…

IDEA JAVA项目gitignore文件模板

target/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/**/target/ !**/src/test/**/target/### STS ### .apt_generated .classpath .factorypath .project .settings .springBeans .sts4-cache### IntelliJ IDEA ##…

自动生成验证码

include int main() { srand(static_cast(time(NULL))); string s = "0123456789zxcvbnmasdfghjklqwertyuiopZXCVBNMASDFGHJKLQWERTYUIOP"; cout << "请输入验证码长度:"; int length; cin…

商城网站带宽控制美肤宝网站建设

运行环境 开发语言&#xff1a;PHP 数据库:MYSQL数据库 应用服务:apache服务器 使用框架:ThinkPHPvue 开发工具:VScode/Dreamweaver/PhpStorm等均可 项目简介 随着社会的发展&#xff0c;社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发…

365 赚钱宝小程序系统:多元化变现与趣味运营一体的小程序解决方案

在数字化营销与流量变现需求增长的当下,365 赚钱宝作为微信小程序运营工具,凭借 “趣味养成 + 广告盈利 + 裂变引流” 模式,为个人与企业提供低门槛、高潜力的流量变现路径,降低技术与维护成本。 一、概述总结 365…

9.22 总结

T1 这题就是一个二分答案,因为 x 特别小所以可以直接跑背包。然后可以 \(O(1)\) check,所以复杂度是一个 \(\log\)。 T2 这题比较难,当时只写了部分分。 T3 这题也只写了部分分。 T4 就是这题的复杂度是 \(O(n^2)\)…

保亭县住房城市建设局网站域名可以绑定几个网站

Q:给你一个有序数组nums &#xff0c;请你原地删除重复出现的元素&#xff0c;使得出现次数超过两次的元素只出现两次 &#xff0c;返回删除后数组的新长度。不要使用额外的数组空间&#xff0c;你必须在原地修改输入数组并在使用O(1)额外空间的条件下完成。 第一种解决方法&a…

iOS 26 系统流畅度深度评测 Liquid Glass 动画滑动卡顿、响应延迟、机型差异与 uni-app 优化策略 - 教程

iOS 26 系统流畅度深度评测 Liquid Glass 动画滑动卡顿、响应延迟、机型差异与 uni-app 优化策略 - 教程pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: b…

邯郸做网站熊掌号WordPress获取标签名字

文章目录 前言一、什么是UART&#xff1f;二、K210的UART三、实验过程总结 前言 串口通讯是平时大家进行调试最常用的方法&#xff0c;嵌入式应用通常要求一个简单的并且占用系统资源少的方法来传输数据。通用异步收发传输器 (UART)即可以满足这些要求&#xff0c;它能够灵活地…

即刻搜索收录网站河南省两学一做网站

8种机械键盘轴体对比本人程序员&#xff0c;要买一个写代码的键盘&#xff0c;请问红轴和茶轴怎么选&#xff1f;chmod命令用来变更文件或目录的权限。在UNIX系统家族里&#xff0c;文件或目录权限的控制分别以读取、写入、执行3种一般权限来区分&#xff0c;另有3种特殊权限可…