SGISTL源码探究-list容器(上)

前言

在本小节中,我们将介绍序列式容器之一的list,它采用的数据结构是环状双向链表,而前面分析的vector是线性存储的。list对于插入数据,删除数据的效率很高,并且迭代器不会轻易失效。但是它的迭代器并不像vector那样是原生指针,所以它的类型可能并不是random_access_iterator_tag型,并且需要自己内嵌那五种相应型别。那么接下来,我们就正式进入到list容器的源码。
首先分析它的数据结构及迭代器,然后是初始化以及结点的申请/释放还有析构,以及一些常用的操作。

list的数据结构

环状双向链表的节点
template <class T>
struct __list_node {
  typedef void* void_pointer;
  void_pointer next;
  void_pointer prev;
  T data;
};

需要注意的是,当list为空时仍有一个空结点。该结点不存储数据,这是因为STL规定迭代器范围必须是前闭后开的,比如[start, finish),如果尾部没有一个空结点,那么就无法完成覆盖全部元素并后开的结果。因为它不像vector是线性空间,可以将finish指向末尾元素的下一个空间。
在讲解构造函数的时候,我们会看到list默认会构造这样一个空结点。

其他

这里主要将list给数据类型定义的别名展示出来,方便后面讲解其他部分。

template <class T, class Alloc = alloc>
class list {
protected:
  typedef void* void_pointer;
  typedef __list_node<T> list_node;
  typedef simple_alloc<list_node, Alloc> list_node_allocator;
public:      
  typedef T value_type;
  typedef value_type* pointer;
  typedef const value_type* const_pointer;
  typedef value_type& reference;
  typedef const value_type& const_reference;
  typedef list_node* link_type;
  typedef size_t size_type;
  typedef ptrdiff_t difference_type;

public:
  typedef __list_iterator<T, T&, T*>             iterator;
  typedef __list_iterator<T, const T&, const T*> const_iterator;
  ......

可以看到前面都是关于迭代器以及空间配置器,还有各种类型的别名定义。list的专属空间配置器以list_node为分配单位。

千万要分清楚listiteratoriterator中的list_node指针的关系。iterator相当于对list_node指针做了一层包装,重载了一些操作符,让它能够正确的给出像*以及->等操作的结果。下面便是list的迭代器部分。

list的迭代器

list的迭代器并不是原生指针,因为它的数据结构是环状双向链表,如果使用原生指针,则无法完成像++,--,*等正确的操作,需要重载各种操作符,比如取值或者成员访问需要重载*->之类的操作符,将迭代器实现成一个对象,也需要自己内嵌五种相应型别。
以下是list的迭代器的源码。


template<class T, class Ref, class Ptr>
struct __list_iterator {
  //迭代器
  typedef __list_iterator<T, T&, T*>             iterator;  
  typedef __list_iterator<T, const T&, const T*> const_iterator;
  typedef __list_iterator<T, Ref, Ptr>           self;  
  //list的迭代器类型是bidirectional_iterator_tag的
  //这里定义了五种相应型别
  typedef bidirectional_iterator_tag iterator_category;
  typedef T value_type;
  typedef Ptr pointer;
  typedef Ref reference;
  typedef __list_node<T>* link_type;
  typedef size_t size_type;
  typedef ptrdiff_t difference_type;
  //迭代器内部的指针,指向双向链表上的节点
  //迭代器的一系列操作都靠这个内部的指针完成
  link_type node;
  //迭代器的构造函数
  __list_iterator(link_type x) : node(x) {}
  __list_iterator() {}
  __list_iterator(const iterator& x) : node(x.node) {}
  //重载相应的操作符
  bool operator==(const self& x) const { return node == x.node; }
  bool operator!=(const self& x) const { return node != x.node; }
  reference operator*() const { return (*node).data; }

#ifndef __SGI_STL_NO_ARROW_OPERATOR
  pointer operator->() const { return &(operator*()); }
#endif /* __SGI_STL_NO_ARROW_OPERATOR */

  self& operator++() {
    node = (link_type)((*node).next);
    return *this;
  }
  self operator++(int) {
    self tmp = *this;
    ++*this;
    return tmp;
  }
  self& operator--() {
    node = (link_type)((*node).prev);
    return *this;
  }
  self operator--(int) {
    self tmp = *this;
    --*this;
    return tmp;
  }
};

这样一来,list的迭代器至少可以表现的像一个指针了,但是由于是bidirectional_iterator_tag型的,所以并不支持随机存取等操作。

list的构造函数

list的构造函数和vector的种类类似。有如下几种:
- 默认构造函数
- 初始化n个值为value的结点
- 初始化n个值为默认值的结点
- 传入迭代器范围
- 拷贝构造函数

接下来我们就依次分析这些函数。

默认构造函数
link_type get_node() { return list_node_allocator::allocate(); }
...
void empty_initialize() {
  node = get_node();
  node->next = node;
  node->prev = node;
}
...
list() { empty_initialize(); }
...

可以看到list的默认构造函数分配了一个结点,不存储任何数据,只是将nextprev指向自己。你可能注意到了,这个node是从哪来的?
当然是list内部自己定义的

protected:
  link_type node;
初始化n个值为value的结点
void fill_initialize(size_type n, const T& value) {
  //先申请一个空白结点
  empty_initialize();
  __STL_TRY {
    //在begin()处插入n个值为value的结点
    insert(begin(), n, value);
  }
  //如果插入失败了,则将所有结点全部销毁(包括空白结点)
  __STL_UNWIND(clear(); put_node(node));
}
...
list(size_type n, const T& value) { fill_initialize(n, value); }
list(int n, const T& value) { fill_initialize(n, value); }
list(long n, const T& value) { fill_initialize(n, value); }

fill_initialize中使用了insertclearput_node等函数,这些我们放在后面分析。

初始化n个为默认值的结点
explicit list(size_type n) { fill_initialize(n, T()); }

可以看到这里和vector一样,都使用了explicit,就是为了防止隐式转换带来的问题。

传入迭代器范围
template <class InputIterator>
void range_initialize(InputIterator first, InputIterator last) {
  //还是老规矩,先申请一个空白结点
  empty_initialize();
  __STL_TRY {
    //调用insert将[first, last)范围内的元素插入从begin()开始的list
    insert(begin(), first, last);
  }
  //异常处理,销毁空间,防止内存泄露
  __STL_UNWIND(clear(); put_node(node));
}
...
template <class InputIterator>
list(InputIterator first, InputIterator last) {
  range_initialize(first, last);
}

主要就是使用了insert函数的功能。

拷贝构造函数
list(const list<T, Alloc>& x) {
  range_initialize(x.begin(), x.end());
}

和上一个构造函数类似,也是转换成迭代器的范围进行构造。

list的析构函数

~list() {
  clear();
  put_node(node);
}

里面调用了两个函数clearput_node,clear会释放了除了node之外的所有结点的空间,而put_node负责删除某个结点,这里是删除node
下面会对这两个函数进行分析。

list的结点的申请及释放

关于list结点的管理,上面已经介绍过了申请一个结点get_node。接下来便是put_nodecreate_node等函数。

释放一个结点
void put_node(link_type p) { list_node_allocator::deallocate(p); }

可见与申请一个结点相对,调用了空间配置器里面释放空间的函数。

申请并初始化一个结点
link_type create_node(const T& x) {
  //获取结点
  link_type p = get_node();
  __STL_TRY {
    construct(&p->data, x);
  }
  __STL_UNWIND(put_node(p));
  //返回该结点
  return p;
}
析构并释放一个结点
void destroy_node(link_type p) {
  destroy(&p->data);
  put_node(p);
}

常用操作

首先是关于迭代器的,如begin()等函数。其次是关于元素的操作,比如插入,删除,排序,合并等,由简到繁。

begin
iterator begin() { return (link_type)((*node).next); }
const_iterator begin() const { return (link_type)((*node).next); }

注意是返回nodenext,因为node仅仅是用来辅助的,它连接了list的尾部和首部。

end
iterator end() { return node; }
const_iterator end() const { return node; }

符合尾迭代器开区间

empty
bool empty() const { return node->next == node; }

只用检测辅助结点的下一个结点是不是自身就行了

size
size_type size() const {
  size_type result = 0;
  distance(begin(), end(), result);
  return result;
}

计算begin()end()返回的迭代器的距离。

clear
template <class T, class Alloc>
void list<T, Alloc>::clear()
{
  link_type cur = (link_type) node->next;
  /* 从第一个元素结点开始遍历
   * 直到回到node结点
   * 即遍历整个list
   */
  while (cur != node) {
    //依次释放结点
    link_type tmp = cur;
    cur = (link_type) cur->next;
    destroy_node(tmp);
  }
  //最后将node结点回归初始状态
  node->next = node;
  node->prev = node;
}

可以看到,调用clear函数会清楚整个list,将其回归到初始状态(只有一个node结点)。

push_back和push_front
void push_back(const T& x) { insert(end(), x); }
void push_front(const T& x) { insert(begin(), x); }

将元素插入到list末尾/开头。

pop_back和pop_front
void pop_front() { erase(begin()); }
void pop_back() {
  iterator tmp = end();
  erase(--tmp);
}

删除list首部/尾部元素。

erase

删除元素的操作有两个版本,一个是针对迭代器指向的位置,另一个是针对迭代器的范围进行删除。
第二种其实就是循环调用第一种erase

定点删除
iterator erase(iterator position) {
  /* 其实就是很简单的双向链表删除元素的操作
   * 将取出来的结点释放了然后返回指向删除的结点原来的下一个结点的迭代器
   */
  link_type next_node = link_type(position.node->next);
  link_type prev_node = link_type(position.node->prev);
  prev_node->next = next_node;
  next_node->prev = prev_node;
  //
  destroy_node(position.node);
  return iterator(next_node);
}
范围删除
list<T,Alloc>::iterator list<T, Alloc>::erase(iterator first, iterator last) {
  //在[first, last)的范围内反复调用定点删除的erase
  while (first != last) erase(first++);
  return last;
}

这里就涉及到迭代器的失效问题,指向被删除的元素的迭代器都将失效。但是其他的不会被影响。
并且list不像vector那样,可能需要释放原空间,新申请一片空间存放元素,这样的做法会造成所有的迭代器失效。

后面还有一些操作,比如insertmergesort等,我们都放在下一节进行分析。

小结

在本小节中我们了解了形成list的数据结构,以及list的迭代器类,还有它的一系列构造函数以及一些常用的操作。剩下的一些操作要复杂一点,所放在下一小节进行分析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值