4. 序列式容器

容器是什么?

容器container是用来存储数据的类模板, 封装了组织数据的各种方法。STL中有两类容器,序列式容器和关联式容器,如下图所示。

在这里插入图片描述


序列式容器主要包括vector、list和deque容器,其特点是元素在容器中的位置与元素值没有关系,即元素在容器中是无序的,元素插入到容器中,指定什么位置,元素就位于什么位置。
关联式容器主要包括setmultisetmapmultimap,其特点是容器内的元素会自动排序,元素插入的位置与元素在容器中的位置没有任何关系。
本文先介绍各种序列式容器的关键实现细节,关联式容器的工作原理将在下一篇文章中介绍。

list

list容器,又称为双向链表容器,容器的底层以双向链表形式实现,其特点是list中的元素可以分散存储在内存空间中,而不是必须存储在一块连续的内存空间中。
为什么首先讲list容器,而不是vector容器?原因是在上一篇迭代器文章中,我们初步实现了一个listiterator,通过与STL的实现可以更好的对比学习迭代器知识。

list节点

list容器底层是以双向链表形式实现,因此底层的数据结构list node的定义如下:

// list node
template <class T>
struct __list_node
{
    
    
    typedef voide* void_pointer;
    void_pointer prev;
    void_pointer next;
    T data;
};

list专属迭代器

通过上一篇文章迭代器解析,我们知道迭代器是一个smart poiner,即有能力进行正确的递增、递减、取值、成员访问等操作;为了能使iterator traits能有效工作,迭代器需要以内嵌型别定义nested typedef方式定义五种常见的型别信息。因此,迭代器设计分为如下两块:

  • smart pointer
typedef __list_node<T>*                 link_type;
link_type node; 			// 迭代器内部指针, 指向list节点
// 实现递增、递减、取值和成员访问等运算符重载
  • 五种型别信息
typedef T                               value_type;
typedef Ref                             reference;
typedef Ptr                             pointer;
typedef ptrdiff_t                       difference_type;
typedef bidirectional_iterator_tag      iterator_category;

list迭代器的设计如下:

// list node的专属迭代器
template <class T, class Ref, class Ptr>
struct __list_iterator
{
    
    
    typedef __list_iterator<T, T&, T*>      iterator;
    typedef __list_iterator<T, Ref, Ptr>    self;
    typedef __list_node<T>*                 link_type;
    typedef size_t                          size_type;

    typedef T                               value_type;
    typedef Ref                             reference;
    typedef Ptr                             pointer;
    typedef ptrdiff_t                       difference_type;
    typedef bidirectional_iterator_tag      iterator_category;

    link_type node; 

    // constructor
    __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;}
    // 对迭代器取值(derefence), 取的是节点的数据值
    reference operator* () const {
    
    return (*node).data;}
    // 对迭代器的成员进行访问
    pointer operator-> () const {
    
    return &(operator*());}
    // 对迭代器累加1,  前进一步
    self& operator++ ()
    {
    
    
        node = (link_type)((*node).next);
        return *this;
    }
    self operator++(int)
    {
    
    
        self tmp = *this;	// 调用拷贝构造函数
        ++*this;
        return tmp;			// 调用拷贝构造函数
    }
    // 对迭代器递减1, 就是后退一个节点
    self& operator--()
    {
    
    
        node = (link_type)((*node).prev);
        return *this;
    }
    self operator--(int)
    {
    
    
        self tmp = *this;
        --*this;
        return tmp;
    }
};

在list迭代器中,递增时指向下一个节点,递减时指向上一个节点,取值时取得是节点的data值,如下图所示:

在这里插入图片描述


其中后置递增操作的工作过程如下,首先调用拷贝构造函数,记录原值,然后对迭代器本身进行加一操作,最后返回记录的原值。

在这里插入图片描述

operator->的工作原理在上一篇迭代器文章中已经介绍过了,简单来说,但operator->的返回值为指针时,右操作数为指针执行数据类型的成员。

在这里插入图片描述

list的内存管理

list中缺省使用alloc作为空间配置器,并据此定义了一个list_node_allocator,以便以节点大小分配内存。

template <class T, class Alloc = alloc>
class list
{
    
    
protected:
    typedef __list_node<T> list_node;
    // 空间配置器, 每次配置一个节点大小
    typedef simple_alloc<list_node, Alloc> list_node_allocator;
	...
}

其中配置、释放、构造、销毁一个节点的实现如下:

// 给一个list节点分配内存, 返回一个list节点指针
link_type get_node() {
    
    return list_node_allocator::allocate();}
// 释放一个list节点内存
void put_node(link_type p) {
    
    list_node_allocator::deallocate(p);}
// 产生一个节点, 包括节点内存分配和调用构造函数
link_type create_node(const T& x)
{
    
    
    link_type p = get_node();
    construct(&p->data, x); // 全局函数, 构造对象
    return p;
}
// 销毁(析构并释放)一个节点
void destroy_node(link_type p)
{
    
    
    destroy(&p->data);  // 全局函数, 析构对象
    put_node(p);
}

list的数据结构

SGI list不仅是一个双向链表,而且还是一个环状双向链表。只需要一个指针,便可以完整地表示整个链表,因此,list中只有一个指针成员变量。

// 对list node数据进行处理
template <class T, class Alloc = alloc>
class list
{
    
    
protected:
    typedef __list_node<T> list_node;
    // 空间配置器, 每次配置一个节点大小
    typedef simple_alloc<list_node, Alloc> list_node_allocator;
public:
    typedef list_node*  link_type;

protected:
    link_type node;     // 只要一个指针, 便可表示整个环状双向链表
	...
}

STL 中通过将成员指针变量node指向一个空白节点(哨兵节点),来符合“前闭后开”的区间要求。其中空白节点,在每个构造函数中都会通过调用empty_initialize函数构建。以默认构造函数为例,代码如下:

public:
    // default constructor
    list() {
    
    empty_initialize();}    // 产生一个空链表
protected:
    void empty_intialize()
    {
    
    
        node = get_node(); // 配置一个节点空间, 令成员node指向它
        node->next = node;  // 令node的头尾都指向自己,不设元素值
        node->prev = node;
    }

空白节点的结构如下:

在这里插入图片描述

list示意图如下:

在这里插入图片描述


当将list的成员变量node指针特意指向空白指针之后,可以轻松的完成一下几个函数:

iterator begin() {
    
     return (link_type)((*node).next); }
iteator end()    {
    
     return node; }

bool empty() const {
    
    return node->next == node;}
size_type size() const
{
    
    
    size_type result = 0;
    distance(begin(), end(), result);   // 全局函数
    return result;
}
// 取头节点的元素值
reference front() {
    
    return *begin();}
// 取为节点的元素值
reference back() {
    
    return *(--end());}

在list中有三个很重要的成员函数:insert、erase和transfer,很多成员函数都是通过调用这三个函数实现。

insert函数

insert是一个重载函数,有多种形式,其功能是在给定迭代器所指位置的前方插入一个节点。需要重点注意新的节点插入的位置是给定迭代器所指节点的前方。insert的代码实现如下:

// insert函数: 在迭代器position所指位置的前方插入一个节点, 内容为x
iterator insert(iteartor postion, const T& x)
{
    
    
    link_type tmp = create_node(x); 
    // 调整双向指针, 将tmp插入进去
    tmp->next = position.node;                      // 1
    tmp->prev = position.node->prev;                // 2
    (link_type(position.node->prev))->next = tmp;   // 3
    position.node->prev = tmp;                      // 4
    return tmp;     // iterator(tmp)
}

插入一个节点,指针变换过程如下图所示,下图指的是输入迭代器为end(),插入一个节点,可以看到end()所指向节点的prev指针指向的就是这个新的节点,也即插入节点在给定迭代器所指节点的前方。

在这里插入图片描述

以下函数内部通过调用insert函数完成操作。

    // ************** 调用insert函数 ************** //
    // 插入一个节点, 作为头节点
    void push_front(const T& x) {
    
    insert(begin(), x);}
    // 插入一个节点, 作为尾节点
    void push_back(const T& x) {
    
    insert(end(), x);}
    // ****************************************** //

erase函数

erase函数的功能是移除给定迭代器所指向的节点。

// erase函数: 移除迭代器position所指位置的节点, 并返回下一个节点
iterator erase(iterator position)
{
    
    
    link_type next_node =  (link_type)(positon.node->next);     // 1
    link_type prev_node = (link_type)(position.node->prev);     // 2
    prev_node->next = next_node;                                // 3
    next_node->prev = prev_node;                                // 4
    destroy_node(position.node);                                // 5
    return iterator(next_node);
}

删除list中一个节点的操作流程入下图所示:

在这里插入图片描述

以下函数内部通过调用erase函数完成操作。

    // ************* 调用erase函数 ************* //
    // 移除节点
    void pop_front() {
    
    erase(begin());}
    // 移除尾节点
    void pop_back()
    {
    
    
        iterator tmp = end();
        erase(--tmp);
    }
    // 清除所有节点
    void clear()
    {
    
    
        link_type cur = (link_type) node->next;  // begin()
        while (cur != node) // begin() != end()
        {
    
    
            link_type tmp = cur;
            cur = (link_type) cur->next;
            destroy_node(tmp) ;  // 销毁一个节点
        }
        // 恢复node原始状态
        node->next = node;
        node->prev = node;
    }
    // 将数值为value的所有元素移除
    void remove(const T& value)
    {
    
    
        iterator first = begin();
        iterator last = end();
        while(first != last)
        {
    
    
            iterator next = first;
            ++next;
            if(*first == value) erase(first); // 找到就移除
            first = next;
        }
    }
    // 移除数值相同的连续元素
    // “连续并且相同的元素”才会被移除剩一个
    void unique()
    {
    
    
        iterator first = begin();
        iterator last = end();
        if(first == last) return;  // 空链表, 什么都不必做
        iterator next = first;
        while(++next != last)
        {
    
    
            if(*first == *next) // 如果在此区段中有相同的元素
                erase(next);
            else
                first = next;
            next = first;
        }
    }   
    // ************************************* //
};

transfer函数

transfer函数是list内部提供的一个迁移操作,其功能是将某连续范围的元素迁移到某个特定位置之前。transfer的源代码如下:

    // 将[first, last)内所有元素移动到position之前
    void transfer(iterator position, iterator first, iterator last)
    {
    
    
        if(position != last)
        {
    
    
            (*(link_type((*last.node).prev))).next = position.node;  // 1
            (*(link_type((*first.node).prev))).next = last.node;     // 2
            (*(link_type((*position.node).prev))).next = first.node;     // 3
            link_type tmp = link_type((*position.node).prev);           // 4
            (*position.node).prev = (*last.node).prev;                  // 5
            (*last.node).prev = (*first.node).prev;                     // 6
            (*first.node).prev = tmp;                                   // 7
        }
    }

总共包含7个步骤,如下图所示:

在这里插入图片描述

transfer函数并不是stl的公开接口,而是splice函数,以下是splice函数的各种版本实现:

    // splice函数
    // 将另一个list结合在position所指位置之前
    void splice(iterator position, list& x)
    {
    
    
        if(!x.empty())
            transfer(position, x.begin(), x.end());
    }
    // 将i所指节点接合于position所指节点之前, position和i可以指向同一个list
    void splice(iterator position, list&, iterator i)
    {
    
    
        iterator j = i;
        ++j;
        if(position==i || position==j) return;
        transfer(position, i, j);
    }
    // 将[first, last)内所有元素结合于position所指节点之前
    // position和[first, last)可指向同一个list
    // 但是position不能位于[first, last)之内
    void splice(iterator position, list&, iterator first, iterator last)
    {
    
    
        if(first != last)
            transfer(position, first, last);
    }

此外,sortmergereverse函数都是通过在内部调用transfer函数完成的,在这里就不做赘述。

vector

vector的数据安排及操作方式,与array非常相似,唯一的区别在于,array是静态空间,空间大小一旦配置就不能再改变,而vector是动态空间,随着元素加入,其内部会自动扩充空间以容纳新元素,下图非常形象的展示它们之间的区别。

在这里插入图片描述

vector元素

vectortemplate class,其内部存储的元素类型在创建对象时指定,如vector<int>vector<string>等,vector容器中存储的元素类型分别是int或者stringvector容器中只能存储同一种类型的元素。

vector迭代器

vector的专属迭代器,其实用vector元素对应数据类型的指针即可,为什么这么说呢?我们先可以仿照list的迭代器编写vector迭代器,代码如下:

template <class T, class Ref, class Ptr>
struct __vector_iterator
{
    
    
    typedef __vector_iterator<T, T&, T*>      iterator;
    typedef __vector_iterator<T, Ref, Ptr>    self;
    typedef T*                              link_type;
    typedef size_t                          size_type;

    typedef T                               value_type;
    typedef Ref                             reference;
    typedef Ptr                             pointer;
    typedef ptrdiff_t                       difference_type;
    typedef random_access_iterator_tag      iterator_category;

    link_type elem_ptr;         // 需要一个指针, 指向vector容器

    // constructor
    __list_iterator(link_type x) : elem_ptr(x) {
    
    }
    __list_iterator()   {
    
    }
    __list_iterator(const iterator& x) : elem_ptr(x) {
    
    }

    bool operator== (const self& x) const {
    
    return elem_ptr == x.elem_ptr;}
    bool operator!= (const self& x) const {
    
    return elem_ptr != x.elem_ptr;}
    // 对迭代器取值(derefence), 取的是节点的数据值
    reference operator* () const {
    
    return *elem_ptr;}
    // 对迭代器的成员进行访问
    pointer operator-> () const {
    
    return elem_ptr;}
    // 对迭代器累加1,  前进一步
    self& operator++ ()
    {
    
    
        ++this;
        return *this;
    }
    self operator++(int)
    {
    
    
        self tmp = *this;
        ++*this;
        return tmp;
    }
    // 对迭代器递减1, 就是后退一个节点
    self& operator--()
    {
    
    
        --this;
        return *this;
    }
    self operator--(int)
    {
    
    
        self tmp = *this;
        --*this;
        return tmp;
    }
};

通过之前的学习,可以知道设计迭代器有两个重点:

  • 迭代器是一个smart pointer,需要完成递增、递减、取值、成员访问等操作,但是观察上面各种operator的实现可以看出只是对指针的operator的简单封装;
  • 迭代器需要以内嵌型别定义nested typedef方式定义五种常见的型别信息,但是从上一篇文章中知道iterator traits对普通指针和常量指针都做了偏特化处理,因此,当传入一个指针时,经过iterator traits能自动得到五种常见的型别信息;
    在这里插入图片描述


综上,在vector容器中,使用vector元素数据类型对应的普通指针作为迭代器即可满足所有要求。

vector的内存管理

vector缺省使用alloc作为空间配置器,并据此另外定义了一个data_allocator,使其能够一次分配N个元素大小空间:

template <class T, class Alloc = alloc>
class vector 
{
    
    
protected:
    typedef T                               value_type;
    typedef value_type*                     iterator;       // 使用普通指针即可满足迭代器的需求
    typedef simple_alloc<value_type, Alloc> data_allocator;
    ...
};

因此,通过data_allocator::allocate(n)即可配置n个元素空间。

vector的数据结构

template <class T, class Alloc = alloc>
class vector 
{
    
    
public:
    typedef T                               value_type;
    typedef value_type*                     iterator;       // 使用普通指针即可满足迭代器的需求
    typedef value_type*                     pointer;
    typedef valure_type&                    reference;
    typedef size_t                          size_type;
    typedef ptrdiff_t                       difference_type;
protected:
    typedef simple_alloc<value_type, Alloc> data_allocator; // 空间配置器

    // 成员变量
    iterator start;             // 表示目前使用空间的头
    iterator finish;            // 表示目前使用空间的尾
    iterator end_of_storage;    //表示目前可用空间的尾
    ...
};

vector类中只有三个迭代器(普通指针),startfinish分别指向当前配置得到的连续空间中已被使用的范围,end_of_storage则指向已经配置连续空间的尾端。

在这里插入图片描述


通过上述三个迭代器,可以很轻松完成如下方法的功能。其中capacity指的是连续空间配置的大小,size指的是当前连续空间已被使用的大小。

public:
    iterator begin() {
    
    return start;}
    iterator end()  {
    
    return finish;}
    size_type size() const {
    
    return size_type(end() - begin());}
    size_type capacity() const {
    
    return size_type(end_of_storage - begin());}
    bool empty() const {
    
    return begin() == end();}
    reference operator[](size_type n) {
    
    return *begin() + n;}

    reference front() {
    
    return *begin();}
    reference back() {
    
    return *(end() - 1);}
    ...

vector的构造函数

在这里插入图片描述

如上图所示,vector中提供了4种构造函数,其中fill指的是使用同一个值初始n个元素空间,range指的是使用已有容器中一段区间内的数据初始化vector容器,copy指的是使用已有的容器初始化新的vector容器。后面3种构造函数的代码如下,内部通过调用unintialized_fill_nuninitialized_copy全局函数完成。

// 构造函数
vector(size_type n, const T& value) {
    
    fill_initialize(n, value);}
void fill_initialize(size_type n, const T& value)
{
    
    
    start = allocate_and_fill(n, value);
    finish = start + n;
    end_of_storage = finish;
}
iterator allocate_and_fill(size_type, n, const T& value)
{
    
    
    iterator result = data_allocator::allocate(n); // 配置n个元素空间
    unintialized_fill_n(result, n, x) ; // 全局函数
    return result;
}
vector (InputIterator first, InputIterator last,
             const allocator_type& alloc = allocator_type())
{
    
    
    finish = uninitialized_copy(first, last, start);        // 全局函数
}
vector(const vector<T, alloc>& x) 
{
    
    
    finish = uninitialized_copy(x.begin(), x.end(), start); // 全局函数
}

insert函数

vector容器中,通过底层调用insert_aux函数来插入数据,并自动动态增加大小,具体做法是以原容器大小的两倍另外配置一块空间,然后将原内容拷贝过来,然后开始在原内容之后构造元素,并释放原空间。具体代码和流程图如下:

void insert_aux(iterator position, const T& x)
{
    
    
    if(finish  != end_of_storage)   // 还存在备用空间
    {
    
    
        // 在备用空间起始处构造一个元素,并以vector最后一个元素值作为其初值
        construct(finish, *(finish - 1));
        ++finish;
        T x_copy = x;
        copy_backward(position, finish - 2, finish - 1);
        *position = x_copy;
    }   
    else    // 没有备用空间
    {
    
    
        const size_type old_size = size();
        const size_type len = old_size != 0 ? 2 * old_size : 1;
        iterator new_start = data_allocator::allocate(len);  // 实际配置空间
        iterator new_finish = new_start;
        try
        {
    
    
            // 将原vector的内容拷贝到新的vector
            new_finish = unintialized_copy(start, position, new_start);
            // 为新元素设定初值x
            construct(new_finish, x);
            ++new_finish;
            new_finish = unintialized_copy(position, finish, new_finish);
        }
        catch(...)
        {
    
    
            ...
        }
        // 析构并释放原vector
        destroy(begin(), end());
        deallocate();
        // 调整迭代器, 指向新的vector
        start = new_start;
        finish = new_finish;
        end_of_storage = new_start + len;
        
    }
}

在这里插入图片描述


在这里插入图片描述


由于空间重新配置,原vector的所有迭代器都失效,不能获取到正确的数据。下面是一个小例子。

// inserting into a vector
#include <iostream>
#include <vector>

int main ()
{
    
    
  std::vector<int> myvector (3,100);    // 分配3个元素空间, 调用unintialized_fill_n全局函数初始化元素内容为100
  std::vector<int>::iterator it;

  it = myvector.begin();                // 获取得到当前vector容器的start迭代器
  myvector.insert ( it , 200 );    // 想vector容器插入一个值为200的数据, 但是, 此时vector容器中已经没有可用空间,会重新分配两倍内存, 6个字节
  // it此时不再指向vector的begin
  std::cout << "myvector contains:";
  for (it=myvector.begin(); it<myvector.end(); it++)    // 需要重新获取
    std::cout << ' ' << *it;
  std::cout << '\n';

  return 0;
}

push_back函数将新元素插入到vector尾端时,会首先判断是否还存在备用空间,如果有则直接构建元素,并调整迭代器。如果没有则通过调用insert_aux函数扩充空间。

erase函数

erase函数支持删除(析构)[first, last) 中所有元素和指定某个位置元素两种形式,下面主要讲解如何清除[first, last)中所有元素的代码和工作流程,如下所示:

iterator erase(iterator first, iterator last)
{
    
    
    iterator it = copy(last, finish, first);//1
    destory(i, finish); // 2
	finish = finish - (last - first);	// 3
    return first;
}

在这里插入图片描述


clear函数通过直接调用上面的erase函数来清除vector中所有的元素,代码如下:

void clear () {
    
     erase(begin(), end()); }

需要强调的一点是,erase函数只是将元素析构,但是并没有释放对应的内存空间。

deque

与vector单向开口不同,deque是一种双向开口的连续线性空间,可以在头尾两端分别做元素的插入和删除操作,如图所示。

在这里插入图片描述

deque的中控器

deque的空间是由一段段连续空间组合而成,通过动态的增加一段新的空间来增加内存空间。为了管理这一段段连续空间,deque采用一块所谓的map,不是STLmap容器,作为主控。
map是一小块连续空间,其中每个元素都是一个指针,指向另一段(较大的)连续线性空间,称为缓冲区。缓冲区才是deque的存储空间主体。

在这里插入图片描述


map在deque中的定义代码如下:

template <class T, class Alloc=alloc, size_t BufSize = 0>
class deque
{
    
    
public:
	typedef T value_type;
	typedef value_type* pointer;
...
protected:
	// 元素的指针的指针
	typedef pointer* map_pointer;
protected:
	map_pointer map;
	size_type map_size; 	// map内可容纳多少指针
};

SGI STL允许指定缓冲区大小,默认值为0表示使用512 bytes缓冲区。map的类型是T**,元素是一个指针,所指之物是型别为T的一块空间。

deque的迭代器

deque采用的是分段连续空间,而表面维持“整体连续”,是通过迭代器的operator--operator++两个操作实现。

  • 以内嵌型别定义nested typedef方式定义五种常见的型别信息
template <class T, class Ref, class Ptr, size_t BufSize>
struct __deque_iterator{
    
    	// 未继承std::iterator
  // 未继承std::iterator,所以自行撰写五个必要的迭代器相应类别
  typedef random_access_iterator_tag iterator_category;   // 1
  typedef _Tp value_type;                                 // 2
  typedef _Ptr pointer;                                   // 3
  typedef _Ref reference;                                 // 4
  typedef ptrdiff_t difference_type;                      // 5
...
};
  • 迭代器本质是一个smart pointer,由于需要能够指出分段连续扣减在哪里, 还需要判断自己是否处于其所在缓冲区的边缘,deque迭代器中包含了三个型别指针对象curfirstlast。此外还需要能准确地在上一个或下一个缓冲区之间跳跃,因此,包含了管控中map
typedef T** map_pointer;
// 保持与容器的联结
T* cur;
T* first;
T* last;
map_pointer node;
...

deque的中控器、缓冲区、迭代器的相互关系如下:

在这里插入图片描述

其中,迭代器中还提供了一个决定缓冲区大小的函数buffer_size(),返回的结果指的是一个缓冲区能存放多少个元素,内部通过调用__deque_buf_size()全局函数完成,代码如下:

static size_t _S_buffer_size() {
    
     return __deque_buf_size(sizeof(_Tp)); }
// 如果 n 不为 0,传回 n,表示 buffer size由使用者自定。
// 如果 n 为 0,表示 buffer size使用默认值,那么
//     如果sz(元素大小,sizeof(value_type))小于 512,传回 512/sz,
//     如果sz不小于 512,传回 1。
inline size_t __deque_buf_size(size_t __size) {
    
    
  return __size < 512 ? size_t(512 / __size) : size_t(1);
}

运算符重载

deque的迭代器需要在不同缓冲区之间进行跳转,因此,对各种指针运算进行重载操作十分必要。

  • 跳转一个缓冲区,调整迭代器对应的firstlast指针
void set_node (map_pointer new_node)
{
    
    
    node = new_node;
    first = *new_node;
    last = first + difference_type(buffer_size());
}
  • dereference和成员访问
reference operator*() const {
    
    return *cur; }
pointer operator->() const {
    
    return &(operator*()); }
  • 减号运算符
difference_type operator-(const self& x) const
{
    
    
    return difference_type(buffer_size()) * (node - x.node - 1) +
            (cur - first) + (x.last - x.cur);
}
  • 自增和自减
self& operator++()
{
    
    
    ++cur;			// 切换至下一个元素
    if(cur == last )	// 如果已经到达缓冲区的尾端
    {
    
    
        set_node(node + 1);  // 就切换至下一节点
        cur = first;			// 第一个元素
    }
    return *this;
}
slef& operator--()
{
    
    
    if(cur == first) {
    
    	// 如果已经到达缓冲区的头端
        set_node(node - 1);
        cur = last;
    }
    --cur;
    return *this;
}
  • 随机存取
self& operator+=(difference_type n){
    
    
    difference_type offset = n + (cur - first);
    if(offset >= 0 && offset < difference_type(buffer_size()))
        // 目标位置在同一缓冲区内
        cur += n;
	else {
    
    
        // 不在同一个缓冲区内
        difference_type node_offset = 
        offset > 0 ? offset / difference_type(buffer_size())
        : -difference_type((-offset - 1) / buffer_size()) - 1;
        // 切换至正确的节点
        set_node(node + node_offset);
        // 切换到正确的位置
        cur = first + (offset - node_offset * difference_type(buffer_size());
    }
    return *this;
}
self& operator-=(difference_type n) {
    
     return *this += -n; }

// 实现随机存取
reference operator[] (difference_type n) const {
    
    return *(*this + n);}

deque的数据结构

deque中的成员除了一个指向map的指针之外,还需要维护两个迭代器start,finish,分别指向第一个缓冲区的第一个元素和最后缓冲区的最后一个元素。此外还需要记住目前map的大小,当map提供的节点不足时,必须重新配置更大的一块map。

template <class T, class Alloc=alloc, size_t BufSize = 0>
class deque
{
    
    
public:
	typedef T value_type;
	typedef value_type* pointer;
...
protected:
	// 元素的指针的指针
	typedef pointer* map_pointer;
protected:
	map_pointer map;
	size_type map_size; 	// map内可容纳多少指针
	iterator start ;
	iterator finish;
...
};

在这里插入图片描述


能很轻易得到下面这些函数的实现:

public:
	iterator begin() {
    
    return start;}
	iterator end() 	 {
    
    return finish;}
	reference operator[](size_type n){
    
     return start[difference_type(n)];}
	reference front() {
    
    return *start; }
	reference back() {
    
    
        iterator tmp = finish;
        --tmp;
        return *tmp;
    }
	size_type size() cosnt {
    
    return finish - start;}
	size_type max_size() const {
    
     return size_type(-1); }
	bool empty() const {
    
    return finish == start; }

deque的构造与内存管理

如下代码所示,设置缓冲区大小为8,即可以保存8个元素,并令其保留20个元素空间,每个元素初值为9。

deque<int, alloc, 8> ideq(20, 9);

上述deque对象配置完内存和初始化之后,整体的示意图如下:

在这里插入图片描述


deque自行定义了两个专属的空间配置器:

protected:
	//专属之空间配置器, 每次配置一个元素大小
	typedef simple_alloc<value_type, Alloc> data_allocator;
	// 专属之空间配置器, 每次配置一个指针大小
	typedef simple_alloc<pointer, Alloc> map_allocator;

deque的一个constructor的定义如下:

deque(int n, const value_type& value)
	: start (), finish (), map(0), map_size(0)
{
    
    
    fill_initialize(n, value);
}

内部所调用的fill_initialize()负责产生并安排好deque的结构,并将元素的初值设定妥当。

添加元素

通过在尾端继续插入三个新元素,此时,由于最后一个缓冲区有4个备用元素空间,所有不会引起缓冲区的再配置,结构如下图所示:

for(int i=0; i<3; i++)
	ideq.push_back(i);

在这里插入图片描述


push_back()函数内容如下:

public:
void push_back(const value_type& t)
{
    
    
    if(finish.cur != finish.last - 1)
    {
    
    
        // 最后缓冲区尚存在两个以上的元素备用空间
        construct(finish.cur, t); // 直接在备用空间构造元素
        ++finish.cur; // 调整最后缓冲区的使用状态
    }
    else	// 最后缓冲区只剩下一个元素备用空间
        push_back_aux(t);
}

之后,再在尾端添加一个新元素,此时由于最后的缓冲区只有一个元素备用空间,则会调用push_back_aux,先配置一整块新的缓冲区,再设置新元素内容,然后更改迭代器finish的状态,新的结构如下:

在这里插入图片描述

同样,通过调用push_front()在前端插入一个元素,首先判断第一缓冲区是否存在备用空间,如果没有则调用push_front_aux()函数再配置一块新缓冲区,设定新元素,修改迭代start的状态,新的结构如下图所示:

在这里插入图片描述

调整map

deque中,什么时候需要调整map,由reserve_map_at_back()和reserve_map_at_front()进行判断,实际操作则是由reallocate_map()执行。

// 如果 map 尾端的节点备用空间不足,符合条件就配置一个新的map(配置更大的,拷贝原来的,释放原来的)
void reserve_map_at_back (size_type nodes_to_add = 1) {
    
    
  if (nodes_to_add + 1 > map_size - (finish.node - map))
    reallocate_map(nodes_to_add, false);
}
// 如果 map 前端的节点备用空间不足,符合条件就配置一个新的map(配置更大的,拷贝原来的,释放原来的)
void reserve_map_at_front (size_type nodes_to_add = 1) {
    
    
  if (nodes_to_add > start.node - map)
    reallocate_map(nodes_to_add, true);
}

删除元素

deque中,使用pop_front()pop_back()可以从首端或者尾端删除元素。当一个缓冲区中没有元素时,还需要缓冲区释放掉,代码如下:

void pop_back()
{
    
    
    if(finish.cur != finish.first){
    
    
        --finish.cur;			// 调整指针,相当于删除了最后元素
        destroy(finish.cur)// 析构最后的元素
    }
    else
        pop_back_aux();		
}
void pop_back_aux() {
    
    
    deallocate_node(finish.first) ;  // 是否最后一个缓冲区
    finish.set_node(finish.node - 1); //调整迭代器finish的状态
    finish.cur = finish.last - 1; 
    destroy(finish.cur)	;        // 将该元素析构
}

clear()函数,用来清除整个deque。需要注意的是,clear()完成之后,deque还原成最初状态只有一个缓冲区。

void clear()
{
    
    
    // 对头尾以外的每个缓冲区的元素都析构,且释放缓冲区的内存
    for(map_pointer node=start.node + 1; node < finish.node; ++node)
    {
    
    
        destroy(*node, *node + buffer_size();
        // 释放缓冲区内存
        data_allocator::deallocate(*node, buffer_size());
    }
    if(start.node != finish.node){
    
    	// 至少有头尾两个缓冲区
        destroy(start.cur, start.last);
        destroy(finish.first, finish.cur);
        // 释放尾端缓冲区
        data_allocator::deallocate(finish.first, buffer_size());
    }
    else
        destroy(start.cur, finish.cur);
    finish = start; // 调整状态
}

erase()函数为了提高效率,会根据删除位置距离start近还是finish近来决定移动。

public:                         // erase
  iterator erase(iterator pos) {
    
    
    iterator next = pos;
    ++next;
    difference_type index = pos - start;
      // 删除的地方是中间偏前, 移动前面的元素
    if (index < (size() >> 1)) {
    
    
      copy_backward(start, pos, next);
      pop_front();
    }
      // 删除的地方是中间偏后, 移动后面的元素
    else {
    
    
      copy(next, finish, pos);
      pop_back();
    }
    return start + index;
  }
 // 范围删除, 实际也是调用上面的erase函数.
  iterator erase(iterator first, iterator last);

同样的,insert()函数也会根据插入位置距离首尾的大小,来移动不同的数据内容,提高效率。

猜你喜欢

转载自blog.csdn.net/hello_dear_you/article/details/128472201