C++ STL源码剖析 笔记

写在前面

记录一下《C++ STL源码剖析》中的要点。

一、STL六大组件

  1. 容器(container):
    • 各种数据结构,用于存放数据;
    • class template 类泛型;
    • vector, list, deque, set, map
  2. 算法(algorithm):
    • function template 函数泛型;
    • sort, search, copy, erase
  3. 迭代器(iterator):
    • 容器和算法之间的胶合剂;
    • pointer template 指针泛型;
    • *, ->, ++, --
  4. 仿函数(functor):
    • 类似函数,可以作为算法的某种策略;
    • operator()的template 小括号泛型;
  5. 配接器(adapter):
    • 修饰容器、仿函数、迭代器的接口;
    • queue, stack
  6. 空间配置器(allocator):
    • 负责空间配置和管理;
    • 实现了空间管理的template 空间管理类泛型;

二、空间配置器

这里是指SGI版本的STL实现,即GCC中的STL实现。

2.1 一般对象的申请和释放过程

2.1.1 新建一个对象

Foo *pf = new Foo;
  • (1) 调用了operator new配置内存;
  • (2) 调用了Foo::Foo()构建对象内容;

2.1.2 删除一个对象:

delete pf;
  • (1) 调用Foo::~Foo()将对象析构;
  • (2) 调用operator delete释放内存;

2.2 STL中对象的内存申请和释放

头文件

主要涉及如下四个函数:

  1. construct()函数;
  2. destroy()函数;
  3. allocate()函数;
  4. deallocate()函数;

2.2.1 construct()函数

construct函数

  • 直接调用new,同时完成空间的分配和根据值对对象的构造;

2.2.2 destroy()函数:

  • 有两种方式:

  • 第一种是只传入一个指针,则删除这个指针;
    删除指针

  • 第二种是传入一头一尾两个迭代器,则删除这两个迭代器之间的元素;
    删除一系列元素

  • 删除的元素范围是[first, last)

  • 但也不是范围内的元素都会被删除,删除之前还要看一下这些元素的析构函数是不是无所谓的trivial destructor),如果是无所谓的就不逐个执行,这样可以提高运行效率;

以上两个函数执行的逻辑示意图如下:
示意图

2.2.3 allocate()函数

  • SGI的STL版本将内存的分配和对象的构造两个步骤拆分开来了,其实是为了能够在更细的粒度上管理内存空间;

  • allocate()函数只负责内存的分配环节,设计的动机如下:

    • 向system heap要求空间;
    • 考虑多线程状态;
    • 考虑内存不足时的应变措施;
    • 考虑过多“小型区块”可能造成的内存碎片(fragment)问题;
  • 为此,SGI设计了双层级配置器,避免空间分配过程中可能出现的大量内存破碎问题。

  • 双层级配置器的结构示意图如下:
    双层级配置器作用

  • 第一层配置器的要点如下:

    • malloc(), free()realloc()等C风格的函数执行实际的内存管理,而不是直接使用new
      allocate()函数

    • 如果allocate()reallocate()malloc()realloc()申请空间时内存不足,则调用oom_malloc()oom_realloc(),来不断循环尝试用某个预先写好的“内存不足处理例程”获取足够的内存;
      reallocate()函数

    • 但如果“内存不足处理例程”并没有实现,则会直接抛出异常或者中止程序;

  • 第二级配置器要点如下:

    • 只处理申请128 bytes及以下空间的操作;

    • 维护16个free-lists,每个list空间大小分别8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128 (16*8);

    • 如果申请的空间不是8的倍数,则会自动将申请空间上调到8的倍数,这是对应了free-lists的空间;

    • 节点为union类型,结构如下:
      节点类型
      节点空间示意图

    • (1) 如果free-lists中有空间,则将这个空间返回,如下图:
      有空间

    • (2) 如果free-lists中对应大小的区块不足,则用refill()函数向内存池中获取20个(默认)新区块,再返回其中一个区块; refill()是通过调用chunk_alloc()函数从内存池中获取新区块的;
      取足量的区块

    • (3) 如果内存池中不足20个新区块,则chunk_alloc()函数会取尽量多的新区块;
      取尽量多的区块

    • (4) 如果内存池中连1个新区块都凑不齐,则将内存池中剩余的空间组成一个区块(大小不一定对应8的倍数),放入free-lists中;

    • 此时start_free是内存池空余内存的起始地址,my_free_listfree-lists数组中某个节点的地址,以start_free为起点的空间不足my_free_list所代表的区块list的常规空间大小,但仍放入其中,避免浪费空间;
      不足1个区块

    • 然后还要向heap空间申请空间补充内存池,申请空间使用malloc函数;
      补充内存池

    • (5) 如果heap空间不足,则反过来再查free-lists是否还有空间,查询从当前的free-lists[size]开始,一直查到free-lists[15]

    • 如果有空闲的区块,则把这个区块从free-lists取出并加入内存池,然后重新分配到free-lists中;
      heap空间不足

    • 如果free-lists中也没有更大的区块可以用了,则调用第一级配置器,从而抛出异常;

    • 调用第一级配置器主要是想通过oom_malloc()oom_realloc来尝试获得内存;
      调用第一级配置器

  • 总之,通过划分两级配置器,可以尽最大的努力获得空间的最大利用率;

2.2.4 deallocate()函数

  • allocate()函数一样,deallocate()函数的实现也是依赖于两级配置器;
  • 第一层配置器的要点如下:
    • 如果释放的空间超过128 bytes,则调用第一级配置器释放;
    • 释放空间直接使用free()函数;
      第一层配置器回收
  • 第二层配置器的要点如下:
    • 释放的空间少于128 bytes,则回收到free-lists中;
      第二层配置器回收
    • 回收的过程如下图:
      回收空间示意图

最后是以上两个函数加上内存池的完整示意图:
完整的二级配置器

2.3 内存构造函数

按照前面的逻辑,SGI将对象的空间分配和对象的值构造两者分开了,因此在allocate()函数的基础上,还需要有一些配套的函数来实现对象值的构造,这些函数是:

  1. uninitialized_copy()函数;
  2. uninitialized_fill()函数;
  3. uninitialized_fill_n()函数;

2.3.1 uninitialized_copy()函数

  • 用于给区间中的各个内存空间依次拷贝对象的值;
  • 遵循commit or rollback原则,即要么全部拷贝成功,要么全部不要拷贝;
  • 调用的是construct()函数;
    uninitialized_copy
  • [result, result+(last-first))是输出的范围,也是要复制的空间;
  • i遍历输入的序列,也就是要复制的原始对象;
  • 然后调用construct(&*(result+(i-first)), *i)执行复制;

2.3.2 uninitialized_fill()函数

  • 用于将传入的参数填充到区间中的各个内存空间上;
  • 遵循commit or rollback原则,即要么全部拷贝成功,要么全部不要拷贝;
  • 调用的是construct()函数;
    unintialized_fill
  • [first, last)是要填充的范围;
  • i遍历输出的序列,也就是要填充的对象;
  • 然后调用construct(&*i, x)执行填充;

2.3.3 uninitialized_fill_n()函数;

  • 用于将连续n个元素填充传入的参数;
  • 遵循commit or rollback原则,即要么全部拷贝成功,要么全部不要拷贝;
  • 调用的是construct()函数;
    unintialized_fill_n()
  • [first, first+n)是要填充的范围;
  • i遍历输出的序列,也就是要填充的对象;
  • 然后调用construct(&*i, x)执行填充;

三、迭代器

作用是提供一种方法,使它能够依次遍历某个容器所含的各个元素,又不用暴露该容器的内部细节。

3.1 auto_ptr

  • 迭代器是一种行为类似指针的对象;
  • 最重要的是对operator*operator->进行重载;
  • 因此首先介绍用于包装原生指针的auto_ptr对象;
  • 迭代器虽然设计是为了为STL容器封装指针功能,但也应当能够兼容原生指针;
  • 相当于是在auto_ptr的基础上扩展,从封装原生指针扩展到为不同容器封装指针功能;

3.1.1 auto_ptr的使用

  • 声明如下:
auto_ptr<指向对象的类型> ptr_name(new 指向的对象);
  • 使用的方法和原生指针完全一致;
    auto_ptr

3.1.2 auto_ptr的实现

  • 成员变量、构造函数和析构函数:
template <class _Tp> class auto_ptr {
    
    
private:
  // 成员变量,即原生指针
  _Tp* _M_ptr;

public:
  // 绑定类型
  typedef _Tp element_type;

  // 构造函数 [1] => 初始化为空指针
  explicit auto_ptr(_Tp* __p = 0) __STL_NOTHROW : _M_ptr(__p) {
    
    }
  // 构造函数 [2] => 初始化为一个现有的指针
  auto_ptr(auto_ptr& __a) __STL_NOTHROW : _M_ptr(__a.release()) {
    
    }

  // 构造函数 [3] => 初始化为一个现有的泛型指针
#ifdef __STL_MEMBER_TEMPLATES
  template <class _Tp1> auto_ptr(auto_ptr<_Tp1>& __a) __STL_NOTHROW
    : _M_ptr(__a.release()) {
    
    }
#endif /* __STL_MEMBER_TEMPLATES */

  // 析构函数
  // 删除指针指向的内存空间
  ~auto_ptr() {
    
     delete _M_ptr; }
  
  // ...
};
  • 重载operator=
  // 重载operator= [1] => 复制一个现有的指针
  auto_ptr& operator=(auto_ptr& __a) __STL_NOTHROW {
    
    
    if (&__a != this) {
    
    
      delete _M_ptr;
      _M_ptr = __a.release();
    }
    return *this;
  }

  // 重载operator= [2] => 复制一个现有的泛型指针
#ifdef __STL_MEMBER_TEMPLATES
  template <class _Tp1>
  auto_ptr& operator=(auto_ptr<_Tp1>& __a) __STL_NOTHROW {
    
    
    if (__a.get() != this->get()) {
    
    
      delete _M_ptr;
      _M_ptr = __a.release();
    }
    return *this;
  }
#endif /* __STL_MEMBER_TEMPLATES */
  • 重载operator*
  // 取自身指针指向内存地址的值
  _Tp& operator*() const __STL_NOTHROW {
    
    
    return *_M_ptr;
  }
  • 重载operator->
  // 返回自身指针
  _Tp* operator->() const __STL_NOTHROW {
    
    
    return _M_ptr;
  }
  _Tp* get() const __STL_NOTHROW {
    
    
    return _M_ptr;
  }
  • 实现releasereset函数:
  // 把自身指针置空,同时返回自身指针
  _Tp* release() __STL_NOTHROW {
    
    
    _Tp* __tmp = _M_ptr;
    _M_ptr = 0;
    return __tmp;
  }
  // 修改指针的指向
  void reset(_Tp* __p = 0) __STL_NOTHROW {
    
    
    if (__p != _M_ptr) {
    
    
      delete _M_ptr;
      _M_ptr = __p;
    }
  }
  • 以上操作只有在取值operator*析构的时候才会操作指针指向内存地址的值,其他操作均是操作指针本身的值;

3.2 迭代器需要实现和重载的操作

  1. 构造函数;

    • 指针赋值;
    • 因为指针指向的空间并非由指针自动创建,故无需分配空间;
    • 由于无需回收空间,故也无需实现析构函数;
      构造函数
  2. operator*()函数;

    • 取指针指向的空间的值;
    • 当返回值的类型是Item&即引用时,该返回值可以充当左值和右值
    • 当返回值的类型是Item时,该返回值只能充当右值,返回时是值复制返回,会产生临时变量;
    • 左值是*ptr = xxx;的形式,右值是xxx = *ptr的形式;
      取值
  3. operator->()函数;

    • 取指针本身的值;
      取指针值
  4. operator++()函数;

    • 包括前置++和后置++两种;
      ++的实现
  5. operator==()函数和operator!=()函数;

    • 判断指针本身是否相等,也就是说指向的空间地址是否相等,或者说指向的是不是同一个空间;
      判断指针相等

3.3 迭代器相应类型

  • 目的是让迭代器在泛型实现的具体运行过程中能够获知泛型具体的类型;
  • 迭代器是由以泛型实现的不同容器提供的,用以处理泛型对应类型的数据的类似指针的对象;
  • 因此通过迭代器起码要获得两个信息:
    • 迭代器是什么类型的容器提供的;
    • 迭代器指向的容器Item中存放的数据是什么类型的;
  • 这些信息在迭代器的处理过程中都有可能用到,或者需要迭代器向外提供给算法使用,也就是充当容器算法之间的桥梁;
  • 一般而言,要获取的信息如下:
    • value_type
    • difference_type
    • pointer
    • reference
    • iterator_category
  • 这五种信息中,仅iterator_category是和容器类型相关,其他四种信息均用于表明迭代器指向的空间所存放的数据的类型信息;

3.3.1 Traits编程思想

  • Traits即特性萃取机,用于获取迭代器的特性,也就是迭代器的相应类型信息;
  • 它出现的目的是为了兼容迭代器对象和原生C风格类型
    • 对于迭代器对象,可以执行迭代器的信息获取方式(C++风格);
    • 对于原生指针,需要手动为指针指派信息(C风格);
    • 除了原生指针,可能还会有别的情况,而Traits也需要兼容(其他C风格);
  • 实现Traits的方式是partial specialization偏特化
    • 即针对任何template参数更进一步的条件限制所设计出来的一个特化版本;
    • 实现的方式是在泛型的类class或者结构struct后面限定泛型的形式,从而进一步区分处理;
      偏特化

3.3.2 泛型的类型声明

  • 目的是通过解析迭代器指向的泛型,获取和泛型相关的类型信息,供自身的函数或者外界的函数进一步调用;
  • 算法如果要调用泛型实现的容器数据,就必须要得知泛型相关的信息,由于迭代器充当了算法访问泛型的中间工具,所以迭代器就要负责获取这些信息;
  1. 声明参数类型
  • 将依赖泛型的类型直接绑定到别名上;
  • 对应容器的泛型类型绑定;
/*第一种用法:只能获取泛型本身的信息*/
template<class T>
struct MyIter {
    
    
	typedef T value_type;  // 将泛型类型直接绑定到别名
	
	T* ptr;
	MyIter(T* p=0):ptr(p) {
    
    }
	// ...
};

/*第二种用法:可以通过取泛型的成员变量获取更多的信息*/
template<class T>
struct MyIter {
    
    
	typedef typename T::value_type value_type;  // 将依赖于泛型的类型绑定到别名
	
	T* ptr;
	MyIter(T* p=0):ptr(p) {
    
    }
	// ...
};
  1. 声明返回值类型
  • 声明返回值为依赖泛型的类型;
  • 对应函数的泛型类型绑定;
/*第一种用法:只能返回泛型本身的类型*/
template<class I>
I func(I ite) {
    
    
	return ite;
}

/*第二种用法:可以通过取泛型的成员变量以返回更多的类型*/
template<class I>
typename I::value_type func(I ite) {
    
    
	return *ite;
}

3.3.3 Traits实现

  • 实现Traits用到了上面讨论的两种方法:
    • 兼容C++风格对象(迭代器对象)和C风格类型(原生指针)是通过偏特化实现;
    • 提取类型信息是通过泛型编程的类型声明实现的;
  • 实现相当于是封装了一个iterator_traits泛型来进行萃取的功能;
  • 常用的需要按照Traits萃取的信息如下:
  1. value_tyoe
  • 迭代器所指对象(空间)的类型
    • (1) 容器的迭代器:直接调用容器/类对象的成员变量即可;
      容器的迭代器

    • (2) 原生指针:用偏特化萃取;
      原生指针

    • (3) 原生常量指针:用偏特化萃取;
      原生常量指针

  1. difference_type
  • 两个迭代器之间的距离,也是用来表示一个容器的最大容量的类型;
    • (1) 容器的迭代器:直接调用容器/类对象的成员变量即可;
      迭代器

    • (2) 原生指针:用偏特化萃取;
      原生指针

  1. reference
  • 迭代器所指对象(空间)的引用类型
  • reference_type作为返回值类型,可以做左值和右值
  • value_type作为返回值类型是值传递,需要进行对象复制,产生临时变量,只能做右值
  • Traits实现在下面一小节;
  1. pointer
  • 迭代器的指针类型
    • (1) 容器的迭代器:直接调用容器/类对象的成员变量即可;
      容器的迭代器

    • (2) 原生指针:用偏特化萃取;
      原生指针

    • (3) 原生常量指针:用偏特化萃取;
      原生常量指针

  1. iterator_category
  • 对应的容器所属类型

  • 是对迭代器对应容器的存储方式的说明;

  • 目的是增强算法的效率,因为算法可以根据迭代器的类型来进行不同的数据访问操作实现;

  • 总共有5种类型;

    • input_iterator
      • 迭代器指向的对象是只读;
    • output_iterator
      • 迭代器指向的对象是只写;
    • forward_iterator
      • 迭代器指向的对象可读可写;
    • bidirectional_iterator
      • 迭代器指向的对象可读可写且可以双向移动,但只能顺次移动;
    • random_access_iterator
      • 迭代器指向的对象可读可写,可以双向移动,且可以随机移动;
  • 这5种类型的关系如下:
    五种类型的关系

  • 高层级的类型是对低层级类型的特殊化,高层级类型一定是低层级类型;高层级类型的泛化能力下降,但是效率会提高,所以能够用高层级类型就一定会用高层级类型;

  • 因为5种类型之间有泛化和特殊化的关系,所以定义的时候用了继承,如下:

五种类型的定义struct

  • 继承实现的好处在于,只要实现了基类的处理方法,如果高层级所对应的方法找不到,会自动调用低层级的方法,这样越高层级的可兜底的方法就越多,虽然使用兜底的方法会降低了调用时的效率,但大大提高了调用时的容错率;

  • 迭代器中的Traits实现如下:

    • (1) 容器的迭代器:直接调用容器/类对象的成员变量即可;
      容器的迭代器

    • (2) 原生指针:用偏特化萃取;
      原生指针

    • (3) 原生常量指针:用偏特化萃取;
      原生常量指针

  • 使用iterator_category的一个例子如下,这个例子实现了advance(),用于迭代器的前后移动,针对不同类型的迭代器,实现了不同的泛型方法;
    advance实现
    advance实现

  • advance()上层对外接口实现如下,通过iterator_category调用上面实现的不同泛型方法;

advance上层接口

  • 注意,上面...class InputIterator, ...中的InputIterator是遵循了STL算法的命名规则的写法,即以算法所能接受的最初级类型来为其迭代器类型参数命名;

3.4 使用方式

  • 迭代器和容器搭配使用的方式如下:
    迭代器使用方式

  • 定义的方式为xxx<xxx>::iterator it,需要说明容器类的具体类型;

  • xxx.begin()xxx.end()返回的是迭代器类型;

  • 通常是作为STL的算法调用参数使用;

四、序列式容器

4.1 vector

  • 相当于是动态数组,分配和维护的是连续线性空间
  • 可以随机访问其中的元素;

4.1.1 数据结构

  • vector的数据结构如下:
    数据结构

  • 基于三个迭代器的一些获取信息函数实现如下:
    获取信息实现

  • 三个迭代器在vector中的分布如下:
    迭代器分布

  • start指向vector首元素

  • finish指向vector尾元素之后的第一个元素,即size()返回容量外的第一个元素,相当于是已用空间的边界(不含)

  • end_of_storage指向capacity()返回最大容量后的第一个元素,相当于是最大可用空间边界(不含)

4.1.2 迭代器类型

  • vector的迭代器用类型对应的普通指针即可,定义如下:
    迭代器定义
  • 例如,vector<int>::iterator的本质是int *
  • 迭代器类型是 random_access_iterator,参看三、3.3.3部分;

4.1.3 内存分配和管理

  1. 构造函数
  • 先分配空间,然后按照初值调用uninitialized_fill_n函数填充值;
  • 最后将三个迭代器放到合适的位置;
  • 注意此时的capacity()就是申请的n,和finish指向相同,并没有自动增加可用空间;
    构造函数
  1. push_back()函数
  • 用于在vector的末尾增加一个元素;
  • 插入的过程如下:
    • 如果空间够,则直接在finish处插入元素,同时++finish
    • 如果空间不够,则申请原来的两倍空间,把原来的startfinishfinishend_of_storage的元素都复制到新空间中;
    • 如果原来的空间为0,则新申请的空间是1而不是两倍;
    • 注意,此时三个迭代器都会重新指向新空间,因此原来的所有迭代器指向均失效
    • 最后还要释放原来的空间;
  • 为什么空间是原来两倍就一定能装下呢?
  • 主要是因为push_back()每次仅增加一个元素;

push_back函数实现
函数实现

  • 扩增空间的过程可以总结为:重新配置、移动数据、释放原空间
  • 因此使用push_back()函数的时候一定要注意原来的迭代器的指向问题

4.1.4 一些常用函数

  1. pop_back()函数
  • 用于把尾端的元素去掉;
  • 同时调整finish的位置;
    函数实现
  1. erase()函数
  • 用于清除某个位置的元素;

函数实现

  • 或者用于清除某个区间上的元素;

函数实现
函数实现

  • 均是用copy()将后面的元素整体迁移覆盖要擦除的元素,同时移动finish指针;
  • 覆盖的示意图如下:
    erase示意图
  1. insert()函数
  • 用于插入一段序列;
  • 如果插入点后的元素个数【3】大于新增元素【2】,则:
    • (1) 将finish前的新增元素个数【2】的元素挪到最后面,同时finish后移;
    • (2) 将剩下的position后面的元素【1】往后压,空出位置给插入的元素;
    • (3) 将插入的元素填充到空开的位置中;

插入过程

  • 如果插入点后的元素个数【2】小于等于新增元素【3】,则:
    • (1) 先用插入元素填充finish后的元素,让它的个数等于新增元素个数【3】,同时finish后移;
    • (2) 将position后的元素【2】挪到最后面,空出位置给剩下的要插入元素,同时finish后移;
    • (3) 将插入的元素填充到空开的位置中;

插入过程

  • 如果finish后的备用空间【2】小于插入元素的空间【3】,则:
    • (1) 先扩展空间,令其是原来的两倍,或者是增加插入元素所需的空间,因为有可能两倍还是放不下的;
    • (2) 然后再分段将原空间的元素拷贝到新空间;
    • (3) 先拷贝插入前的元素,再填充要插入的元素,最后拷贝插入后的元素;

插入过程

  • 其实之所以要把这个过程弄得这么复杂,主要是为了:
    • 充分利用finish指针;
    • 充分利用已有的fill()函数和copy()函数;
    • 未赋初值的空间单独处理,已赋初值的空间覆盖也单独处理;
    • 而且操作过程中vector赋值始终保持连续;
  • 实现的代码如下:
    函数实现

4.2 list

  • 相当于双向环状链表
  • 不可以随机访问,但插入删除操作时间复杂度低至O(1),而且省空间;

4.2.1 数据结构

  • (1) 每个节点的结构如下:
    节点结构

  • 是一个双向链表
    双向链表

  • (2) 整个双向链表的结构如下:
    list结构

  • 此时的list_node节点类型,link_type指向节点的指针类型;

  • 是一个环状双向链表:
    list形状

  • node指针指向的是环状双向链表的最后一个节点list.end()(不含),是一个空白节点,即伪头节点(亦是伪尾节点);

  • 注意:

    • 链表头front元素是node->next
    • 链表尾back元素是node->pre
    • 这两个元素均不是由node指针指向;

4.2.2 迭代器类型

  • 节点类型的指针的封装充当迭代器即可,即类型是link_type或者说是list_node*的封装;
  • 定义如下:

迭代器定义

  • 迭代器类型是bidirectional_iterator,可以双向移动,但不能进行随机读取

  • 增加元素、删除元素的操作不会令原来的迭代器失效

  • 赋值和取值函数重载
    函数实现

  • 自增和自减函数重载

  • self& operator++()相当于++i,先自增再返回;

  • self operator++(int)相当于i++,先返回再自增;
    函数实现

  • 其他的一些获取信息的函数如下:

函数实现
函数实现

4.2.3 内存分配和管理

  • 其实就是类似于双向链表的空间管理;
  • 4个基本的空间管理函数如下:
    空间管理函数
  1. 构造函数
  • 构建一个空白节点,这个节点将一直和node指针绑定,不会赋值;

构造函数

  • 初始的示意图如下:
    在这里插入图片描述
  1. push_back()函数
  • 用于在尾部插入一个节点;

函数实现

  • 用到的insert()函数实现如下:

  • 插入前是pre, positon, next,插入后是pre, tmp, position, next

  • 因此是在position之前插入;
    函数实现

  • 一个例子如下所示:
    在这里插入图片描述

4.2.4 一些常用函数

  1. 两个push函数

函数实现

  1. erase()函数

函数实现

  1. 两个pop函数

函数实现

  1. clear()函数
  • node->next出发,一直遍历并销毁到node
  • 最后仅保留node节点;

函数实现

  1. transfer()函数
  • 用于将某一段链表移动到position之前;

函数实现

  • 移动过程需要完成:
    • firstlast(不含)的链表和原链表断开;
    • 然后修复原链表;
    • 最后将firstlast(不含)的链表放到position之前;
  • 是一段比较复杂的指针操作;
  • 移动的示意图如下:
    示意图

4.3 deque

  • 相当于双向数组,是双向开口的连续线性空间;
  • 可以随机访问
  • 相比vector增加了双向空间增长和删除,相比list实现了随机访问,而且使用的是连续的空间,因此在实现上远比vector或者list复杂;
  • 是一种隐藏了底层细节的伪双向连续空间,在底层是由一段一段的定量连续空间(称为缓冲区)组成,增加映射控制(连续的指向缓冲区的指针数组)连接各个连续空间,从而维持了整体连续的假象;
    deque结构
  • 缓冲区的作用在于,当数据向两端生长但空间不够的时候,仅扩展指针数组即可(也就是申请新空间,将指针数组整体拷贝,再释放旧空间),而不需要移动所有实际的数据,这将极大提高空间扩展的效率;

4.3.1 数据结构

  • (1) 中央映射的数据结构
  • 中央映射充当中控器,负责串联各个缓冲区,结构定义变量map,如下:

中控器结构
中控器结构

  • 也就是定义一个指针数组,数组中的各个指针均指向一块缓冲区;

  • 然后用变量map指向这个指针数组的头节点;

  • 结构示意图如下:
    结构示意图

  • (2) 指向缓冲区的头尾指针:

  • 除了map之外,还需要用两个迭代器指向缓冲区头尾,分别是变量startfinish,如下:
    指向头尾缓冲区的指针

  • 迭代器的结构见下节;

4.3.2 迭代器类型

  • 用一个新的结构作为迭代器,其中封装了指向缓冲区关键位置的3个指针和指向中控器指针的指针;
  • 相当于是对指向缓冲区的指针的封装
  • 但实际指向的元素仅有一个,就是指缓冲区中cur指针指向的那个元素,是某个实际数据元素,而不是整个缓冲区;
  • 因此startfinish迭代器的含义是start.curfinish.cur,分别指向deque的头元素和尾元素;
  • 定义如下:

迭代器定义

  • 迭代器的结构示意图如下:
    迭代器结构示意图
  • 迭代器使用的一个例子如下:
  • 每个缓冲区可以放8个int类型,共用了三个缓冲区;
  • 有三个迭代器,分别负责三个缓冲区,其中一个是dequestart(起始点),一个是finish(终止点);
    迭代器例子
  • 注意:
    • map指针数组的缓冲区指针是有序的,即左侧指针指向的缓冲区一定在右侧指针指向的缓冲区前面;
    • map的缓冲区使用从数组中央开始,向两边展开使用,而不是从头开始使用;
    • 如果指针的使用超过了map数组的两头,则需要重新分配一个更大的map数组,并把原map拷贝过去;
    • 迭代器的cur指针是指向真正的当前访问元素
  • 核心:迭代器如何指向不同的缓冲区?
  • 是通过set_node()函数用于迭代器在不同的缓冲区之间跳转
  • set_node()函数是实现迭代器运算符重载的关键,它通过对map数组指针的遍历,实现跳转到下一指针指向的缓冲区(*new_node)的功能,其实现如下:

函数实现

  • 在此基础上,运算符的重载实现主要是,先判断所要处理的元素是在当前迭代器指向的缓冲区还是其他的缓冲区中,然后通过移动迭代器,用它的3个指针找到元素,并完成元素的处理;
  • 一些迭代器的运算符重载函数如下:

函数实现
函数实现
函数实现
函数实现

  • 一些获取信息的函数实现如下:

函数实现

4.3.3 内存分配和管理

  1. 构造函数
  • 构造函数如下:

函数实现

  • fill_initialize()函数用于分配空间并设置元素初值,包括:
    • deque申请空间;
    • 为每个节点的缓冲区中的元素设置初值,因为缓冲区才是真正存放数据的地方;

函数实现

  • create_map_and_nodes()函数用于为deque申请空间,包括:
    • 计算map数组大小,即需要节点数+2,最少为8;
    • 需要节点数用总元素除以每个缓冲区能放下的元素的结果上取整即可,刚好整除则仍+1;
    • 使用map数组中间的指针,然后为每个指针申请缓冲区内存;
    • 将计算得到的nstart指针和nfinish指针赋予迭代器startfinish

函数实现
函数实现

  1. push_back()函数
  • 如果finish所在的缓冲区仍有一个以上空间,则直接增加即可;
  • 否则,在增加元素后(此时缓冲区已满),还需要新开一个缓冲区,然后将finish迭代器跳到该缓冲区中;

函数实现
函数实现

  • 新开缓冲区的示意图如下,此时finish指向的缓冲区全空:
    在这里插入图片描述
  1. reserve_map_at_back()函数
  • 用来判断当前map数组是否需要扩增;
  • 如果需要增加的节点放不下了,就扩增,实现如下:

函数实现

  • reallocate_map()函数用于扩增:
    • 如果map数组比较空(因为是从中间向两边填充的,所以有可能出现仅用了一边的空间,导致此时map数组的节点利用率不高),则将已经使用的节点(即map数组中的指针元素)往数组中间挪动即可,无需申请新的空间;
    • 否则,申请一个新空间,将空间增加到原来的两倍或者增加所需的节点数(因为两倍可能还是放不下)再+2,将旧空间内容拷贝到新空间后释放旧空间;
    • 最后还要更新迭代器startfinish

函数实现

4.3.4 一些常用函数

  1. pop_back()函数
  • 用于移除末尾的元素;
  • 移除元素时,若finish.cur == finish.first,(也就是说移除之前缓冲区就为空),则需要消除缓冲区,并回退到上一个缓冲区,再进行元素的移除,此时移除的是上一个缓冲区的最后一个元素
  • 实现思路是和push_back()相吻合的,也就是说允许有完全空的缓冲区出现,而且两端无论何时都需要有空余的空间,实现如下:

函数实现

  1. clear()函数
  • 清空所有缓冲区,仅保留一个缓冲区(也就是起始条件中的start.node+1);
  • 实现如下:

函数实现

  1. erase()函数
  • 清除缓冲区中的某个元素
  • 也就是说清除的对象是实际的元素,由于是双向队列,因此清除之后要尽量让剩下的元素往数组中间靠;
  • 之所以能够用copy()copy_backward()函数来完成复杂的支离破碎的缓冲区之间的移动,是因为迭代器已经定义和实现好迭代器前后移动的代码了,因此在高层算法的使用上,deque和其他的容器无异;
  • 这里的pos实际上是指pos.cur指针所指的元素;
  • 实现如下:

函数实现

  1. insert()函数
  • 用于在position.cur处插入一个元素;
  • erase()函数一样,之所以不需要显式给出复杂的缓冲区之间的操作,是因为使用了copy()copy_backward()函数,因此在逻辑上可以完全按照完整的双向连续空间来处理,无需理会繁杂的中控器和缓冲区管理;

函数实现
函数实现

4.4 stack 【deque的adapter】

  • 相当于是仅尾端开口的deque
  • 不能随机访问;
  • 在逻辑上是单向开口的连续线性空间;
  • 没有迭代器
    示意图

4.4.1 数据结构

  • 在实现上完全是在deque的基础上进一步封装;
  • 并不是继承deque,而是拥有一个deque的成员变量,这样确实能够让这个封装关系不那么复杂而且合乎逻辑,因为从逻辑来说,stackdeque同级的而不是父子关系;
  • 仅对外暴露deque的某些功能,而且均进行了二次封装
  • 实现如下:

数据结构
数据结构

4.4.2 一些常用的函数

  • 一些常用的函数实现如下:

函数实现

4.5 queue 【deque的adapter】

  • 相当于是两端开口的deque,但数据只能从尾端入,从首端出;
  • 不能随机访问;
  • 在逻辑上是单向开口的连续线性空间;
  • 没有迭代器
    示意图

4.5.1 数据结构

  • 在实现上完全是在deque的基础上进一步封装;
  • 并不是继承deque,而是拥有一个deque的成员变量
  • 仅对外暴露deque的某些功能,而且均进行了二次封装
  • 实现如下:

数据结构

4.5.2 一些常用的函数

  • 一些常用的函数实现如下:

函数实现
函数实现

4.6 priority_queue 【vector的adapter】

  • 相当于是一个大顶堆
  • 底层是用vector来实现;
  • 没有迭代器;
  • 在实现的过程中使用了4个STL标准的大顶堆操作函数;
    示意图

4.6.1 数据结构

  • 堆的结构是一个完全二叉树;

  • 完全二叉树可以用数组来表示;

  • root节点是最值,且位于数组的第一个元素

  • 数组保存的顺序相当于是完成二叉树的广度优先遍历
    堆和数组之间的关系

  • 因此,priority_queue封装了一个vector成员变量,如下所示:

数据结构

4.6.2 和heap相关的4个标准函数

  • 这些STL标准函数均是针对最大堆来实现的;
  1. push_heap()函数
  • 要求原来是一个大顶堆;
  • 将处于vector末尾的新加入元素上浮,以满足最大堆的性质;
  • 根节点是first,新插入的元素在last

函数实现

  • 一个例子如下:
    例子
  1. pop_heap()函数
  • 要求原来是一个大顶堆;
  • 先把最大值换到vector的最尾端(并未取走);
  • 然后将新换上来的root下沉,直到满足大顶堆的性质;
  • 最后除vector最后一个元素外,其余元素满足大顶堆;
  • 实现如下:

函数实现
函数实现
函数实现

  • adjust_heap()函数用于将根节点下沉,以满足最大值性质;
  • 一个例子如下:
    例子
  1. sort_heap()函数
  • 实现堆排序
  • 要求原来的数组是一个大顶堆;
  • 其实就是多次执行pop_heap()函数,因为每次执行都会将最大值放到vector表示最大堆部分的末尾;
  • 实现如下:

函数实现

  1. make_heap()函数
  • 用于将一个vector变成符合大顶堆性质的完全二叉树;
  • 是从下往上建堆,第一个处理的元素是最后一个非叶节点,一直处理到根节点;
  • 对每个非叶节点,执行类似于pop_heap()节点下沉操作,使得以当前节点为根节点的子树满足大顶堆性质
  • 实现如下:

函数实现

4.6.3 一些常用的函数

  • priority_queue的函数实现基本就是直接封装和heap相关的函数,来实现一个完整的大顶堆的功能;
  • 其中,堆主要的函数是:
    • push()函数:往堆中添加元素,先用vector.push_back()往数组中添加元素,再调用的是push_heap()函数;
    • pop()函数:往堆中删除最大值元素,调用的是pop_heap()函数,然后再用vector.pop_back()弹出末尾的元素;
  • 常用的函数实现如下:

函数实现

4.7 forward_list

  • 相当于单向链表
  • 在SGI标准中,这种容器被命名为slist,这里的标题是采用了更为常用的C++11标准容器命名;
  • 由于只能单向遍历,因此操作最好发生在头节点附近,否则将需要从头遍历整个链表以到达操作点;

4.7.1 数据结构

  • (1) 首先是定义一个节点基本类型__slist_node_base,里面仅包含一个next指针,定义如下:

结构定义

  • (2) 然后是节点的数据结构
  • 继承于__slist_node_base类,也就是包含一个next指针;
  • 同时新增加一个数据变量;
  • 定义如下:

结构定义

  • 继承的示意图如下:
    继承示意图

  • (3) slist的数据结构

  • 整个单向链表的数据结构定义如下:

  • 仅需包含一个伪头节点head

结构定义

  • 基于上述数据结构定义的插入节点函数实现如下:
  • 插入前:->pre
  • 插入后:->pre->new

函数实现

  • 统计链表大小节点数量函数实现如下:

函数实现

4.7.2 迭代器类型

  • slist的迭代器可以看作是对指向节点的指针的指针,即指向__slist_node_base的指针;
  • 从这里就可以看出前面对节点结构的继承定义的好处,就是将节点指针的定义和节点的定义分开了,那么在别的地方就可以更加灵活地使用,解耦性更强,虽然会增加理解的难度;
  • 迭代器的定义也是用了继承的结构;
  • 结构定义:

结构定义

  • 迭代器示意图如下:
    迭代器示意图

  • 由于__slist_node__slist_node_base的子类,因此通过迭代器的node指针取值的时候需要将父类指针强制转换为子类指针,才能调用子类对象的成员变量;

  • 迭代器的一些函数实现如下:

函数实现

4.7.3 一些常用的函数

  • 正如前面提到的,单向链表通常只能操作头部的元素,处理其他元素的时间复杂度都太高了;
  • 因此功能函数只有3个,即取头部元素从头部插入元素从头部取走元素
  • 从功能上来说,其实是很适合用来做stack的底层容器的,相当于是一个倒转的stack
  • 一些常用的函数实现如下:

函数实现

  • 由于并没有伪尾节点,因此判断到达链表末尾的条件是将指针和空指针(0或者NULL)进行比较;
  • 所以end()函数实际上是返回了用空指针初始化的迭代器,指向空指针;

五、关联式容器

  • 关联式容器的特征是:每个元素都是一个键值对,包含keyvalue
  • 实现的底层技术有两种,一种是红黑树(Red-Black Tree),另一种是哈希表(Hash Table)
  • 红黑树具有对数平均时间,即O(logN),因为底层用的是指针形式的二叉搜索树;
  • 哈希表具有常数平均时间,即O(1),因为底层用的是可以随机访问的索引数组;
  • 从效率上看,哈希表的效率更高;

5.1 树的相关知识

  1. 二叉树
  • 特点
    • 任何节点最多只能有两个子节点;
  1. 二叉搜索树
  • 特点
    • 任何节点的值一定大于其左子树中节点的值,一定小于其右子树中节点的值;
  • 插入操作如下:

插入操作

  • 删除操作如下:

删除操作

  1. AVL树
  • 是平衡二叉搜索树;
  • 特点
    • 任何节点的左右子树高度相差最多1;
      AVL树
  1. 红黑树
  • 是平衡二叉搜索树;

  • 特点

    • 每个节点只能是红色或者黑色;
    • 根节点是黑色;
    • 红色节点的子节点一定是黑色;
    • 任一节点到NULL的路径上所含的黑色节点数量一定相同;
      红黑树
  • STL中实现了红黑树的完整容器,但并不暴露,而是作为其他关联式容器的底层容器;

  • 使用的是指针形式二叉树来实现红黑树;

  • 由于红黑树真的很复杂,无论是原理还是实现都很复杂(究极麻烦的分类讨论和指针操作),这里就无力再赘述了(>﹏<),可以参考原书中的内容;

5.2 set 【rb_tree的adapter】

  • 集合,所有元素会根据元素的键值自动排序
  • 不允许两个元素有相同的键值;
  • keyvalue是统一,就保存一个值,既是键(可被索引和排序)也是值(可被取出使用);

5.2.1 数据结构

  • 包含了一个re_tree的成员变量;
  • 数据结构定义如下:

数据结构
数据结构

5.2.2 迭代器类型

  • 只读类型的迭代器,直接使用了红黑树的迭代器类型;
  • 可以通过迭代器访问元素,但不能通过迭代器修改值;

5.2.3 一些常用的函数

  • 基本上就是调用红黑树的方法;
  • 实现如下:

函数实现
函数实现

  • find()函数是比较常用的函数:

函数实现
函数实现

5.3 map 【rb_tree的adapter】

  • 键值对映射,所有元素会根据元素的键值自动排序
  • 不允许两个元素有相同的键值;
  • keyvalue是分开的,键用于被索引和排序,值用于被取出使用;
  • pair数据结构实现键值对;

5.2.1 数据结构

  • (1) 键值对的数据结构
  • 包含两个成员变量,first作为键,second作为值;
  • 实现如下:

数据结构
数据结构

  • 包含了一个re_tree的成员变量;
  • 数据结构定义如下:

数据结构

5.2.2 迭代器类型

  • 迭代器不只是只读类型,限制比set容器的少;
  • 可以通过迭代器访问元素并修改value,但不能修改key

5.2.3 一些常用的函数

  • 一些常用的函数实现如下:

函数实现
函数实现
函数实现

  • 一些常用的map操作:

函数实现
函数实现

5.4 multiset和multimap【rb_tree的adapter】

  • 分别是在setmap的基础上增加了允许键值重复
  • 插入操作用的是红黑树实现的insert_equal()而不是insert_unique(),因此可以实现键值的重复;

5.5 哈希表的相关知识

  • 哈希表的原理其实就是用某种映射函数(hash function,散列函数),将key的大数映射成小数,然后就可以放到一个小索引里面了;
  • 小索引可以用数组实现,因而能够进行随机访问;
  • 可以提供常数时间的查找、插入和删除效率;
  • 哈希表在STL中有完整实现的容器hashtable,但并不向外暴露,而是作为其他关联式容器的底层容器使用;

5.5.1 哈希碰撞和5种处理碰撞的方法

  • 哈希碰撞:即有不同的元素(在这里指key)被映射到同一个索引数组位置上;
  • 解决哈希碰撞的方式主要如下:
  1. 线性探测
  • 插入策略:如果当前索引位置已经有key了,就从当前的位置开始,一直往下找,直到找到一个空闲的位置来放key
  • 也就是下一个查找的地址为:i = i + stepstep = step + 1
  • 这个查找空闲位置的过程是循环的,查到数组尾端之后,就转到头部继续查找,直到找到空位位置;
  • 一个例子如下:
    示意图
  • 查找策略:和插入策略类似,如果索引位置不是要找的key,则往下循环查找,直至找到;
  • 删除策略:必须采用惰性删除,也就是只标记删除,实际并未删除,要等到rehashing的时候再删除;
  • 一些讨论
    • 假设碰撞随机出现,则平均处理碰撞的时间是O(0.5N),因为可能要查找一半的数组,最坏的情况下是O(N);
    • 如果有连片的碰撞,也就是连续的索引空间被占据,则平均处理碰撞的时间还要比O(0.5N)要高,也就是主集团(primary clustering)问题
    • 最多能存储的元素数量不能超过索引数组的大小,不像红黑树那样可以动态无限增长;
    • 存储空间满了之后需要申请新空间并迁移数据,同时进行rehashing操作;
  1. 二次探测
  • 在线性探测的基础上解决主集团问题;
  • 插入策略:如果当前位置i已有元素发生碰撞,则探查的下一个位置不是i+1,而是i+1^2, i+2^2, i+3^2,直到找到空闲的位置;
  • 也就是下一个查找的地址为:i = i + step^2step = step + 1
  • 一个例子如下:
    示意图
  • 一些讨论
    • 如果负载系数在0.5以下(即索引数组只有少于一半被使用),且索引数组的大小是质数,则可以确定碰撞时探测的次数不多于2
    • 如果索引数组空间不够了,则需要将空间扩展到一个大约是两倍的质数,并且同时执行rehashing操作,和线性探测类似;
  1. 双重散列(double hashing)
  • 增加一个hash函数处理碰撞;
  • 处理碰撞的效果优于前两种方法;
  • 也就是下一个查找的地址为:i = i + step*hash(key)step = step + 1
  • 其中hash(key)是另外一个hash函数;
  1. 多哈希法
  • 就是用多个hash函数计算key对应的索引数组位置;
  • 插入策略:如果当前位置i已有元素发生碰撞,则用下一个hash函数计算位置,再探测是否有碰撞;
  • 如果hash函数设计得够好,或者数量够多,则总会找到一个空位不发生碰撞的;
  1. 开链法(separate chaining):
  • 前面的四种方式都是开放地址法,核心思路还是在碰撞时再找一个空位;
  • 因此受索引数组大小的限制,在空间不够的时候均需要扩增空间
  • 开链法不需要扩增空间,它使用的是数组和链表相结合的方式处理碰撞;
  • 其结构由一个桶数组和每个桶引出的节点链表组成,示意图如下:
    示意图
  • 具有动态扩充的能力;
  • 是STL中采用的处理碰撞方式;

5.5.2 哈希表数据结构

  • (1) 桶数组的底层是用vector实现;

  • (2) 链表用另外定义的__hashtable_node实现,如下:

数据结构
数据结构

  • 示意图如下:
    示意图

  • (3) 哈希表的数据结构

  • 就保存一个__hashtable_node指针类型的数组

  • 数组中的每个指针均作为桶的起始指针

  • 同时记录当前全部的节点数量

  • 实现如下:

数据结构

  • 关于桶数组大小的一些说明,仍用质数作为桶数组的大小:

桶数组大小的说明

5.5.3 哈希表的迭代器类型

  • forward_iterator类型,只能前进,不能后退;
  • 包括一个指向桶(同一索引下的链表)中的某个节点的指针cur指向整个哈希表对象的指针ht
  • 结构定义如下:

迭代器类型

  • 重载的一些函数实现如下:

函数实现
函数实现

  • 前置++和后置++函数的重载如下:
  • 如果当前桶还有元素,就通过链表的指针跳到下一个元素;
  • 否则,则从指针数组的当前桶指针bucket开始往后遍历,直到找到一个非空的桶,然后把cur指针指向这个桶链表的第一个节点

函数实现

  • bkt_num()函数用于计算某个key的哈希值并返回它在指针数组中的位置,实现如下:

函数实现
函数实现

5.5.4 哈希表的内存分配和管理

  • 初始的时候用一个质数初始化指针数组的大小;

  • 如果感觉指针数组不够,就会扩增指针数组的空间,过程仍然是:申请新空间,复制原有数据,释放旧空间

  • 判断指针数组不够的标准是:如果已放入哈希表中的元素个数大于指针数组的大小,则扩增指针数组;

  • 复制原有数据的操作如下:

    • 依次遍历旧指针数组中的每个桶;
    • 逐个处理桶中的链表节点,首先计算该节点应该放到新桶指针数组中的哪个桶,然后修改它的指针,把它放到新桶的第一个节点位置上;
    • 操作过程示意图如下:
      示意图
  • 其余的关于hashtable的详细定义这里就不再赘述了,实现是比较像deque的实现的;

  • 只不过hashtable引入了哈希函数作为指针数组的定位,而不是用下标定义,在实现上还比deque简单一点点,因为指向的区域是链表而不是固定连续的空间(即deque的缓冲区),可以动态增长;

  • hashtable在扩增指针数组的时候需要rehashing重新分配每个桶内的链表,而不仅仅是修改头部指针即可;

5.6 unordered_set 【hashtable的adapter】

  • set的功能基本相同;
  • 内部没有自动排序功能
  • 操作效率比set更高,可达常数时间复杂度;
  • 在SGI标准中,这种容器被命名为hash_set,这里的标题是采用了更为常用的C++11标准容器命名;

5.6.1 数据结构

  • 包含了一个hashtable的成员变量;
  • 结构定义如下:

数据结构

5.6.2 一些常用的函数

  • 一些常用的函数实现如下:
  • 基本上就是调用hashtable的函数;
  • 初始的桶指针数组大小默认初始传参为100,最近的质数为193,也就是说最多可以放193个元素到哈希表中,之后就需要扩增指针数组了;

函数实现

5.7 unordered_map 【hashtable的adapter】

  • map的功能基本相同;
  • 内部没有自动排序功能
  • 操作效率比map更高,可达常数时间复杂度;
  • 在SGI标准中,这种容器被命名为hash_map,这里的标题是采用了更为常用的C++11标准容器命名;

5.7.1 数据结构

  • 包含了一个hashtable的成员变量;
  • 结构定义如下:

数据结构

5.7.2 一些常用的函数

  • 一些常用的函数实现如下:
  • 基本上就是调用hashtable的函数;
  • 初始的桶指针数组大小默认初始传参为100,最近的质数为193,可以放入的元素个数也是193;

函数实现
函数实现

5.8 unordered_multiset和unordered_multimap【hashtable的adapter】

  • 分别是在unordered_setunordered_map的基础上增加了允许键值重复
  • 插入操作用的是hashtable实现的insert_equal()而不是insert_unique(),因此可以实现键值的重复;
  • 在SGI标准中,这两个容器被命名为hash_multisethash_multimap,这里的标题是采用了更为常用的C++11标准容器命名;

六、仿函数

  • 仿函数本质上是一个类class或者struct
    • 它的特殊之处在于它充当的是函数的作用,可以看做是一种函数的对象,只包含函数的功能,而不包括存储数据的功能,因为没有成员变量;
  • 目的是作为对象给算法或者容器泛型传参,更一般来说,就是作为函数的传入参数使用
    • 函数指针也作为函数的传入参数,但它的抽象性并不如仿函数,涉及到泛型的时候就很难和泛型绑定使用了;
    • 仿函数可以看作是抽象性和功能性都更强大的函数指针,也就是作为函数复用的媒介
  • 实现是通过重载operator()函数,而且必须要重载operator()函数
  • 用得比较多的是在自定义比较函数cmp()的时候;
  • 由于STL的算法均以函数的形式实现,因此仿函数多作为算法的传入参数配合使用,示意图如下:
    仿函数和算法的关系

6.1 两种仿函数基类泛型

  • STL里面只有两种仿函数基类泛型;
  • 两种基类都仅定义了传入参数和返回值的类型别名,供重载operator()时使用;
  • 两种基类如下:
  • (1) 仅有一个传入参数,和一个返回值类型,定义如下:

函数实现

  • (2) 有两个传入参数,和一个返回值类型,定义如下:

函数实现

  • STL中没有标准的两个以上参数的仿函数;

6.2 常用的一些仿函数实现

  • 实现仿函数通常用struct即可;
  • 主要实现operator()的重载;
  • 没有成员变量和其他成员函数;
  1. 算术类仿函数
  • 完成加减乘除等算术类运算;

函数实现

  • 使用方式如下:

使用方式

  • STL算法搭配使用:
  • 这也是STL中使用仿函数的主要目的

使用方式

  1. 关系类仿函数
  • 完成元素的大小比较关系运算;

函数实现

函数实现

  1. 逻辑类仿函数
  • 用于完成逻辑与或非运算;

函数实现

七、配接器

  • 配接器是一种设计模式,并没有独立的一种结构叫做配接器,而是需要依附于别的结构;
  • 有三种类型的配接器:
    • (1) 迭代器配接器;
    • (2) 容器配接器;
    • (3) 仿函数配接器
  • 相当于是对其他结构的对象做进一步的封装,以复用它们的功能,属于对基本结构拓展;
  • 但本质上不产生新的基本结构;

八、算法

8.1 sort()

  • 用于进行排序;

  • 传入的迭代器类型:

    • 随机访问迭代器
  • 实现机制:

    • (1) 当数据量较小的时候,使用直接插入排序
      • 当元素小于16或者32时,则数据量较小;
    • (2) 当数据量较大的时候,使用快速排序
      • 但出现以下情况会改用其他排序:
      • a. 当递归子区间元素较少时,改用直接插入排序
        • 当元素小于16或者32时,则数据量较小,改用直接插入排序;
      • b. 当快速排序的递归层次过深时,改用堆排序
        • 当递归的最大深度大于2*logN仍未改用直接插入排序时,改用堆排序;
  • 参考:

猜你喜欢

转载自blog.csdn.net/weixin_43992162/article/details/128100725