【C++】STL——list深度剖析 及 模拟实现

简介: 【C++】STL——list深度剖析 及 模拟实现

前言

这篇文章我们来继续STL的学习,今天我们要学习的是list,也是STL中容器的一员。

和之前一样,我们还是先学习它的使用,然后再对它进行一个深度剖析和模拟实现。

9033db7c135c40dfa31febac9b85ac27.png

1. list的介绍及使用

1.1 list的介绍

list的文档介绍

acc7b9877133425db1fb7c379dcaaf63.png

list的底层实现其实就是我们之前数据结构学过的带头双向循环链表

0aa38314674f40c786bbf19a6527a643.png

b9d4bd3f8907449480892cbd6b92a362.png

092e06e022474e2c876f4a064002018f.png

1.2 list的使用

首先我们来学习一下list的使用:

那经过之前string和vector的学习,我们想要学会list的使用,成本就很低了,所以下面我们就带大家快速的过一下,我们的重点还是在于后面的模拟实现。

我们来看一下它的接口:

首先看一下构造:

7c0f3d80adcd4fb0abe6cc0c6b2393e8.png

也是我们熟悉的这几个,默认构造、n个val的构造、迭代器区间的构造以及拷贝构造。

然后看一下它的迭代器:

67466f7528fb4421b11362cb2d17b728.png

也是这几个,相信学到现在大家都很熟悉了。

修改操作:a81f3f54744342f9a9be1ce4d55d05f8.png

这里面常用的几个接口我们也都比较熟悉。

但是我们看到list这里只有resize,没有reverse了,因为它是链表嘛,就没有扩容这一说了。

418e84f6ced541f4aadf3ba2bd36541c.png

那剩余的比较重要的接口我们后面讲到再说。

但是我们会注意到:


list与string和vector最大的区别是啥?

我们会发现list没有重载[],也就是说我们要遍历和访问list,就只能用迭代器了(范围for的底层也是迭代器)。

所以说,迭代器才是通用的方式,所有的容器都可以用迭代器,而[]只是针对特定容器的特殊方式。

遍历

那经过之前的学习,相信大家就可以直接上手使用list的迭代器了:

int main()
{
  list<int> l;
  l.push_back(1);
  l.push_back(2);
  l.push_back(3);
  l.push_back(3);
  l.push_back(5);
  for (list<int>::iterator it = l.begin(); it != l.end(); ++it)
    cout << *it << " ";
  cout << endl;
  for (auto e : l)
    cout << e << " ";
  cout << endl;
  for (list<int>::reverse_iterator rit = l.rbegin(); rit != l.rend(); ++rit)
    cout << *rit << " ";
  cout << endl;
  return 0;
}

96243a5e7ba64a508a5213251f5634f1.png

插入删除数据

然后我们来演示一下insert和erase:

我们发现list也没有提供find,所以我们要获取某个位置的迭代器,可以用算法库里面的find:1f91838038984eb89fe5b85b1187b808.png

7b76dbd1cdd440f082595db076d17cd9.png

也是这几个版本,就不给大家全部演示了。

然后erase:

871e7755e2264c7b8d9870beb10754b0.png

791b330f81304c33b19a9a51f89d4ee8.png但是注意list的迭代器是双向迭代器:

8b0caf8c6cb746d6af8aaf6c1db403f6.png

只能++或者- -,不能+或-

405bee4d8e35404ba2ec12ab90ae7c97.png

Operations

然后来看下这几个接口:

0094c350612c4d4ba39f2c00d5e7e4fd.png首先这个splice ,用的不多,它可以把一个链表的一部分转移到另一个链表

e53cd5d987f44b84b63002dfce106a3c.png

大家需要的时候自己看一下文档就会用了。

然后remove就是删除指定的元素,find+erase

f116e956c7514a91b342eb4fb5a80256.png

后面还有个remove_if,这个涉及仿函数,我们先不用管。

然后unique就是去重:

7e22aceb60374610af53385d168b4706.png


merge可以合并两个有序链表:

6785ad7b3857491794973f0ac30a50b7.png

然后呢,list也提供了sort

就是可以对链表进行排序:

d388839d6df844e29dbfe5cb7063fd73.png

最后reverse就是对链表逆置,就不多说了。

那我们接下来思考一个问题:算法库里面不是已经有sort了吗,为什么链表自己还要提供一个sort?

最主要的原因是算法库里的排序list就用不了。

ac8e841b5c334fd38994ace4a1038fb2.png

我们发现报了一堆错怎么,回事呢?

4edb0c664f084dc382c74e2ba070fd58.png

我们看到算法库的sort进行了什么,是不是-啊?

但是我们上面说了,**list的迭代器是双向迭代器,是不能进行-操作的。**当然除此之外里面其它操作list也不行。

迭代器的功能分类

所以呢:

094a7072836546f8a32be0f6149d8e35.png

虽然库里的sort是一个函数模板,理论而言这里可以传任意类型的参数,但是其内部对使用的迭代器有要求,参数的名字就暗示了我们要传随机迭代器。

当然不同容器对应的迭代器是什么类型跟它的底层结构有关系。

那我们之前文章也提到过:

迭代器我们之前讲的什么正向反向,const迭代器,这些是使用属性;那还有一个特性属性,迭代器严格来说还可以细分为单向迭代器,双向的和随机的,单向的就是只能++不能- -,双向就是可以++也可以- -,那随机就是除了可以++和- -之外还可以+或- 。

那这个单向,双向,随机大家可以认为是迭代器的功能分类:

1、单向迭代器:只能++,不能- -。例如forward_list和unordered_map;

2、双向迭代器:既能++也能–。例如list;

3、随机访问迭代器:能++ 和- -,也能+和-。例如vector和string。

ce8921a0d9c646139d49a1b3313c243c.png

文档里面在Member types我们能看到当前容器的迭代器类型。

🆗,那除了sort,算法库里的:

ed18ff01ac2e4270ade126127c6b5752.png

我们看到reverse参数名字是不是也有暗示,暗示我们要传双向迭代器

那你传个单向可以吗?

是不是不行啊,因为双向既要支持++还要支持- -,而单向是不是只能++啊:

281f16895bc0457c8ee3c3722ad3f298.png但是我们传随机可以吗?

是不是可以啊,因为随机是支持++和- -的。

🆗 ,那相信现在大家就明白为什么list要自己提供一个sort 了。


44d1233b075f4b878f4e798d035de149.png但是,想告诉大家的是:


list提供了一个sort其实是有点没必要的,大多数情况下我们都不会用list的排序。

为什么,因为效率太慢了。


list 的sort性能测试

现在我这里已经有一段写好的代码用来测试vector和list排序的性能,具体实现大家可以不用关心,看一下结果就行了。

现在产生100000个随机数,分别放到vector和list中,然后list调自己提供的sort,vector调库里面的sort,我们来对比一下它们的运行时间:

76104ed0893f4e6d8e8c7281a89984a5.png

多运行几次:

12708f91a62c4820be81443f13efb273.png

31220bea6f5745f485e0129ee024222d.png

我们看到vector是更快一些在它们排一组相同的数据下。

那我们加到1000000个数据再来看:

a7b8627b1cfd4bcab4e4c26fafd1d959.png

然后我们再换一种玩法:

我们现在定义两个list,还是上面那些数据,一个list我直接排,另一个怎么做呢?

先把它拷贝到一个vector里面去排序,然后排完再拷贝回list,我们来看一下:

5203acf4c24843c2b5c83e0e16e4e091.png

🆗,我们看到还是比直接在list里排快很多。

所以说:

虽然list提供了sort,但是我们大多数情况下是不会选择用它的。

🆗:

那list的使用我们差不多就讲到这里,大家用到哪些接口如果没有讲到可以自行查阅文档。

2. list的模拟实现

那接下来我们就来对list进行一个深度剖析和模拟实现,那首先我们还是先来简单的浏览一下STL中list的源码:

2.1 STL_list源码浏览

首先我们可以看到:

它里面有三个模板类9b8d999d85844f2f868af8175acfaede.png

第一个类是结点;

2f1b55b9ac8441dcaff16a313d81ebf2.png

第二个是迭代器;

e184137a26f34e35806517e4bec3b0ac.png

最后一个就是链表对应的类模板。

但是我们发现结点和迭代器的类他都是用struct定义的,那用struct就说明他想把类里面的所有成员都对外开放出去,因为struct的默认访问权限为public。

那我们来看一下list类的成员变量:

60c4312a8f354719b8adddc1f1be66e5.png

🆗,我们会发现它只有一个成员变量link_type node,那它的类型link_type 是啥呢?

我们找一下:5aedc0d284634df2aa2706bd9130f3f6.png

我们看到link_type其实就是结点的指针。


然后我们在看什么呢?


🆗,是不是可以看一下它的构造啊,看明白构造,我们就可以知道它的一个初始状态是怎么样的,然后我们就可以再去看一下它的一些核心的接口,当然对于链表来说无非也就是头插头删、尾插尾删这些,那这样我们对它就差不多了解一个七七八八了。

那构造函数:

19f67ad151394b88a018ec4802f1a420.png

我们看到它里面又调了另一个函数empty_initialize,就是空初始化的意思嘛。

empty_initialize干了什么呢?

c81ee386511041c79a238a4a19aacfc8.png

我们看到就是创建了一个结点,然后让他的next和prev都指向自己,什么意思呢?

那如果大家看过我之前数据结构的文章,学过里面的带头双向循环链表的话,一看就明白了。

其实就是创建了一个哨兵位的头结点嘛。

然后再来看:

96b923ca4ad44007b9a741deea89e64f.png

头插push_front和尾插push_back,那头插就是在begin的位置插入一个元素,尾插就是在end的位置插入一个元素。

那对于list来说,begin和end应该在哪呢?

1680201ef1574bbb81f35bc523c8dee0.png

🆗,那剩下的接口我们就可以不用看了,本身我们之前也学过,已经比较了解它的结构了,后面有需要的地方我们再来看。

那接下来我们就可以开始模拟实现了。

2.2 基本结构实现

那首先我们写一下结点的结构:

namespace yin
{
  template <class T>
  struct list_node
  {
    list_node<T>* _next;
    list_node<T>* _prev;
    T _data;
    //构造
    list_node(const T& x)
      :_next(nullptr)
      , _prev(nullptr)
      , _data(x)
    {}
  };
}

这里还是放到我们自己的命名空间里。

然后写一下list的结构,迭代器我们先放到后面在搞:

ad990402f8e74580ae64fd5d8d5f3e55.png

然后先来给一个默认构造:

24d9c5b7e14c43e1b56f0d35067e6948.png

然后我们先写一个push_back:

6774ce27a86e4b55923cdb44dee01c64.png

尾插要怎么写,想一下?

很简单,首先拿要插入的数据创建一个新结点,找尾然后改变指针指向链接就行了嘛


ef79f45da7c94f498ee4936833456bc4.png

void push_back(const T& x)
{
  node* newnode = new node(x);
  //找尾
  node* tail = _head->_prev;
  //链接
  _head->_prev = newnode;
  newnode->_next = _head;
  tail->_next = newnode;
  newnode->_prev = tail;
}

测试一下:

518cd4108038452985491a83a7c7f5a5.png

🆗,我们的链表就成形了。

2.3 思考:list迭代器是否可以用原生指针

那接下来我们来搞一下list的迭代器:

大家回想一下,我们之前模拟实现string和vector的时候,它们的迭代器我们是不是都使用了原生指针去实现啊,因为使用指针完全是可行的嘛。

那现在list的迭代器我们还可以使用指针来搞吗?

🆗,string和vector的迭代器之所以能使用原生指针去实现,最主要的原因是不是因为它们底层的物理空间是连续的啊,那连续的话,用指针是不是很方便啊,++就往后走正好访问到下一个元素。

那list呢?list的迭代器用原生指针实现可行吗?或者说用原生指针实现有没有什么问题呢?

🆗,list里面是一个一个的结点,如果我们用结点的指针node*的话,首先它解引用是啥?

是不是对应的结点啊,但是我们访问链表,取的应该是每个结点里面data域的数据吧。

其次,结点的指针++,得到的是下一个结点的指针吗?

是不是大概率不是啊,因为每个结点的空间都不一定是连续的啊。

那怎么办呢?我们可以来看一下库里面怎么实现的:

那其实刚才开始我们在浏览源码的时候也提到了,库里面是不是把迭代器也实现成一个类模板了,那这个类模板是啥呢?

4825f33ad4174c1da09be577f1cb37ae.png

02476cd265cd4d58aec2ec10ba4df0cb.png

我们发现迭代器这个类模板其实就是对结点的指针进行了一个封装。

但是,迭代器要能++找到下一个结点位置的迭代器,还要能够解引用取到结点里面的值等等一系列操作,怎么办?

1bd83cc929b24d06864a135501acb833.png

我们看到,它在类模板里面对++,–,解引用这些操作进行了重载。

++怎么实现的?

就是让node走到下一个结点。

解引用呢?

就是去取结点里data域的数据。

这样是不是就满足迭代器的需求了。

2.4 list迭代器的实现(重难点)

list_iterator:结点指针的封装

那接下来我们就来实现一下list的迭代器:

就是对结点的指针进行一个封装:

a6145436430845b3a37f0d00300c599c.png

*、前置++ 、!=的重载

那我们先来重载一下迭代器的++,*和!=,这三个重载完就可以使用迭代器遍历了

首先解引用就是返回当前结点的data:

811a64ba9980481db57003ad46d04330.png

然后++(先写一下前置),就是让结点的指针走到下一个结点,前置++返回++之后的值。就是他自己嘛

724f000691614debbf33c9626b05c273.png当然这个类型比较长,我们可以typedef一下:

53df138036394570bc934ad155864cac.png

然后!=:

94941b54af04417a944c504211c1bcc9.png

begin、end

然后我们在list里面加一个begin和end就可以用迭代器了:

那begin就是返回第一个元素位置(即头结点后面)的迭代器,end就是返回最后一个元素的下一个位置(即头结点位置)的迭代器:

74acde43b4264dd2903b6a3a8f433e59.png

那现在我们就可以用迭代器遍历list了:

3096b9436d0a435ea4bd18ff223fa375.png

🆗,是不是就可以了。

当然范围for也就支持了:

6db53d1f19974932a172a6fcfefb0c42.png

那学到这个地方,我们其实可以得到一个结论:

迭代器要么就是原生指针,要么就是自定义类型对原生指针进行封装,模拟指针的行为

思考

然后,大家来思考一下问题:d36dbad86ec3485e8d7ed6f4aaa84774.png

大家看,这个地方把begin的返回值赋值给it,发生了什么?

🆗,这个地方是不是会调用迭代器__list_iterator这个类的拷贝构造啊。

但是,我们自己并没有实现拷贝构造,所以这个地方回调用默认生成的拷贝构造。

但是默认生成的是浅拷贝,那这个地方浅拷贝有问题吗?

是不是没问题啊,这个地方是不是就应该是浅拷贝啊。begin返回的迭代器里面有一个结点的指针,指向list的第一个元素,然后把它拷贝给it,it里面的结点指针也指向第一个元素,这样后面++是不是才能正确找到后续的元素啊。

所以这个地方就应该是浅拷贝。


那再来思考:


为什么这个地方浅拷贝但是没有报错呢?

浅拷贝的话这里两个对象不是指向同一块空间了,我们之前遇到这种情况不是都报错了嘛,为什么这里没事呢?

🆗,我们之前浅拷贝造成程序崩溃时因为什么,是不是最后对同一块空间析构了两次,所以才会崩。

但是这里我们的迭代器__list_iterator类我们是不是自己都没写析构函数啊。

那需要写吗?

是不是不需要啊,因为它不需要去释放里面指针指向的结点的空间。

那为什么不需要释放啊?

🆗,它里面虽然有结点的指针,但是它指向的结点属于谁,是不是属于list啊,那结点的释放应该是谁的事情?

是不是list的析构应该干的事情啊。

这里的结点本身就不是你迭代器创建的,也不需要你去释放,你这里拿到结点的指针,只是帮助我去访问和修改链表的。


其它运算符重载

那我们再来重载一下后置++:


后置++和前置++的重载怎么区分,还记得吗?

前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载。C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递(它的作用就是为了构成重载),编译器自动传递。(- -也是如此)

那后置++和前置++的区别就是返回值不一样,后置++是先使用,后++,返回++之前的值。


1521204a84314c21a809fb206c693f58.png

再来实现一个前置- -和后置- -:

很简单,++向后走,–向前走

3e7c16bedffe49d6a8e08c3aae394f0c.png

再来个==:

50a26a647a24459a846e364b3c8efdec.png

那就实现的差不多了。

const迭代器

假如现在我们要写一个打印链表的函数:

a6e5721e818e4302ade8ea2d20e5ed7d.png

然后我们调用该函数:

2f574e3dfcaf4c3bbc6e56500d3d1bbc.png

发现报错了,为什么,是不是又是权限放大的问题啊?

怎么解决?

🆗,我们是不是要实现const迭代器,提供const版本的begin和end啊。


那在我们的list中,我们可以怎么实现const迭代器呢?


我们现在已经有了一个普通迭代器的类__list_iterator,那我们可以再实现一个const迭代器的类__list_const_iterator:

c01bc6b95b484711b0b9f2602cf47dbf.png

和普通的迭代器一样,我也可以进行++ - -等操作,唯一的区别就是你只能访问我指向的数据,而不能修改它。

然后,我们list类里面就可以实现const版本的begin和end了:

b47a3e153eeb4f14ba1b5f046eb9fc5c.png

那这样的话,我们的print_list函数里面:

9457e19b577647aaa66e3cfd1ca910f3.png

这个地方换成const_iterator就行了。

520e40d2d78c4baa9561e3f68769fdee.png

这下就可以了。

当然const_iterator就不能修改了:

e97aab7ba8b54f7ab6b02f6dcf5a70f6.png

代码优化:增加一个模板参数

但是:

这样写的话:

a318ecca18b84f1d95cec67df68ad55e.png

我们看到这两个类除了名字不同之外,唯一的区别就是operator*的返回值类型不同,一个返回引用,一个返回const引用。

那这样是不是太冗余了呀,那我们能不能想想办法,只写一个类,就搞定这两种情况呢,其实就是控制一下这里operator*的返回值,const对象调用就返回const引用,普通对象调用就返回引用。

那可以怎么做呢?

🆗,我们可以这样做:

我们不是要控制operator*的返回值不同情况下不一样(T&或const T&)嘛

a1e106ab08884b28afed07a3a70825ef.png

现在有一个参数T我们是可以拿到T&的,但是我们现在故意增加一个模板参数ref(reference——引用)。

d083ff4ee2424972831a0b0504eb325b.png

然后我们再做这样一件事情,在list类模板里面,我们就不再像原来那样搞了,这样:

e3956db3262a4a9aa0c0ea36c1609b70.png

普通迭代器就传引用,const迭代器就传const引用。

1d8db37826f5499c9e01df6a3225a495.png

1237738ffc3d46d5864a90c24e9932b0.png

是不是可以啊。

这种写法是不是感觉很牛逼啊,而且是不是就很好的避免了我们上面那样写造成的代码冗余的问题。

那其实库里面就是这样写的:

7183a6a38824435cb9e3789098b0c20a.png

->的重载

但是呢:

我们看到

6df1ebec1aee40af8222d714a5546293.png

库里面有三个模板参数,还有一个Ptr,这个又是干什么的呢?

299b207ae01f424499efb3a90f6440ba.png

那我们会发现库里面对于迭代器除了重载*还重载了->

那为什么还要重载->呢,什么场景下会用到呢?

🆗,我们看这种场景:

0b839829d8954a3d9cbb45328059e65c.png

现在有一个自定义类型AA,我们定义一个list变量l,让他里面存AA类型的数据。

然后我们使用迭代器打印一下:

84c25dff17a34f4d9f83b58096a95332.png

我们发现报错了,怎么回事?

🆗,我们这里对it进行解引用并打印,但是*it得到的是什么,是不是当前it对应的结点data域里面的数据,是什么?

是不是一个AA类型的变量啊,但是它是自定义类型,并且我们没有重载流插入,所以这里打印不成。

那如何解决呢?

首先我们可能会想到对AA这个类重载流插入,这当然是一个办法。

但是它是struct定义的,所以它的成员默认是公有的,所以我们可以不重载,这样也可以:

bbb1b14ea5604b30905bf1565578758e.png

那此时list里面放AA类型数据的话,我们的迭代器是不是就相当于是对AA*这个类型的结构体指针(或者说类指针)的封装啊,模拟结构体指针的行为。

但是正常情况下,我们拿到一个结构体指针或类对象的指针去访问它的成员,会先解引用,再通过.去访问吗?

是不是可以直接用->啊。

所以:

基于这样的原因,我们的迭代器也需要重载一下->

那怎么实现呢?

4c8460bc2e854ff8b92bb27c9c940f07.png

🆗,去调operator*operator*返回的是啥?

是结点的data域中的数据,然后再取它的地址返回。这样如果是自定义类型数据是不是返回的就是原生的类对象的指针啊,那就可以用->了。

我们来实现一下:

8a89fcd7514e42da9bd75129760ae690.png

是不是就这样啊。

113ebbc9a25848e8b0213684264bef96.png

就可以用->了。

但是,如果我们仔细观察一下的话,会发现好像有点不对啊,哪里不对呢?

大家看it->_a1,这样写对吗?

it->是不是it去调用operator->这个函数了,那它的返回值是啥?

是不是返回了结点的data域中放的类对象的地址(指针),那我们用这个地址去访问成员变量是不是可以再用->去访问,所以这里正常是不是应该这样写:it->->a1,等同于it.operator->()->a1。

没毛病啊,就是这样。

所以呢:

这个地方本来应该是两个->,但是为了增强代码可读性,省略了一个->,大家也可以认为这个地方进行了一个特殊处理。

就像前面我们讲过的前置++后置++重载的区分那种情况。

第三个模板参数

那现在回到我们上面的那个问题:

482a0bc733ae4014941eebcf1c954d98.png

为什么还有第三个模板参数Ptr

再看我们重载的->:

d3f2f39dfc604157947129e0ba446c81.png

现在它的返回值是T*,但是如果是const对象调用的话,是不是应该返回const T*,所以呢?

operator*,我们增加一个模板参数来控制不同情况下返回不同类型的返回值。

746b2bc083e5411c8b23f128885fabf7.png

196384e18dd445499a4d004d1625f305.png

这样const对象也可以使用->了:

aa2b14bd77fd4a0eaf1b0bd8b2473681.png

反向迭代器我们学到后面一点再讲。

2.5 插入删除操作

那接下来我们来实现一下insert和erase:


insert和erase实现好,头插头删、尾插尾删就可以直接复用了。

那这些东西呢也很简单,没什么新东西,都是我们数据结构阶段玩过的,所以这个部分的重点其实就是迭代器的实现,大家要好好看一看。


insert

那我们先来搞一下insert:

11b8332ff5aa4a20a0c21a741c93726d.png

1a018197810e41ca8d9e0aa88e9e6599.png

创建新结点链接就行了。

e65102200c674a6f92cafa46f911012b.png

测试 一下:

37d005f30f0b42b7be3b00ea00456905.png

没问题。


那大家来思考一个问题,list的insert会导致迭代器失效吗?


🆗,是不会的。

list的底层结构为带头结点的双向循环链表,在list中进行插入操作是不会导致list的迭代器失效的。

之前学到vector进行插入会导致迭代器失效是因为vector插入数据可能会扩容,扩容之后原来的迭代器就指向一块被释放的空间了,而且就算没有扩容,由于插入元素要挪动数据,那插入之后pos位置的就不在是原来的数据了。但是对于链表来说不存在这些问题。

插入前后,pos始终指向同一个结点,不会发生改变,因此在list中进行插入操作是不会导致list的迭代器失效的。

push_back 和 push_front

那实现了insert,push_back 和 push_front就可以直接复用了:


6ae5ddf4fafd4fdeaa902f1fc0d99e2a.png

试一下:

a55db5b77e554a2caed9cbd1813e8cf3.png

没问题。

erase、pop_back和pop_front

再来实现一下erase:

af9d56e8e8d24d9cb0df8315e9f18b99.png

ae6b45432b414152a39c9b70017a4056.png

然后pop_back和pop_front直接复用:

12ad7c9fa9d048708bc3f6b52214cac6.png

来测试一下:

e63f65511a644c44b1fd7ffc6e45dba6.png

那大家思考一下,erase会导致迭代器失效吗?

是不是会啊!

进行erase这些删除操作之后,当前迭代器指向的结点都被释放了,那它肯定失效了。

那失效了我们还想继续用怎么办?

7be90b10d5ff4abda0552fd3ee525f7b.png

那erase正常情况下是有返回值的:

b9fb26d875cc4a6e9814a5abcac8c92c.png

返回指向被删除元素后面元素的迭代器,如果被删除的是最后一个元素,则返回的是end()。

11618e4045234907bc95c6f42b233f75.png

那我们想继续用的话,接收一下返回值就行了。

2.6 clear和析构

那接下来我们写一下析构:

不过写析构之前我们可以先写一下clear,然后析构可以复用一下clear。

那clear是啥?

是不是清空list里面所有的元素啊,当然头结点不能清除。

来写一下:

1c231fd2a7bd42f390f26fc3f57b5722.png

这样是不是就行了啊,直接复用erase,但是erase会导致迭代器失效,所以我们接收一下返回值。

来试一下:

c2597e7461344d1aae18e249f760f9e2.png

除此之外呢,这个地方还可以这样写:

480f89cd73ad4def9a0212f823d3c782.png

大家看这样写可以吗?

🆗,这样也是可以的,我们看后置++是怎么实现的:

3b33df753bc34ef2a9f989f26bd77249.png

上来先++,迭代器已经更新到下一个位置了,然后这里返回的是++之前的迭代器的拷贝,所以不会导致迭代器失效,这样也是可以的。

8ef1fe115f5148da9908a95adfdcc972.png

但是这样写肯定是错误的:

f5584b2bae70453395d8de872144fd9c.png

1c6c5e6780c04ae6bf563a94a9299284.png

那然后我们来写析构:

那析构就是释放所有资源,包括头结点。

2532e6c65df7438b8ddd6896b7a2d373.png

就完成了。

2.7 迭代器区间构造和拷贝构造

我们再来实现一下迭代器区间的构造:

d0232e3c18894aa5b04fe16cbbfbc43b.png

很简单,我们vector就写过嘛,来写一下,

6fbda1142d174f7ba30c96cad5012a7f.png

但是现在这样写有没有什么问题?

🆗,直接尾插的话是不是的有头结点啊。

那我们可以学一下库里:

dc22be5a2ffa4cf7b66726dcfa3e5865.png

搞一个这个函数empty_initialize(空初始化):

e8b4c1e747f34138b816526eb9db3d96.png

这样在好多地方都可以直接复用。

测试一下:

75e358d2fc944ea4bb02eff4393fd7e9.png没问题。


不过呢,顺便提一下


在这个地方有的老铁可能会提出这样的疑惑,就是这里empty_initialize是非const成员函数,那要是定义const对象是不是调不了啊?

🆗,肯定是可以调的,要是这样认为的话,那我们的构造函数也都是非const的,那我们是不是就定义不了const对象了啊。

那为什么可以呢?

补充一个小知识点:

问大家一个问题,const变量在定义的时候有const属性吗?

是没有的,否则它还怎么初始化呢?

9ff9a2cdaea54386b392d1b637cad961.png

111行这句代码可以通过吗,它可以通过那110行就也没问题。

所以大家也可以认为这是一个特殊处理,const变量在定义的时候是不具有const属性的,定义完成之后才有

f648883f424948438bf0fe57dddc1bc4.png

所以是可以调的,没问题。

然后来搞一下拷贝构造:

a916cefed4a74c7a86604f400425aeb1.png

这样是不是就可以啊。

试一下:

80a1994139ce4c84a85a072b97003080.png

当然也可以用现代写法:

6ee8f65da0bf4faea3c312ae1ce51c27.png

但是呢?

825f466980154870a6cca1c9a7f03f97.png

有问题啊,怎么回事?

🆗,是不是忘了初始化了,就直接跟tmp交换了。

b6d77164de38439bb8c81b26fea1640b.png

96fc6d9ca51349a2aa3220f213d097d6.png

好了。

2.8 赋值重载

再来搞一个赋值重载:

那这个用现代写法也很easy:

4df5138613f84ba0b01845de6075768b.png

两句代码就搞定了。

测试一下:

39d56ba39d5e41aab48a1ea7d8ce33ea.png

注意:bb0bb8488a984a7092b0c13f7a2bf186.png

参数不能传引用,传引用的话就会把给它赋值的对象给改变了。

3. 源码展示

list.h

#pragma once
#include <assert.h>
#include <algorithm>
namespace yin
{
  template <class T>
  struct __list_node
  {
    __list_node<T>* _next;
    __list_node<T>* _prev;
    T _data;
    __list_node(const T& x = T())
      :_next(nullptr)
      , _prev(nullptr)
      , _data(x)
    {}
  };
  template <class T, class Ref, class Ptr>
  struct __list_iterator
  {
    typedef __list_node<T> node;
    typedef __list_iterator<T, Ref, Ptr> self;
    node* _node;
    __list_iterator(node* n)
      :_node(n)
    {}
    Ref operator*()
    {
      return _node->_data;
    }
    Ptr operator->()
    {
      return &_node->_data;
    }
    self& operator++()
    {
      _node = _node->_next;
      return *this;
    }
    self operator++(int)
    {
      self tmp(*this);
      _node = _node->_next;
      return tmp;
    }
    self& operator--()
    {
      _node = _node->_prev;
      return *this;
    }
    self operator--(int)
    {
      self tmp(*this);
      _node = _node->_prev;
      return tmp;
    }
    bool operator!=(const self& s)
    {
      return _node != s._node;
    }
    bool operator==(const self& s)
    {
      return _node == s._node;
    }
  };
  /*template <class T>
  struct __list_const_iterator
  {
    typedef __list_node<T> node;
    typedef __list_const_iterator<T> self;
    node* _node;
    __list_const_iterator(node* n)
      :_node(n)
    {}
    const T& operator*()
    {
      return _node->_data;
    }
    self& operator++()
    {
      _node = _node->_next;
      return *this;
    }
    self operator++(int)
    {
      self tmp(*this);
      _node = _node->_next;
      return tmp;
    }
    self& operator--()
    {
      _node = _node->_prev;
      return *this;
    }
    self operator--(int)
    {
      self tmp(*this);
      _node = _node->_prev;
      return tmp;
    }
    bool operator!=(const self& s)
    {
      return _node != s._node;
    }
    bool operator==(const self& s)
    {
      return _node == s._node;
    }
  };*/
  template <class T>
  class list
  {
    typedef __list_node<T> node;
  public:
    typedef __list_iterator<T, T&, T*> iterator;
    typedef __list_iterator<T, const T&, const T*> const_iterator;
    //typedef __list_const_iterator<T> const_iterator;
    const_iterator begin()const
    {
      return const_iterator(_head->_next);
    }
    const_iterator end()const
    {
      return const_iterator(_head);
    }
    iterator begin()
    {
      return iterator(_head->_next);
    }
    iterator end()
    {
      return iterator(_head);
    }
    void empty_initialize()
    {
      _head = new node;
      _head->_next = _head;
      _head->_prev = _head;
    }
    list()
    {
      empty_initialize();
    }
    template <class Iterator>
    list(Iterator first, Iterator last)
    {
      empty_initialize();
      while (first != last)
      {
        push_back(*first);
        ++first;
      }
    }
    /*list(const list<T>& l)
    {
      empty_initialize();
      for (auto e : l)
        push_back(e);
    }*/
    void swap(list<T>& l)
    {
      std::swap(_head, l._head);
    }
    list(const list<T>& l)
    {
      empty_initialize();
      list<T> tmp(l.begin(), l.end());
      swap(tmp);
    }
    list<T>& operator=(list<T> l)
    {
      swap(l);
      return *this;
    }
    ~list()
    {
      clear();
      delete _head;
      _head = __nullptr;
    }
    void clear()
    {
      iterator it = begin();
      while (it != end())
      {
        //it = erase(it);
        erase(it++);
      }
    }
    void push_back(const T& x)
    {
      //node* newnode = new node(x);
      找尾
      //node* tail = _head->_prev;
      链接
      //_head->_prev = newnode;
      //newnode->_next = _head;
      //tail->_next = newnode;
      //newnode->_prev = tail;
      insert(end(), x);
    }
    void push_front(const T& x)
    {
      insert(begin(), x);
    }
    void insert(iterator pos, const T& x)
    {
      node* cur = pos._node;
      node* prev = cur->_prev;
      node* newnode = new node(x);
      prev->_next = newnode;
      newnode->_prev = prev;
      newnode->_next = cur;
      cur->_prev = newnode;
    }
    iterator erase(iterator pos)
    {
      assert(pos != end());
      node* prev = pos._node->_prev;
      node* next = pos._node->_next;
      prev->_next = next;
      next->_prev = prev;
      delete pos._node;
      return iterator(next);
    }
    void pop_back()
    {
      erase(--end());
    }
    void pop_front()
    {
      erase(begin());
    }
    void print_list(const list<T>& l)
    {
      for (list<T>::const_iterator it = l.begin(); it != l.end(); ++it)
      {
        //(*it)++;
        cout << *it << " ";
      }
      cout << endl;
    }
  private:
    node* _head;
  };
}

test.cpp

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
#include <algorithm>
#include "list.h"
//int main()
//{
//  list<int> l;
//  l.push_back(1);
//  l.push_back(8);
//  l.push_back(4);
//  l.push_back(7);
//  l.push_back(-3);
//  for (auto e : l)
//    cout << e << " ";
//  cout << endl;
//  l.sort();
//  sort(l.begin(), l.end());
//  for (auto e : l)
//    cout << e << " ";
//  cout << endl;
//  return 0;
//}
//int main()
//{
//  yin::list<int> l;
//  l.push_back(1);
//  l.push_back(2);
//  l.push_back(3);
//  l.push_back(4);
//
//  for (auto e : l)
//    cout << e << " ";
//  cout << endl;
//
//  l.insert(++l.begin(), 99);
//  l.push_back(100);
//  l.push_front(200);
//
//  for (auto e : l)
//    cout << e << " ";
//  cout << endl;
//
//  l.erase(++l.begin());
//  l.pop_back();
//  l.pop_front();
//
//  for (auto e : l)
//    cout << e << " ";
//  cout << endl;
//  return 0;
//}
//struct AA
//{
//  int _a1;
//  int _a2;
//
//  AA(int a1 = 0, int a2 = 0)
//    :_a1(a1)
//    , _a2(a2)
//  {}
//};
//void print(const yin::list<AA>& l)
//{
//  for (yin::list<AA>::const_iterator it = l.begin(); it != l.end(); ++it)
//  {
//    //cout << (*it)._a1 << " " << (*it)._a2 << " ";
//    cout << it->_a1 << " " << it->_a2 << " ";
//    cout << endl;
//  }
//}
//int main()
//{
//  yin::list<AA> l;
//  l.push_back(AA(1, 1));
//  l.push_back(AA(2, 2));
//  l.push_back(AA(3, 3));
//  for (yin::list<AA>::iterator it = l.begin(); it != l.end(); ++it)
//  {
//    //cout << (*it)._a1 << " " << (*it)._a2 << " ";
//    cout << it->_a1 << " " << it->_a2 << " ";
//    cout << endl;
//  }
//  //print(l);
//  return 0;
//}
int main()
{
  yin::list<int> l;
  l.push_back(1);
  l.push_back(2);
  l.push_back(3);
  l.push_back(4);
  for (auto e : l)
    cout << e << " ";
  cout << endl;
  yin::list<int> l2;
  l2.push_back(10);
  l2.push_back(20);
  l2.push_back(30);
  l2.push_back(40);
  for (auto e : l2)
    cout << e << " ";
  cout << endl;
  l2 = l;
  for (auto e : l2)
    cout << e << " ";
  cout << endl;
  return 0;
}

🆗,那我们关于list的讲解就到这里了,欢迎大家指正!!!

d328b817d6f6435682f9d839415f2e1b.png

目录
相关文章
|
1天前
|
存储 算法 C语言
【C++】详解STL的适配器容器之一:优先级队列 priority_queue
【C++】详解STL的适配器容器之一:优先级队列 priority_queue
|
1天前
|
设计模式 存储 缓存
【C++】详解STL容器之一的deque和适配器stack,queue
【C++】详解STL容器之一的deque和适配器stack,queue
|
1天前
|
存储 算法 C++
【C++】详解STL容器之一的 vector
【C++】详解STL容器之一的 vector
|
1天前
|
算法 C语言 C++
【C++】详解STL的容器之一:list
【C++】详解STL的容器之一:list
|
6天前
|
C++ 容器
C++ STL标准库 《map容器详解》
C++ STL标准库 《map容器详解》
11 0
|
6天前
|
存储 C++ 容器
C++ STL标准库 《map容器详解》
C++ STL标准库 《map容器详解》
13 0
|
6天前
|
C++ 容器
【c++】优先级队列|反向迭代器(vector|list)
【c++】优先级队列|反向迭代器(vector|list)
5 0
|
6天前
|
C++
【c++】list模拟实现(2)
【c++】list模拟实现(2)
9 0
|
6天前
|
C++
【c++】list模拟实现(1)
【c++】list模拟实现(1)
12 0
|
7天前
|
算法 搜索推荐 C++
C++之STL常用算法(遍历、查找、排序、拷贝、替换、算数生成、集合)
C++之STL常用算法(遍历、查找、排序、拷贝、替换、算数生成、集合)
14 0