C++源码剖析——list

Posted 落樱弥城

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++源码剖析——list相关的知识,希望对你有一定的参考价值。

  前言:之前看过侯老师的《STL源码剖析》但是那已经是多年以前的,现在工作中有时候查问题和崩溃都需要了解实际工作中使用到的STL的实现。因此计划把STL的源码再过一遍。
  摘要:本文描述了llvm中libcxx的list的实现。
  关键字list
  其他:参考代码LLVM-libcxx
  注意:参考代码时llvm的实现,与gnu和msvc的实现都有区别。

  list是STL提供的双向链表,可以在头和尾部操作元素。

1 节点

  和forward_list一样,list针对索引和节点进行了区分,索引只包含对应访问节点的指针,而节点同时包含链接的指针和数据成员。

template <class _Tp, class _VoidPtr>
struct __list_node_base
    typedef __list_node_pointer_traits<_Tp, _VoidPtr> _NodeTraits;
    typedef typename _NodeTraits::__link_pointer __link_pointer;
    __link_pointer __prev_;         //前向指针
    __link_pointer __next_;         //后向指针
;

template <class _Tp, class _VoidPtr>
struct _LIBCPP_STANDALONE_DEBUG __list_node : public __list_node_base<_Tp, _VoidPtr>
    _Tp __value_;
;

2 迭代器

  list的迭代器就是一个list_node的指针,其自增和自减的操作实现也比较简单就是访问node的前向和后向连接的指针。

template <class _Tp, class _VoidPtr>
class _LIBCPP_TEMPLATE_VIS __list_iterator
    typedef __list_node_pointer_traits<_Tp, _VoidPtr> _NodeTraits;
    typedef typename _NodeTraits::__link_pointer __link_pointer;

    __link_pointer __ptr_;

    __list_iterator& operator++()
        _LIBCPP_DEBUG_ASSERT(__get_const_db()->__dereferenceable(this),
                             "Attempted to increment a non-incrementable list::iterator");
        __ptr_ = __ptr_->__next_;
        return *this;
    

    __list_iterator& operator--()
        _LIBCPP_DEBUG_ASSERT(__get_const_db()->__decrementable(this),
                             "Attempted to decrement a non-decrementable list::iterator");
        __ptr_ = __ptr_->__prev_;
        return *this;
    
;

3 list实现

  list的内存布局由__list_impl描述,成员__end_作为索引节点描述当前链表的头尾。__end_nextprev两个指针分别指向链表的头和尾节点,__size_alloc_则保存了当前链表的长度。

template <class _Tp, class _Alloc>
class __list_imp
    __node_base __end_;
    __compressed_pair<size_type, __node_allocator> __size_alloc_;

    iterator begin() _NOEXCEPT
        return iterator(__end_.__next_, this);
    

    iterator end() _NOEXCEPT
        return iterator(__end_as_link(), this);
    
;

  而节点的操作就是比较典型的双链表的操作,没什么好说的。clear操作时通过for-loop来遍历每个节点并销毁对应的节点对象以及内存来实现的。

template <class _Tp, class _Alloc>
void __list_imp<_Tp, _Alloc>::clear() _NOEXCEPT
    if (!empty())
        __node_allocator& __na = __node_alloc();
        __link_pointer __f = __end_.__next_;
        __link_pointer __l = __end_as_link();
        __unlink_nodes(__f, __l->__prev_);
        __sz() = 0;
        while (__f != __l)
            __node_pointer __np = __f->__as_node();
            __f = __f->__next_;
            __node_alloc_traits::destroy(__na, _VSTD::addressof(__np->__value_));
            __node_alloc_traits::deallocate(__na, __np, 1);
        
        std::__debug_db_invalidate_all(this);
    

  list就是在__list_impl的基础上封装了常见的链表操作。

template <class _Tp, class _Alloc /*= allocator<_Tp>*/>
class _LIBCPP_TEMPLATE_VIS list : private __list_imp<_Tp, _Alloc>;

  我们就简单看下push_backpop_back咋实现的(其实就是简单的链表节点的插入)。
  push_back首先创建内存在对应的内存上构造输入的对象,然后将节点插入到尾部,节点插入也比较简单就是一般的双链表节点操作,不详述。

template <class _Tp, class _Alloc>
void list<_Tp, _Alloc>::push_back(const value_type& __x)
    __node_allocator& __na = base::__node_alloc();
    __hold_pointer __hold = __allocate_node(__na);
    __node_alloc_traits::construct(__na, _VSTD::addressof(__hold->__value_), __x);
    __link_nodes_at_back(__hold.get()->__as_link(), __hold.get()->__as_link());
    ++base::__sz();
    __hold.release();


template <class _Tp, class _Alloc>
inline void list<_Tp, _Alloc>::__link_nodes_at_back(__link_pointer __f, __link_pointer __l)
    __l->__next_ = base::__end_as_link();
    __f->__prev_ = base::__end_.__prev_;
    __f->__prev_->__next_ = __f;
    base::__end_.__prev_ = __l;

  pop_front的实现也比较简单直观,不详细描述。

template <class _Tp, class _Alloc>
void list<_Tp, _Alloc>::pop_front()
    _LIBCPP_ASSERT(!empty(), "list::pop_front() called with empty list");
    __node_allocator& __na = base::__node_alloc();
    __link_pointer __n = base::__end_.__next_;
    base::__unlink_nodes(__n, __n);
    --base::__sz();
    __node_pointer __np = __n->__as_node();
    __node_alloc_traits::destroy(__na, _VSTD::addressof(__np->__value_));
    __node_alloc_traits::deallocate(__na, __np, 1);


template <class _Tp, class _Alloc>
inline void __list_imp<_Tp, _Alloc>::__unlink_nodes(__link_pointer __f, __link_pointer __l)_NOEXCEPT
    __f->__prev_->__next_ = __l->__next_;
    __l->__next_->__prev_ = __f->__prev_;

《STL源码剖析》——第四章序列容器

 1、容器的概观与分类 

 技术图片

 

 

所谓序列式容器,其中的元素都可序(ordered)【比如可以使用sort进行排序】,但未必有序(sorted)。C++语言本身提供了一个序列式容器array,STL另外再提供vector,list,deque,stack,queue,priority-queue 等等序列式容器。其中stack和queue由于只是将 deque 头换面而成,技术上被归类为一种配接器(adapter)。

 2vector

vector的数据安排以及操作方式,与array非常相似。两者的唯一差别在于空间的运用的灵活性。

array是静态空间,一旦配置了就不能改变;要换个大(或小)一点的房子,可以,一切琐细得由客户端自己来:首先配置一块新空间,然后将元素从旧址一一搬往新址,再把原来的空间释还给系统。

vector是动态空间,随着元素的加入,它的内部机制会自行扩充空间以容纳新元素。因此,vector的运用对于内存的合理利用与运用的灵活性有很大的帮助,我们再也不必因为害怕空间不足而一开始就要求一个大块头array了,我们可以安心使用vector,吃多少用多少。

  • 常用的源码:

技术图片

 

 

 技术图片

 

 

 技术图片

 

 

 

 技术图片

 

 

 注意:

  • size() & captical:

表示已存储数据的大小,而capital则是内部开辟的空间的大小

所以,在erase、pop_back、clear等删除操作,都不会使得其captial产生变化,只会变的就是size()

  • 动态空间配置:

注意,所谓动态增加大小,并不是在原空间之后接续新空间(因为无法保证原空间之后尚有可供配置的空间),而是以原大小的两倍另外配置一块较大空间,然后将原内容拷贝过来,然后才开始在原内容之后构造新元素,并释放原空间。因此,对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了。这是程序员易犯的一个错误,务需小心。

 技术图片技术图片

  • insert():

insert()会根据需要插入元素的位置p以及p后面的元素个数与需要插入元素个数n进行比较,分两种情况进行插入

 技术图片技术图片

  • emplace_back()  VS   push_back()减少内存拷贝和移动
 1 struct President
 2 {
 3     President(std::string && p_name, std::string && p_country, int p_year)
 4         : name(std::move(p_name)), country(std::move(p_country)), year(p_year)
 5     {
 6         std::cout << "I am being constructed.
";
 7     }
 8     President(President&& other)
 9         : name(std::move(other.name)), country(std::move(other.country)), year(other.year)
10     {
11         std::cout << "I am being moved.
";
12     }
13     President& operator=(const President& other) = default;
14 };
15  
16 int main()
17 {
18     std::vector<President> elections;
19     std::cout << "emplace_back:
";
20     elections.emplace_back("Nelson Mandela", "South Africa", 1994);
21  
22     std::vector<President> reElections;
23     std::cout << "
push_back:
";
24     reElections.push_back(President("Franklin Delano Roosevelt", "the USA", 1936));
25 }
26 emplace_back:
27 I am being constructed.
28  
29 push_back:
30 I am being constructed.
31 I am being moved.

merge()函数: 

merge方式要注意三点:

merge(vec1.begin(),vec1.end(),vec2.begin(),vec2.end(),vec3.begin());

1、vec1,和vec2需要经过排序,merge只能合并排序后的集合,不然会报错。

2、vec3需要指定好大小,不然会报错。

3、merge的时候指定vec3的位置一定要从begin开始,如果指定了end,它会认为没有空间,当然,中间的位置我没有试,回头有空试一下。

 3list

相较于vector的连续线性空间,1ist就显得复杂许多,它的好处是每次插人或删除一个元素,就配置或释放一个元素空间。因此,list 对于空间的运用有绝对的精准,一点也不浪费。而且,对于任何位置的元素插人或元素移除,list永远是常数时间。

由于STL1ist是一个双向链表(double linked-list),迭代器必须具备前移、后移的能力,所以1ist 提供的是Bidirectional lterators。

list 有一个重要性质:插入操作(insert)和接合操作(splice)都不会造成原有的list迭代器失效。这在vector是不成立的,因为vector的插入操作可能造成记忆体重新配置,导致原有的迭代器全部失效。甚至1ist的元素删除操作(erase),也只有“指向被删除元素”的那个迭代器失效,其它迭代器不受任何影响。

技术图片

  • SGI list 是一个环状双向链表,它只需要一个指针就可以完整表现整个链表

 技术图片

  • list链表的初始结构:

node->next = node;

node->prv = node;

 技术图片

  • .transfter() //迁移函数

void transfer(iterator position, iterator first, iterator last);

将list2的first-last之间的元素插入到list1中的position中

 

  • .splice()  //衔接函数

void splice( const_iterator pos, list& other );

(1)

 

void splice( const_iterator pos, list&& other );

(1)

(C++11 起)

void splice( const_iterator pos, list& other, const_iterator it );

(2)

 

void splice( const_iterator pos, list&& other, const_iterator it );

(2)

(C++11 起)

void splice( const_iterator pos, list& other, 

             const_iterator first, const_iterator last);

(3)

 

void splice( const_iterator pos, list&& other, 

             const_iterator first, const_iterator last );

(3)

(C++11 起)

从一个 list 转移元素给另一个。

不复制或移动元素,仅重指向链表结点的内部指针。若 get_allocator() != other.get_allocator() 则行为未定义。没有迭代器或引用被非法化,指向被移动元素的迭代器保持合法,但现在指代到 *this 中,而非到 other 中。

1) 从 other 转移所有元素到 *this 中。元素被插入到 pos 所指向的元素之前。操作后容器 other 变为空。若 other 与 *this 指代同一对象则行为未定义。

2) 从 other 转移 it 所指向的元素到 *this 。元素被插入到 pos 所指向的元素之前。

3) 从 other 转移范围 [first, last) 中的元素到 *this 。元素被插入到 pos 所指向的元素之前。若 pos 是范围 [first,last) 中的迭代器则行为未定义。

参数

pos

-

将插入内容到其前的元素

other

-

要自之转移内容的另一容器

it

-

要从 other 转移到 *this 的元素

first, last

-

要从 other 转移到 *this 的元素范围

 

  • .merge()

c1.merge(c2)     

//合并2个有序的链表并使之有序,从新放到c1里,释放c2。

c1.merge(c2,comp) 

//合并2个有序的链表并使之按照自定义规则排序之后从新放到c1中,释放c2。

c1.splice(c1.beg,c2)     

//将c2连接在c1的beg位置,释放c2

c1.splice(c1.beg,c2,c2.beg)     

//将c2的beg位置的元素连接到c1的beg位置,并且在c2中释放掉beg位置的元素

c1.splice(c1.beg,c2,c2.beg,c2.end)     

//将c2的[beg,end)位置的元素连接到c1的beg位置并且释放c2的[beg,end)位置的元素

归并二个已排序链表为一个。链表应以升序排序

不复制元素。操作后容器 other 变为空。若 other  *this 指代同一对象则函数不做任何事。若 get_allocator() != other.get_allocator() ,则行为未定义。没有引用和迭代器变得非法,除了被移动元素的迭代器现在指代到 *this 中,而非到 other 中,第一版本用 operator< 比较元素,第二版本用给定的比较函数 comp 

此操作是稳定的:对于二个链表中的等价元素,来自 *this 的元素始终前驱来自 other 的元素,而且 *this  other 的等价元素顺序不更改。

参数

other

-

要交换的另一容器

comp

-

比较函数对象(即满足比较 (Compare) 概念的对象),若第一参数小于(即序于)第二参数则返回 ?true 。

比较函数的签名应等价于如下:

 bool cmp(const Type1 &a, const Type2 &b);

虽然签名不必有 const & ,函数也不能修改传递给它的对象,而且必须接受(可为 const 的)类型 Type1 与 Type2的值,无关乎值类别(从而不允许 Type1 & ,亦不允许 Type1 ,除非 Type1 的移动等价于复制 (C++11 起))。

类型 Type1 与 Type2 必须使得 list<T,Allocator>::const_iterator 类型的对象能在解引用后隐式转换到这两个类型。 ?

 

 4deque

deque和vector的最大差异:vector与deque都可以随机读取】

一在于deque允许于常数时间内对起头端进行元素的插入或移除操作,

二在于deque没有所谓容量(capacity)观念,因为它是动态地以分段连续空间组合而成,随时可以增加一段新的空间并链接起来。

  • 中控器:

deque是连续空间(至少逻辑上看来如此)。

deque系由一段一段的定量连续空间构成。一旦有必要在deque的前端或尾端增加新空间,便配置一段定量连续空间,串接在整个deque的头端或尾端。

deque的最大任务,便是在这些分段的定量连续空间上,维护其整体连续的假象,并提供随机存取的接口。避开了“重新配置、复制、释放”的轮回,代价则是复杂的迭代器架构。

deque采用一块所谓的map(注意,不是STL的map容器)作为主控。这里所谓map是一小块连续空间,其中每个元素(此处称为一个节点,node)都是指针,指向另一段(较大的)连续线性空间,称为缓冲区。缓冲区才是deque的储存空间主体。SGI STL允许我们指定缓冲区大小,默认值0表示将使用512bytes缓冲区。

当map的空间也不够后,会开辟另一个大的map空间

技术图片

 

 

 

  • 迭代器:

deque是分段连续空间。维持其“整体连续”假象的任务,落在了迭代器的operator++operator--两个运算子身上。

技术图片

  • 数据结构:
    • deque 除了维护一个先前说过的指向map的指针外,也维护 start,finish两个迭代器,分别指向第一缓冲区的第一个元素和最后缓冲区的最后一个元素(的下一位置)。此外,它当然也必须记住目前的map大小。因为一旦map所提供的节点不足,就必须重新配置更大的一块map。
    • 什么时候map需要重新整治?这个问题的判断由 reserve_map_at_back()和 reserve_map_at_front()进行,实际操作则由reallocate_map()执行;
    • 一个deque对象包含四个成员变量,_M_map指向主控器,_M_map_size表示中控器的大小,能够容纳多少个指针,_M_start表示deque的迭代器,所有元素的起始位置,_M_finish表示deque的迭代器,所有元素的终止位置。
    • deque的insert()操作:首先判断插入的地方是头或者尾,如果都不是则在调用一个名为insert_aux的辅助函数。此辅助函数通过判断当前的插入位置更靠近头端或者尾端。
    • deque的+=操作:首先判断是否在同一级缓冲区区域,如果不在,在确定应该夸几个缓冲区,然后到相应的缓冲区后,再移动

 5stack

  • sack定义完整列表

deque是双向开口的数据结构,若以deque为底部结构并封闭其头端开口,便轻而易举地形成了一个stack。因此,SGI STL便以deque作为缺省情况下的stack底部结构,stack的实现因而非常简单,源代码十分简短,

  • stack没有迭代器
  • list作为stack的底层容器

 6queue

  • queue定义完整列表
    • deque是双向开口的数据结构,若以deque为底部结构并封闭其底端的出口和前端的入口,便轻而易举地形成了一个queue。因此,SGISTL便以deque作为缺省情况下的queue底部结构queue的实现因而非常简单。
  • queue没有迭代器
  • 以list作为queue的底层容器
  • 优先队列
    • priority_queue<Type, Container, Functional>

 7 heap(隐式表述,implicit representation)【堆排序】

  • 概念

heap并不归属于STL容器组件,它是个幕后英雄,扮演priority queue

的助手。顾名思义,priority queue允许用户以任何次序将任何元素推入容器内,但取出时一定是从优先权最高(也就是数值最高)的元素开始取。binary max heap正是具有这样的特性,适合作为priority queue的底层机制。

如果使用list作为priority queue的底层机制,元素插入操作可享常数时间。但是要找到list中的极值,却需要对整个list进行线性扫描。我们也可以改变做法,让元素插人前先经过排序这一关,使得list的元素值总是由小到大(或由大到小),但这么一来,收之东隅却失之桑榆:虽然取得极值以及元素删除操作达到最高效率,可元素的插入却只有线性表现。

binary search tree作为priority queue的底层机制。这么一来,元素的插入和极值的取得就有O(logN)的表现。

但杀鸡用牛刀,未免小题大做,一来binary search tree的输入需要足够的随机性,二来binary search tree并不容易实现。priority queue的复杂度,最好介于queue和binary search tree 之间,才算适得其所。binary heap便是这种条件下的适当候选人。

binary heap就是一种 complete binary tree(完全二叉树)2,也就是说,整棵binary tree除了最底层的叶节点(s)之外,是填满的,而最底层的叶节点(s)由左至右又不得有空隙。【即是一棵完全搜索二叉树】

  • heap算法
    • push_heap

                    实现的是堆排序中的插入操作

        

 1 //向上调整
 2 void  upAdjust(int L, int R)
 3 {
 4     int  i = R, j = (i - 1) / 2;//i为欲调整结点,j为其父亲
 5     while (j >= L)
 6     {
 7         if (v[j] < v[i])//父节点小了,那么就将孩子节点调上来
 8         {
 9             swap(v[i], v[j]);
10             i = j;
11             j = (i - 1) / 2;//继续向上遍历
12         }
13         else//无需调整
14             break;
15     }
16 }
17 void insert(int  x)
18 {
19     v[n] = x;//将新加入的值放置在数组的最后,切记保证数组空间充足
20     upAdjust(0, n);//向上调整新加入的结点n
21 }
  • pop_heap
    实现的是堆排序的删除操作pop_heap
  •  1 //向下调整
     2 void downAdjust(int L, int R)
     3 {
     4     int i = L, j = 2 * L + 1;//i为父节点,j为左子节点
     5     while (j <= R)
     6     {
     7         if (j + 1 <= R && v[j + 1] > v[j])//若有右节点,且右节点大,那么就选右节点,即选取最大的子节点与父节点对比
     8             ++j;//选取了右节点
     9         if (v[j] <= v[i])//孩子节点都比父节点小,满足条件,无需调整
    10             break;
    11         //不满足的话,那么我就将最大孩子节点j与父节点i对调,
    12         swap(v[i], v[j]);
    13         i = j;
    14         j = 2 * i + 1;//继续向下遍历
    15     }
    16 }
    17 
    18 //删除堆顶元素
    19 
    20 void deleteTop()
    21 {
    22     v[0] = v[n - 1];//也就是堆顶使用最后一个数值来替代
    23     downAdjust(0, n - 2);//然后对前n-1个数进行排序
    24 }
  • sort_heap
    就是不断的pop出最大的元素sort_heap

                实现的就是堆排序

1 for (int i = n - 1; i > 0; --i)//从最后开始交换,直到只剩下最后一个数字
2 {
3     swap(v[i], v[0]);//每次都将最大值放到最后
4     downAdjust(0, i - 1);//将前0-i个数字重新构成大根堆
5 }
  • make_heap

实现的是堆排序的构建

1 //建堆
2 void createHeap()
3 {
4     for (int i = n / 2; i >= 0; --i)
5         downAdjust(i, n - 1);
6 }
  • heap没有迭代器

 8 priority_queue

  • 概念
    • priority_queue带有权值观念,其内的元素并非依照被推入的次序排列,而是自动依照元素的权值排列(通常权值以实值表示)。权值最高者,排在最前面。
  • 定义
    • 由于priority_queue 完全以底部容器为根据,再加上heap处理规则,所以其实现非常简单。缺省情况下是vector为底部容器。具有这种“修改某物接口,形成另一种风貌”之性质者,称为adapter(配接器),因此,STL priority-queue往往不被归类为container(容器),而被归类为container adapter。
  • 没有迭代器

 

 9slist

  • 概述
    • STL list是个双向链表double linked list)。SGI STL另提供了一个单向链表single linked list),名为slist。
  • slist和list的主要差别在于,前者的迭代器属于单向的Forward lerator,后者的迭代器属于双向的Bidirectional lterator。单向链表所耗用的空间更小,某些操作更快,不失为另一种选择。
  • 注意,根据STL的习惯,插入操作会将新元素插入于指定位置之前,而非之后。然而作为一个单向链表,slist没有任何方便的办法可以回头定出前一个位置,因此它必须从头找起。换句话说,除了slist起点处附近的区域之外,在其它位置上采用insert或erase操作函数,都属不智之举。这便是slist相较于1ist之下的大缺点。为此,slist 特别提供了insert_after()和erase_after()供灵活运用。
  • 迭代器

 10、常见错误总结:

迭代器失效:

·由于vector在扩容时,是在一块新地址上开辟空间,然后将原数据复制过来,并把原来的内存空间给释放了,所以一旦vector发生扩容,那么指向原来迭代器将会失效。

 

·由于list不是连续空间,所以删除和添加都在原的内存上添加或删除一个空间即可,所以指向原来的迭代器不会失效【除非该迭代器指向的位置被删除了】

 

·迭代器不仅可以后移,而且可以前进的,--ptr, ++ptr

 

以上是关于C++源码剖析——list的主要内容,如果未能解决你的问题,请参考以下文章

C++源码剖析——list

stl源码剖析-序列式容器 之 list

STL源码剖析(list)

《python解释器源码剖析》第4章--python中的list对象

Redis源码剖析 - Redis数据类型之列表List

List集合就这么简单源码剖析