【C++/STL】:list容器的深度剖析及模拟实现

【C++/STL】:list容器的深度剖析及模拟实现

目录

🚀前言

点击跳转到文章:【list的基本使用】
要模拟实现list,必须要熟悉list的底层结构以及其接口的含义,list的底层是带头双向循环链表,通过上一篇文章的学习,这些内容已基本掌握,现在我们来模拟实现list容器的主要接口。

与前面的vector类似,由于使用了模板,也只分成.cpp和.h两个文件。

.cpp文件里放节点类,迭代器类,list类及其成员函数,测试函数的实现,在.h文件里进行测试

本文的重点是:对三个类的区分与理解,迭代器类的实现

🚀一,节点类

1.为什么定义节点结构体时使用struct而不是class?

答:(1)其实用class也可以,但是class与struct默认的访问限定不同,当没有声明公有,私有时,struct内容默认是公有,class内容默认的私有,所以用class要加上public

(2)当我们用class没有加上public,也没有实例化对象时,编译不会报错(报私有成员的错误),因为模版是不会被细节编译的。只有当我们实例化出对象,模版才会被编译,并且类的实例化并不是对所有成员函数都实例化,而是调用哪个成员函数就实例化哪个。这叫做按需实例化。

2.可用匿名对象初始化。如果T是自定义类型,则调用其默认构造,并且T是内置类型也升级成了有默认构造的概念了。

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

🚀二,迭代器类

前面学习的string类和vector的迭代器用的是原生指针类型,即T*。但是在list容器中是不能这样的,因为前面两者的底层物理空间是连续的,符合迭代器++与- -的行为。但是list是由一个一个节点构成的,物理空间不连续,Node*的++和- -不符合迭代器的行为,无法变遍历

所以用一个类把Node* 封装,就可以重载运算符,使得用起来像内置类型,但会转换成函数调用,继而控制Node*的行为。

1,普通迭代器类的实现

遍历需要的核心运算符重载是 *,!=,++ 和 ->。所以只需要利用带头双向循环链表的特性,对Node * 进行封装,从而控制Node * 的行为。

class ListIterator {typedef ListNode<T> Node;typedef ListIterator<T> Self;//名字变得简短 public: Node* _node;//定义一个节点指针ListIterator(Node* node):_node(node){}//前置:返回之后的值//++it;//返回与自己一样的类型 Self& operator++(){ _node = _node->_next;return*this;} Self& operator--(){ _node = _node->_prev;return*this;}//后置:返回之前的值 Self operator++(int){ Self tmp(*this); _node = _node->_next;return tmp;} Self operator--(int){ Self tmp(*this); _node = _node->_prev;return tmp;} T& operator*(){return _node->_data;}//返回的是数据的地址 T* operator->(){return&_node->_data;} bool operator!=(const Self& it){return _node != it._node;} bool operator==(const Self& it){return _node == it._node;}};

2,->运算符的使用场景

假设某个场景下存在一个坐标类:

structPos{int _row;int _col;Pos(int row =0,int col =0):_row(row),_col(col){}};

如果我们插入坐标,并且想要打印出坐标,该如何遍历?

错误示范

voidtest_list2(){ list<Pos> lt1; lt1.push_back(Pos(100,100));//使用匿名对象 lt1.push_back(Pos(200,200)); lt1.push_back(Pos(300,300));//这里的it是Pos*,是结构体指针 list<Pos>::iterator it = lt1.begin();while(it != lt1.end()){ cout <<*it <<" ";//err++it;} cout << endl;}

原因:因为这里的*it返回的是Pos自定义类型,而访问自定义类型需要需要在类中自己重载流插入(<<),这里并没有重载,所以报错

正确操遍历的两种方式

方式1:通过.操作符直接访问结构体的成员变量(一般不这样访问数据)。

cout <<(*it)._row <<":"<<(*it)._col << endl;//ok

方式2:通过重载->运算符,对结构体指针进行解引用。

cout << it.operator->()->_row <<":"<< it.operator->()->_col << endl;//ok

注意:其实这里严格来说是有两个箭头,第一个运算符重载的调用 it.operator->() 返回的是 Pos*,第二个箭头才是原生指针,Pos*再用箭头访问。为了可读性,省略了一个->。

voidtest_list2(){ list<Pos> lt1; lt1.push_back(Pos(100,100));//使用匿名对象 lt1.push_back(Pos(200,200)); lt1.push_back(Pos(300,300));//这里的it是Pos*,是结构体指针 list<Pos>::iterator it = lt1.begin();while(it != lt1.end()){//方式1://cout << (*it)._row << ":" << (*it)._col << endl;//ok//*it就是Pos结构体,再用.操作符访问成员//方式2: cout << it->_row <<":"<< it->_col << endl;//ok//cout << it.operator->()->_row << ":" << it.operator->()->_col << endl;//ok++it;} cout << endl;}

3,const迭代器类的实现

在我们遍历数据时,有时会写一个打印函数,引用传参,一般建议加const,这就出现了一个const链表

voidFunc(const list<int>& lt1){ list<int>::const_iterator it = lt1.begin();while(it != lt1.end()){ cout <<*it <<" ";++it;} cout << endl;}

const迭代器不是在普通迭代器前面加const,即不是const iterator

//err 这样使it本身也不能++了const list<int>::iterator it = it.begin();

const 迭代器目的:本身可以修改,指向的内容不能修改,类似const T* p。

所以我们要再定义一个类,控制*和->的返回值就可以了。

template <class T> class ListConstIterator {typedef ListNode<T> Node;typedef ListConstIterator<T> Self;//名字变得简短 public: Node* _node;//定义一个节点指针ListConstIterator(Node* node):_node(node){}//前置:返回之后的值//++it;//返回与自己一样的类型 Self& operator++(){ _node = _node->_next;return*this;} Self& operator--(){ _node = _node->_prev;return*this;}//后置:返回之前的值 Self operator++(int){ Self tmp(*this); _node = _node->_next;return tmp;} Self operator--(int){ Self tmp(*this); _node = _node->_prev;return tmp;}// 所以我们要再定义一个类,使用const控制*和->的返回值就可以const T& operator*(){return _node->_data;}const T* operator->(){return&_node->_data;} bool operator!=(const Self& it){return _node != it._node;} bool operator==(const Self& it){return _node == it._node;}};

4,通过模板参数,把两个类型的迭代器类结合

可以发现,其实普通迭代器和const迭代器的本质区别是 * 和 ->,这两个运算符的返回类型的变化。两个类冗余,所以可以通过模板,给不同的模板参数,让编译器自己实例化两个类。

template <class T,class Ref,class Ptr>structListIterator{typedef ListNode<T> Node;typedef ListIterator<T, Ref, Ptr> Self;//名字变得简短 Node* _node;//定义一个节点指针ListIterator(Node* node):_node(node){}//前置:返回之后的值//++it;//返回与自己一样的类型 Self& operator++(){ _node = _node->_next;return*this;} Self& operator--(){ _node = _node->_prev;return*this;}//后置:返回之前的值 Self operator++(int){ Self tmp(*this); _node = _node->_next;return tmp;} Self operator--(int){ Self tmp(*this); _node = _node->_prev;return tmp;} Ref operator*(){return _node->_data;} Ptr operator->(){return&_node->_data;} bool operator!=(const Self& it){return _node != it._node;} bool operator==(const Self& it){return _node == it._node;}};

5,迭代器类的一些问题的思考

(1) 类中是否需要写析构函数

这个迭代器类不要写析构函数,因为这里的节点不是迭代器的,是链表的,不用把它释放。我们使用begin,end返回节点给迭代器,是借助迭代器修改,访问数据,所以我们不需要释放

(2) 类中是否需要写拷贝构造进行深拷贝和写赋值拷贝

这里也不需要写拷贝构造进行深拷贝,因为这里要的就是浅拷贝。begin返回了第一个节点的迭代器给it,这里就是用默认生成的拷贝构造,浅拷贝给it,那这两个迭代器就指向同一个节点,所以这里用默认的拷贝构造和赋值拷贝就可以了

🚀三,list 类

1,list类的结构

template <class T> class list {typedef ListNode<T> Node; public://物理空间不是连续的,不符合迭代器的行为,无法遍历//typedef Node* iterator;//规范命名//typedef ListIterator<T> iterator;//typedef ListConstIterator<T> const_iterator;typedef ListIterator<T, T&, T*> iterator;typedef ListIterator<T,const T&,const T*> const_iterator;//……………… private: Node* _head;};

2,迭代器的实现

包含普通迭代器和const迭代器

iterator begin(){//iterator it(_head->_next);//return it;returniterator(_head->_next);//使用匿名对象} iterator end(){returniterator(_head);} const_iterator begin()const{returnconst_iterator(_head->_next);} const_iterator end()const{returnconst_iterator(_head);}

3,插入数据insert

iterator insert(iterator pos,const T& x){ Node* cur = pos._node;//找到当前节点 Node* newnode = new Node(x);//申请节点 Node* prev = cur->_prev;//找到前一个节点//prev newnode cur 进行链接 newnode->_next = cur; cur->_prev = newnode; prev->_next = newnode; newnode->_prev = prev;returniterator(newnode);}

注意:链表的insert没有迭代器失效问题,因为没有扩容的概念,pos位置的节点不会改变。但是STL库里insert也给了返回值,返回的是新插入位置的迭代器

4,删除数据erase

iterator erase(iterator pos){assert(pos !=end());//防止删除头节点 Node* cur = pos._node;//找到当前节点 Node* prev = cur->_prev;//找到前一个节点 Node* next = cur->_next;//找到后一个节点 prev->_next = next; next->_prev = prev; delete cur;returniterator(next);}

注意:链表的erase后有迭代器失效问题,pos失效了,因为pos指向的节点被释放了。所以也要返回值,返回的是删除节点的下一个节点的迭代器

5,头插,头删,尾插,尾删

可以复用前面的 insert和 erase

//尾插:end()的下一个位置voidpush_back(const T& x){//Node* newnode = new Node(x);//申请节点并且初始化//Node* tail = _head->_prev;////链接节点//tail->_next = newnode;//newnode->_prev = tail;//_head->_prev = newnode;//newnode->_next = _head;insert(end(), x);}//尾删voidpop_back(){erase(--end());//注意:前置--}//头插:在begin前面插入voidpush_front(const T& x){insert(begin(), x);}//头删voidpop_front(){erase(begin());}

6,常见构造函数的实现

主要包含:构造函数,拷贝构造,initializer_list构造(列表构造)

注意:由于这些都是在有哨兵位节点的前提下实现的,所以可以把申请哨兵位头节点这一步骤提取出来。

//空初始化,申请哨兵位头节点voidempty_init(){ _head = new Node(); _head->_next = _head; _head->_prev = _head;}list(){empty_init();}//拷贝构造:直接复用尾插,前提要有哨兵位头节点//lt2(lt1)list(const list<T>& lt){empty_init();//注意:使用范围for时加上const和&for(constauto& e : lt){push_back(e);}}//initializer_list构造,前提要有哨兵位头节点list(initializer_list<T> il){empty_init();for(constauto& e : il){push_back(e);}}

7,析构函数

析构函数的作用是:删除整个链表结构,包括哨兵位节点

//清空当前数据 留头节点,其余节点释放voidclear(){auto it =begin();while(it !=end()){//返回删除节点的下一个节点的迭代器 it =erase(it);}}//析构:销毁整个链表~list(){clear(); delete _head; _head = nullptr;}

Read more

自动化打造信息影响力:用 Web Unlocker 和 n8n 打造你的自动化资讯系统

自动化打造信息影响力:用 Web Unlocker 和 n8n 打造你的自动化资讯系统

一、研究背景 在信息爆炸的时代,及时获取高质量行业资讯成为内容创作者、运营者以及研究者的刚需。无论是IT、AI领域的技术动态,还是招聘、人才市场的趋势新闻,第一时间掌握热点、总结观点并进行内容输出,正逐渐成为提升影响力与构建个人/组织品牌的关键手段。 为实现“日更内容”目标,很多人开始探索自动化的路径——使用爬虫工具定期抓取目标网站内容,借助 AI 模型自动生成摘要,再将结果推送至社群平台。这一流程的核心,是稳定、高效地获取网页数据,在实际操作中,却出现了很多问题: * 首先是出现了验证码,阻断自动化流程; * 紧接着是请求返回403 Forbidden,提示IP被封; * 最终是目标网站直接对我们常用IP段进行了临时封禁,哪怕切换机器或重启网络都无济于事。 按照检查方法,当处于非爬虫操作时,我们在F12控制台输入window.navigator.webdriver时,显示的是false,输入进去出现了刺眼的红色报错,而且显示也出现了True, “Failed to load resource: the server responded with

山东大学《Web数据管理》期末复习宝典【万字解析!】

山东大学《Web数据管理》期末复习宝典【万字解析!】

🌈 个人主页:十二月的猫-ZEEKLOG博客 🔥 系列专栏:🏀山东大学期末速通专用_十二月的猫的博客-ZEEKLOG博客 💪🏻 十二月的寒冬阻挡不了春天的脚步,十二点的黑夜遮蔽不住黎明的曙光  目录 1. 第二章 网络爬虫 1.1 爬虫基础知识 1.2 爬虫分类 1.3 开源工具 Nutch 2. 第三章 网页分析 2.1 正则表达式 2.2 DOM模型 2.3 Beautiful Soup工具 2.4 Scrapy框架 2.5 不同爬虫工具比较 2.6 元搜索引擎 3. 第四章 爬虫与网站的博弈 3.1 Robot协议 3.

Qt 前后端通信(QWebChannel Js / C++ 互操作):原理、示例、步骤解说

Qt 前后端通信(QWebChannel Js / C++ 互操作):原理、示例、步骤解说

Qt 提供的 QWebEngineView 是一个基于 Chromium 内核的浏览器组件,通过它,开发者可以使用 HTML、CSS、JavaScript 等技术开发 Web 页面并呈现在 Qt 桌面应用中,但与开发纯 Web 页面不同的是,这些页面通常需要和 应用中的其他组件交互,例如获取后端数据进行渲染、将前端用户指令传达给后端执行等,这将不可避免地涉及到前端 Js 和 后端 C++ 之间的交互问题,而 Qt 为此给出的解决方案就是 QWebChannel,通过 QWebChannel 前端 Web 页面和与后端 C++ 程序实现自然而顺畅的交互,甚至前后端的操作风格都极为一致。本文我们将细致地介绍QWebChannel 前后端交互的原理,通过四个详实的示例程序讲解每一步重要的操作步骤,通过本文,你将对 QWebChannel 有一个全面而深入的了解。 1. 工作原理

前端状态管理:别让你的状态变成一团乱麻

前端状态管理:别让你的状态变成一团乱麻 毒舌时刻 这状态管理得跟蜘蛛网似的,谁能理得清? 各位前端同行,咱们今天聊聊前端状态管理。别告诉我你还在使用 setState 管理所有状态,那感觉就像在没有地图的情况下寻宝——能找,但累死你。 为什么你需要状态管理 最近看到一个项目,组件之间传递状态需要经过 5 层,修改一个状态要修改多个地方。我就想问:你是在做状态管理还是在做传递游戏? 反面教材 // 反面教材:混乱的状态管理 function App() { const [user, setUser] = useState(null); const [posts, setPosts] = useState([]); const [comments, setComments] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { async function fetchData() { setLoading(