STL容器(libstdc++11.4版本) --- list

124 阅读8分钟

1. UML类图

image.png

list是一个环状双向链表. 支持从容器的任何位置常数时间插入和删除元素.不支持快速随机访问。与std::forward_list相比,此容器提供双向迭代功能,但空间效率较低.

list就像下面图这样:
image.png 注意: list提供一个哨兵结点,不存储任何值. begin()则是通过哨兵结点._M_next来获取.
但是,end()返回是哨兵结点,本应该不存储值.通过解引用,却获取了哨兵结点底层存储的值,不知道为什么.哪位大佬如果知道请告诉小弟一下.调试了好几遍,实在不知道为什么


图中一个结点就是上图的_List_node,数据是存储在_List_node的成员_M_storage中.因为继承自_List_node_base,所以从基类继承两个成员_M_next_M_prev.

std::list还从_List_base继承来一个成员_M_size,类型是size_t.这个表示链表结点个数,也就是链表的大小.

2. std::list源码剖析

_List_node_base类中提供了几个成员函数用来操作结点,其中两个函数就是用来将结点连接到链表中,以及将结点从链表中删除. image.png

_M_hook函数

image.png 该函数接收一个参数__position,用来将当前结点连接到__position这个结点上. 先看函数体内的前两行代码,假设__position是一个空list,begin()函数返回的头结点,当前结点是要新添加的结点(list初始化会将prev和next都指向this指针)
image.png 执行函数体内的前两行代码后,如下所示:
image.png 执行后两行代码后,形成一个双向循环链表 image.png

_M_unhook函数

image.png 比如说要删除下图的存储值为1的结点 image.png 执行前两行代码获取存储值为2存储值为4的结点
后两行代码执行完如下图所示 image.png

2.1 构造函数

整个list的构造函数调用过程大致如下:
image.png 最终_List_node_header类的构造函数调用_M_init()成员函数,该成员函数初始化next、prev和size image.png

2.1.1 list(size_type __n, const allocator_type& __a = allocator_type())

image.png

该构造函数底层调用_M_default_initialize()使用默认构造元素创建list. _M_default_initialize函数细节如下:
image.png emplace_back函数细节如下:
image.png 底层调用_M_insert函数在end()尾巴插入结点

begin()end()函数的细节如下: image.png

_M_insert函数细节如下:
image.png 核心函数就是这个,第一行先创建一个结点_List_node; 第二行,将这个结点连接到链表中去; 第三行,递增链表个数.

_M_create_node函数细节如下: image.png 第一行创建一个存储结点的内存; image.png 然后使用分配器在这个内存上构造结点,并存储相应的值.
image.png

_M_create_node函数执行完,创建了一个结点,然后调用_M_hook连接到list链表中,并链表个数加1.整个构造函数完成.

2.2 list(size_type __n, const value_type& __value, const allocator_type& __a = allocator_type())

image.png 看下_M_fill_initialize函数的细节: image.png 底层调用push_back函数,再来看下push_back函数细节:
image.png push_back有两个版本,一个接收左值常量引用,一个接收右值引用.底层都是调用_M_insert函数.上面已经讲过,这里就不在赘述.

2.3 list(initializer_list<value_type> __l, const allocator_type& __a = allocator_type())

image.png 底层调用_M_initialize_dispatch,第三个参数传入__false_type.因为_M_initialize_dispatch一共有两个版本,函数细节如下:
image.png 这里调用的是第二个版本,for循环遍历参数传入的迭代器范围,然后调用emplace_back添加结点

2.2 拷贝构造、移动构造

2.2.1 list(const list& __x)

image.png

这里调用的也是false_type版本的_M_initialize_dispatch

2.2.2 list(list&& __x, const allocator_type& __a)

image.png 该函数使用了委托构造,并传入第三个参数_Node_alloc_traits::is_always_equal{},确定参数__x和当前容器使用的分配器类型是否一致.
image.png

2.3 拷贝赋值、移动赋值运算符

2.3.1 list& operator=(const list& __x)

image.png

底层调用_M_assign_dispatch将参数__x的所有元素复制到当前容器中.在这之前判断两个容器分配器类型是否相等.

2.3.2 list& operator=(list&& __x)

image.png 底层调用_M_move_assign,该函数也有两个版本 image.png 分别针对分配器相同的和分配器不同的版本.

2.3.3 list& operator=(initializer_list<value_type> __l)

给定一个初始化列表进行赋值.
image.png

底层调用assign函数进行拷贝赋值.
image.png

image.png

2.3 begin、end函数

list里有一个哨兵结点,该节点的_M_next存储的是头结点.

2.3.1 begin()、rbegin()

image.png image.png

2.3.2 end()、rend()

end()表示最后一个元素的下一个位置,所以这里返回的是哨兵结点.
image.png image.png 然后在哨兵结点里也存储着list尾结点的值,所以解引用哨兵结点也可以获取值.不用通过找到尾结点来获取值.

2.4 Capacity

2.4.1 empty

初始化的时候将_M_prev_M_next指向this,所以判断是否为空,就判断这两个其中之一是否指向this. image.png

2.4.2 size

image.png image.png image.png 返回成员变量_M_size

2.5 Modifiers

2.5.1 clear

image.png image.png

2.5.2 insert

iterator insert(const_iterator __position, const value_type& __x) image.png

iterator insert(const_iterator __position, value_type&& __x) image.png image.png

iterator insert(const_iterator __p, initializer_list<value_type> __l) image.png image.png 该函数核心是splice函数,用来将一个list插入到一个位置.该函数的底层核心是_List_node_base类的_M_transfer函数 image.png _M_transfer函数细节如下,在每行代码前加上个序号,用于后续画图标明是哪个步骤 image.png 这里使用侯捷老师书里的图,觉得那个图画的比较好.

_M_tranfser函数执行之前: image.png

_M_tranfser函数执行过程: image.png

注意:这里是不包括last迭代器指向的结点的

2.5.3 emplace

image.png image.png

2.5.4 erase

image.png image.png

2.5.5 push_back

image.png 上面已经将结果_M_insert,这里就不再说一遍了

2.5.6 emplace_back

image.png

2.5.7 pop_back

image.png image.png 上图所示,begin()是哨兵结点的next,end()是哨兵结点的prev 所以,上图pop_back函数源码,底层调用_M_erase删除的是_M_node._M_prev,也就是删除end()结点.

后面emplace_front、pop_front也都是对_M_erase_M_insert函数的封装

2.6 Operations

2.6.1 merge 合并两个链表

merge底层使用的函数一共有两个版本: image.png image.png

第一个版本: image.png 底层调用_M_tranfser用来将参数__x容器的结点迁移到当前容器中.实现当前容器的结点升序排列.代码没有什么难的地方,浏览一遍就能明白.
这里使用了一个_Finalize_merge类,主要作用就是在该merge函数执行完,将相应的容器大小设置为合适的值.
image.png 执行该析构函数时, 参数src容器的_M_next执行到了end()这里,所以__num_unmerged为0,后面几行代码将dest和src容器设置为合适的值.

第二个版本,和第一个版本大差不差,只不过参数提供了一个比较器,用来决定最后链表是升序还是降序
image.png

2.6.2 splice 将元素移动到其他链表中

因为splice版本太多,只解析底层的几个版本的实现.底层核心函数还是使用_M_tranfser用来迁移结点.

版本1 image.png

版本2 image.png

版本3 image.png

2.6.3 remove 传入一个值,删除链表所有存储该值的结点

image.png 代码并不复杂,就是遍历链表一点点找.然后将找到的结点存放到一个局部变量list中.在函数结束后,这个局部变量也会析构,连带着它里面的结点也会被析构释放掉.

2.6.4 remove_if 参数是一个谓语,比如传入一个Lambda,将某个条件下的元素删除掉

代码和上面差不多,只不过多了一个参数谓语 image.png

2.6.5 reverse 反转链表

image.png image.png

2.6.6 unique 删除连续的重复元素

一共有两个版本,不接受谓语和接收谓语条件 版本1 image.png

版本2 image.png

2.6.7 sort(使用归并排序思想)

同样也有两个版本,接收条件和不接受条件的.

侯捷老师写的《STL源码剖析》在书上写的是quick sort,书上写错的。本质上是归并排序的思想,而不是快速排序的思想.快速排序思想是要有一个轴点,轴点左边的数都比轴点小,轴点右边的数都比右边小.然后再对轴点左右两边重复这个操作.
而归并排序的思想,是将整个序列分为最小不能再分的子序列,然后将子序列归并.依次往上归并,然后变为一个大序列.而std::list的sort函数就是使用这个思想.

无参数版本 image.png

代码中定义了一个64大小的数组,用来存放归并好的子序列,它就类似于下面这样,每个数组存放的结点个数跟下标有关.比如下标0存放的是2^0个结点,下标1存放的是2^1个结点,下标2存放的是2^2个结点,依次类推. image.png

第一次: 将5取出来,判断__tmp[0]是否是空的,然后放到__tmp[0]中 第二次: 将10取出来,判断__tmp[0]是否是空的,不是空的,则将两个进行归并.然后放到__tmp[1]中 第三次: 将8取出来,__tmp[0]不是空的,则放到__tmp[0]中 第四次: 将9取出来,和__tmp[0]进行归并.然后又因为__tmp[1]不是空的,__tmp[1]不能存储超过两个结点,所以将这次归并后的两个结点和__tmp[1]中的两个结点存放到__tmp[2]中 然后依次类推

最后通过一个for循环,将最后的几个序列归并为一个大的序列 image.png 然后再调用swap函数将排序好序列存放到原list中 image.png

有参数版本 image.png 接收一个比较器,用来设置是升序还是降序排序.