1. UML类图
list是一个环状双向链表. 支持从容器的任何位置常数时间插入和删除元素.不支持快速随机访问。与std::forward_list相比,此容器提供双向迭代功能,但空间效率较低.
list就像下面图这样:
注意: 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类中提供了几个成员函数用来操作结点,其中两个函数就是用来将结点连接到链表中,以及将结点从链表中删除.
_M_hook函数
该函数接收一个参数
__position,用来将当前结点连接到__position这个结点上.
先看函数体内的前两行代码,假设__position是一个空list,begin()函数返回的头结点,当前结点是要新添加的结点(list初始化会将prev和next都指向this指针)
执行函数体内的前两行代码后,如下所示:
执行后两行代码后,形成一个双向循环链表
_M_unhook函数
比如说要删除下图的存储值为1的结点
执行前两行代码获取存储值为2和存储值为4的结点
后两行代码执行完如下图所示
2.1 构造函数
整个list的构造函数调用过程大致如下:
最终
_List_node_header类的构造函数调用_M_init()成员函数,该成员函数初始化next、prev和size
2.1.1 list(size_type __n, const allocator_type& __a = allocator_type())
该构造函数底层调用_M_default_initialize()使用默认构造元素创建list.
_M_default_initialize函数细节如下:
emplace_back函数细节如下:
底层调用
_M_insert函数在end()尾巴插入结点
begin()和end()函数的细节如下:
_M_insert函数细节如下:
核心函数就是这个,第一行先创建一个结点
_List_node; 第二行,将这个结点连接到链表中去; 第三行,递增链表个数.
_M_create_node函数细节如下:
第一行创建一个存储结点的内存;
然后使用分配器在这个内存上构造结点,并存储相应的值.
_M_create_node函数执行完,创建了一个结点,然后调用_M_hook连接到list链表中,并链表个数加1.整个构造函数完成.
2.2 list(size_type __n, const value_type& __value, const allocator_type& __a = allocator_type())
看下
_M_fill_initialize函数的细节:
底层调用
push_back函数,再来看下push_back函数细节:
push_back有两个版本,一个接收左值常量引用,一个接收右值引用.底层都是调用_M_insert函数.上面已经讲过,这里就不在赘述.
2.3 list(initializer_list<value_type> __l, const allocator_type& __a = allocator_type())
底层调用
_M_initialize_dispatch,第三个参数传入__false_type.因为_M_initialize_dispatch一共有两个版本,函数细节如下:
这里调用的是第二个版本,for循环遍历参数传入的迭代器范围,然后调用
emplace_back添加结点
2.2 拷贝构造、移动构造
2.2.1 list(const list& __x)
这里调用的也是false_type版本的_M_initialize_dispatch
2.2.2 list(list&& __x, const allocator_type& __a)
该函数使用了委托构造,并传入第三个参数
_Node_alloc_traits::is_always_equal{},确定参数__x和当前容器使用的分配器类型是否一致.
2.3 拷贝赋值、移动赋值运算符
2.3.1 list& operator=(const list& __x)
底层调用_M_assign_dispatch将参数__x的所有元素复制到当前容器中.在这之前判断两个容器分配器类型是否相等.
2.3.2 list& operator=(list&& __x)
底层调用
_M_move_assign,该函数也有两个版本
分别针对分配器相同的和分配器不同的版本.
2.3.3 list& operator=(initializer_list<value_type> __l)
给定一个初始化列表进行赋值.
底层调用assign函数进行拷贝赋值.
2.3 begin、end函数
list里有一个哨兵结点,该节点的_M_next存储的是头结点.
2.3.1 begin()、rbegin()
2.3.2 end()、rend()
end()表示最后一个元素的下一个位置,所以这里返回的是哨兵结点.
然后在哨兵结点里也存储着list尾结点的值,所以解引用哨兵结点也可以获取值.不用通过找到尾结点来获取值.
2.4 Capacity
2.4.1 empty
初始化的时候将_M_prev和_M_next指向this,所以判断是否为空,就判断这两个其中之一是否指向this.
2.4.2 size
返回成员变量
_M_size
2.5 Modifiers
2.5.1 clear
2.5.2 insert
iterator insert(const_iterator __position, const value_type& __x)
iterator insert(const_iterator __position, value_type&& __x)
iterator insert(const_iterator __p, initializer_list<value_type> __l)
该函数核心是
splice函数,用来将一个list插入到一个位置.该函数的底层核心是_List_node_base类的_M_transfer函数
_M_transfer函数细节如下,在每行代码前加上个序号,用于后续画图标明是哪个步骤
这里使用侯捷老师书里的图,觉得那个图画的比较好.
_M_tranfser函数执行之前:
_M_tranfser函数执行过程:
注意:这里是不包括last迭代器指向的结点的
2.5.3 emplace
2.5.4 erase
2.5.5 push_back
上面已经将结果
_M_insert,这里就不再说一遍了
2.5.6 emplace_back
2.5.7 pop_back
上图所示,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底层使用的函数一共有两个版本:
第一个版本:
底层调用_M_tranfser用来将参数__x容器的结点迁移到当前容器中.实现当前容器的结点升序排列.代码没有什么难的地方,浏览一遍就能明白.
这里使用了一个_Finalize_merge类,主要作用就是在该merge函数执行完,将相应的容器大小设置为合适的值.
执行该析构函数时, 参数src容器的
_M_next执行到了end()这里,所以__num_unmerged为0,后面几行代码将dest和src容器设置为合适的值.
第二个版本,和第一个版本大差不差,只不过参数提供了一个比较器,用来决定最后链表是升序还是降序
2.6.2 splice 将元素移动到其他链表中
因为splice版本太多,只解析底层的几个版本的实现.底层核心函数还是使用_M_tranfser用来迁移结点.
版本1
版本2
版本3
2.6.3 remove 传入一个值,删除链表所有存储该值的结点
代码并不复杂,就是遍历链表一点点找.然后将找到的结点存放到一个局部变量list中.在函数结束后,这个局部变量也会析构,连带着它里面的结点也会被析构释放掉.
2.6.4 remove_if 参数是一个谓语,比如传入一个Lambda,将某个条件下的元素删除掉
代码和上面差不多,只不过多了一个参数谓语
2.6.5 reverse 反转链表
2.6.6 unique 删除连续的重复元素
一共有两个版本,不接受谓语和接收谓语条件
版本1
版本2
2.6.7 sort(使用归并排序思想)
同样也有两个版本,接收条件和不接受条件的.
侯捷老师写的《STL源码剖析》在书上写的是quick sort,书上写错的。本质上是归并排序的思想,而不是快速排序的思想.快速排序思想是要有一个轴点,轴点左边的数都比轴点小,轴点右边的数都比右边小.然后再对轴点左右两边重复这个操作.
而归并排序的思想,是将整个序列分为最小不能再分的子序列,然后将子序列归并.依次往上归并,然后变为一个大序列.而std::list的sort函数就是使用这个思想.
无参数版本
代码中定义了一个64大小的数组,用来存放归并好的子序列,它就类似于下面这样,每个数组存放的结点个数跟下标有关.比如下标0存放的是2^0个结点,下标1存放的是2^1个结点,下标2存放的是2^2个结点,依次类推.
第一次: 将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循环,将最后的几个序列归并为一个大的序列
然后再调用swap函数将排序好序列存放到原list中
有参数版本
接收一个比较器,用来设置是升序还是降序排序.