STL学习笔记

-目录-

前言

STL是GP编程的典范,使用容器涉及六大模块:

分配器为容器分配内存,迭代器用来访问容器数据,算法通过迭代器操作容器内的数据,算法使用仿函数改变算法过程。适配器是各部分的粘合剂。

这其中,除了算法是模板函数外,其他都是模板类。

容器

list实现

Sequence Container

有头节点的 双向 循环 链表。

deque实现

Sequence Container

deque是分段连续的。有很多个小段连续空间(buffer),外部一个数组用来存放buffer指针。通过iterator模拟连续。 外部buffer指针数组会类似vector扩容。这意味着可以不改变元素扩容,不像vector那样真正的连续。

容器适配器:stack queue

stack和queue默认的Seqence是deque,stack和queue可以默认是deque的适配器。 二者还可以选用list作为底层容器。

array和vector实现

Sequence Container

vector动态连续数组,空间不足时会两倍扩充。

array,底层数组直接封装。

_Rb_tree 容器

Associative Container

如名,红黑树,节点是双向链表形式的树。模板参数提供至少前四个,分别是Key,Value(Key和Data的结合体),GetKey(仿函数,如何从Value里拿到Key),Compare(仿函数,如何比较Key)

#include <bits/stl_tree.h>

int main() {
    struct MyS {
        int a;
        int b;
    };
    struct GetKey {
        int operator()(const MyS &s) const {
            return s.a;
        }
    };
    //key, key and data = value, how to get key from value, compare, allocator(default)
    std::_Rb_tree<int, MyS, GetKey, std::less<int>> rbTree;
    rbTree._M_insert_unique(MyS{1, 2});
    rbTree._M_insert_unique(MyS{3, 4});
    rbTree._M_insert_equal(MyS{1, 6});
}

容器适配器:map set multimap multiset

#include <map>
#include <set>

int main() {
    //key type, data type, compare function, allocator
    std::map<int, int> m;
    //key type, data type, compare function, allocator
    std::multimap<int, int> mm;

    //key type, compare function, allocator
    std::set<int> s;
    //key type, compare function, allocator
    std::multiset<int> ms;
}

_HashTable 容器

Associative Container

哈希表,内部是vector,每个vector元素称作bucket,每个vector元素是一个节点的指针,这个节点是一个链表的首个节点,这个链表用来解决hash冲突。

vector的size为质数,每次扩容扩容到容量最接近2倍的质数,扩容后重新分配所有bucket。

注意哦,直接使用hash table的话,要写全模板参数,尤其是它没有默认构造函数,要手动指定至少bucket大小、hash函数、key比较函数这三个参数。

#include <iostream>
#include <hashtable.h>
#include <cstring>

int main() {
    struct StrCmp {
        bool operator()(const char *a, const char *b) const {
            return strcmp(a, b) == 0;
        }
    };

    __gnu_cxx::hashtable<
            const char *,               //value(data+key),
            const char *,               //key
            std::hash<const char *>,    //hash function,
            std::_Identity<const char *>, //how to get key
            StrCmp,                 	//key equal function
            std::allocator<const char *>>      //allocator
    myHashTable(100, std::hash<const char *>(), StrCmp());
    myHashTable.insert_equal("hello");
    myHashTable.insert_equal("world");
    auto it = myHashTable.find("hello");
    std::cout << *it; //输出hello
}

GNU C++的内置的hash函数

std::hash就是传入一个类型返回一个size_t的函数。

STL的size_t hash(Ty t);使用模板偏特化对不同类型做不同处理:

实际放到bucket数组时要对存放bucket的vector大小取模的。

以下代码是指针类型偏特化 和 数值类型偏特化。

  /// Partial specializations for pointer types.
  template<typename _Tp>
    struct hash<_Tp*> : public __hash_base<size_t, _Tp*>
    {
      size_t
      operator()(_Tp* __p) const noexcept
          //指针类型,重新解释为size_t
      { return reinterpret_cast<size_t>(__p); }
    };

  // Explicit specializations for integer types.
#define _Cxx_hashtable_define_trivial_hash(_Tp)    \
  template<>                  \
    struct hash<_Tp> : public __hash_base<size_t, _Tp>  \
    {                                                   \
      size_t                                            \
      operator()(_Tp __val) const noexcept              \
      // 数值类型直接转化
      { return static_cast<size_t>(__val); }            \
    };

  /// Explicit specialization for bool.
  _Cxx_hashtable_define_trivial_hash(bool)

适配器:unordered_ 容器

unordered容器内部实现是

注意他们是两个数据结构,而不是一个数组,然后数组各自存放自己的单链表,推测是为了添加第0个元素时提高效率并且为了迭代时直接跳过空bucket。

数组有很多bucket(实际上就是存放链表首个指针的数组元素),每个bucket存放链表用于解决冲突。bucket数量是固定的且大于元素个数,每次当元素数量等于bucket数量时,会重新分配下一个数量的bucket(一般是最接近之前数量二倍的质数),同时会rehash之前的所有元素到新的bucket数组。

#include <unordered_map>
#include <unordered_set>


int main() {
    //key type, data type, hash function, compare function, allocator
    std::unordered_map<int, int> m;
    //key type, data type, hash function, compare function, allocator

    std::unordered_multimap<int,int> mm;
    //key type, hash function, compare function, allocator
    std::unordered_set<int> s;
    //key type, compare function, compare function, allocator
    std::unordered_multiset<int> ms;
}

迭代器

迭代器类型

迭代器是用来访问容器的。容器对外提供迭代器。

访问容器是有规则的:是否能随机访问?是否能双向访问?那么迭代器应该有自己的属性,以便让外部调用时注意到。

这个规则在迭代器里是怎样的表现形式?首先想到使用enum指定标签,实际上STL使用struct xx_tag{},使用一个类作为一个属性。

具体有这5种属性

iterator_category 是什么类型的,比如说双向遍历类型
value_type		容器内装的值是什么类型的,比如说int
difference_type	容器内距离的类型,比如说long int
pointer			指针类型是什么,比如说int*
reference		引用类型是什么,比如说int&

不同的属性组合成迭代器不同的类型。

迭代器类型萃取器 iterator_traits

什么是类型萃取

在编译期就知道一个变量/类的类型,比如重载就可以看作类型萃取的一个应用。

为什么需要类型萃取

在编译期知道类型,就可以根据类型做出一些事情咯,比如类型萃取知道这个变量是指针还是引用,然后定义typedef isPointer OK,之后程序运行期就可以直接知道isPointer()==OK(),就设置不改变这个变量的内容。

为什么需要迭代器类型萃取

算法需要通过iterator操作容器内数据,一个算法需要得知iterator的属性(需要知道这些属性来做最佳的算法处理),具体来说,是需要上一节提到的五种属性。

怎么实现呢?

具体而言,如何知道根据模板传入的类型参数做出判断。

一个错误的想法:使用运行期程序判断

c++11提供了decltype()做类型推断,可以用decltype(_Ty)==decltype(int*)做判断然后具体赋值吗?显然不可能,c++不提供类型间的操作。不能比较int==int,必须要实例化比如int()==int(),而==的前提是左右对象类型相同,不相同的情况下会编译报错。

这里要和golang的类型推断语法比较一下,golang的类型推断 switch val.type,然后就可以判断类型是否符合了。

还有一种方法,c++为了能够根据传入类型做操作,天然支持了重载,自动根据类型调用不同的函数。难道说要在容器里定义重载函数,然后让外部调用函数获取属性吗?好像也可以,但是一方面这样还需要调用函数,会降低性能。另一方面,不符合泛型编程数据和方法分开的准则。接下来看看STL是怎样实现的。

STL是这样实现的:使用编译期的类型萃取

  1. 每个容器自己有一个_xx_iterator模板类,使用typedef,把自己的属性写死,比如下面是stl_list的源码,把自己的iterator_category写死为bidirectional_iterator_tag。
template<typename _Tp>
struct _List_iterator
{
	...
    typedef ptrdiff_t                          difference_type;  //ptrdiff_t 到最后就是long int
    typedef std::bidirectional_iterator_tag    iterator_category; 
    //std::bidirectional_iterator_tag到最后就是  struct input_iterator_tag { };
    typedef _Tp                                value_type;
    typedef _Tp*                               pointer;
    typedef _Tp&                               reference;
    ...
};

这样外部就可以直接从容器里获取到属性,获得这个属性的对象。

//获取到属性对象
list<int>::iterator::iterator_category listCat;
vector<int>::iterator::iterator_category vectorCat;

至此,完成了属性类型typename到一个属性对象object的转换。

  1. 还有一个问题,不是所有iterator都是模板,比如指针,引用。这时候也想获得他们的属性,他们没有模板类,怎么办呢。很自然的想到使用适配器模式。若传入的是指针或者引用,则进行一个partial specialization。若是普通类型,则读取他内置的属性。

    这个适配器是std::traits<iterator _It>,看看他是怎样进行偏特化的。

    传入普通类型

  template<typename _Iterator>
    struct __iterator_traits<_Iterator,
			     __void_t<typename _Iterator::iterator_category,
				      typename _Iterator::value_type,
				      typename _Iterator::difference_type,
				      typename _Iterator::pointer,
				      typename _Iterator::reference>>
    {
      typedef typename _Iterator::iterator_category iterator_category;
      typedef typename _Iterator::value_type        value_type;
      typedef typename _Iterator::difference_type   difference_type;
      typedef typename _Iterator::pointer           pointer;
      typedef typename _Iterator::reference         reference;
    };

  template<typename _Iterator>
    struct iterator_traits //从这里实例化,父类接到并读取传入iterator内置的
    : public __iterator_traits<_Iterator> { };

​ 传入指针/引用类型

template<typename _Tp>
struct iterator_traits<_Tp*>
{
    typedef random_access_iterator_tag iterator_category;
    typedef _Tp                         value_type;
    typedef ptrdiff_t                   difference_type;
    typedef _Tp*                        pointer;
    typedef _Tp&                        reference;
};

/// Partial specialization for const pointer types.
template<typename _Tp>
struct iterator_traits<const _Tp*>
{
    typedef random_access_iterator_tag iterator_category;
    typedef _Tp                         value_type;
    typedef ptrdiff_t                   difference_type;
    typedef const _Tp*                  pointer;
    typedef const _Tp&                  reference;
};

外部怎样通过适配器获得属性的对象呢?

//使用适配器
iterator_traits<list<int>::iterator>::iterator_category listItAdapter;
iterator_traits<int*>::iterator_category intItAdapter;
//不使用适配器
list<int>::iterator::iterator_category listIt; //和前者都是std::bidirectional_iterator_tag对象

算法

算法是什么

通过迭代器对容器内的数据信息进行操作。

迭代器类型对算法的影响

之前提到迭代器类型,其中有个属性是iterator_category,前面提到它其实是一个类。它有这些类型,继承关系如下:

其中input_iterator_tag和output_iterator_tag是最Base的。

  //@{ 
  ///  Marking input iterators.
  struct input_iterator_tag { };

  ///  Marking output iterators.
  struct output_iterator_tag { };

  /// Forward iterators support a superset of input iterator operations.
  struct forward_iterator_tag : public input_iterator_tag { };

  /// Bidirectional iterators support a superset of forward iterator
  /// operations.
  struct bidirectional_iterator_tag : public forward_iterator_tag { };

  /// Random-access iterators support a superset of bidirectional
  /// iterator operations.
  struct random_access_iterator_tag : public bidirectional_iterator_tag { };
  //@}

iterator type决定了算法将怎么运作来获得最佳性能。如下,距离函数distance用来算两个迭代器间的距离,对于随机的(random_access_iterator_tag)直接相减就可以了,而对于广泛的,则是使用迭代器++,计数得到。

 template<typename _InputIterator>
//使用迭代器类型萃取 获得长度类型 difference_type,不能随意返回int哦
   inline typename iterator_traits<_InputIterator>::difference_type
   __distance(_InputIterator __first, _InputIterator __last,
              input_iterator_tag)//广泛型
   {
		...
   }

//使用重载根据random_access_iterator_tag定位到函数 
 template<typename _RandomAccessIterator>
   inline typename iterator_traits<_RandomAccessIterator>::difference_type
   __distance(_RandomAccessIterator __first, _RandomAccessIterator __last,
              random_access_iterator_tag)//随机访问型
   {
		...
   }

仿函数

为什么需要仿函数

在某些算法需要传入函数的时候,在C语言里是传入函数指针。

C++可以传入一个重载了operator()的class,算法使用时会实例化这个class,然后调用operator(),这个class里的还可以重载operator(),就很方便。使用仿函数+模板,削弱了类型限制,毕竟运行时才知道传入的仿函数是不是正确的。

一个简单的例子:

template<typename ty1, typename ty2, typename op>
class Show {
public:
    ty1 show(ty1 t1, ty2 t2) {
        op()(t1);
        op()(t1, t2);
        // op()(t1, t2, t2); 编译不出错,运行时出错
    }
};

struct op {
    int operator()(int a, int b) {
        std::cout << a + b << std::endl;
    }

    int operator()(int a) {
        std::cout << a << std::endl;

    }
};

int main() {
    Show<int, int, op> s;
    s.show(1, 2);
};

注意,为了让自己的仿函数具有更好的兼容性,要继承unary_function 或binary_function等(一元函数,二元函数),这里继承了里面的typedef,做到类似iterator type的效果。

STL可以传入的函数

当然可以传入算法的不是只有仿函数。

如下展示了可以传入:函数、仿函数、lambda函数、STL仿函数

struct MyLessObj
        : public std::binary_function<int, int, bool> {
    bool operator()(int a, int b) const {
        return a < b;
    }
};


bool MyLessFunc(int a, int b) {
    return a < b;
}

int main() {
    std::vector<int> v;
    std::sort(v.begin(), v.end(), MyLessObj());
    std::sort(v.begin(), v.end(), MyLessFunc);
    std::sort(v.begin(), v.end(), [](int a, int b) { return a < b; });
    std::sort(v.begin(), v.end(), std::less<int>());
};

STL提供的一些仿函数

比如,std::plus<T>,就是简单包装了operator+。在有些必须传简单仿函数时,可以直接传入。

仿函数的适配器

为什么需要仿函数适配器?

我们试图改变某个std函数或者自己写的函数里面的某个选项。

以std::bind2nd为例

std::less,我们可以指定比某个数值小,这时使用适配器std::bind2nd,将某个值绑定到第二个参数上。

本质上就是封装了一层,粗糙版见下:

struct MyLess {
    bool operator()(int a, int b) const {
        return a < b;
    }
};

struct AdapterMyLess {
    AdapterMyLess(int b) : b(b) {}
	//重载()
    bool operator()(int a) const {
        return MyLess()(a, b);
    }

    int b;
};

//调用
//AdapterMyLess(1)(2);//2<1 false

而详细版需要考虑类型通用(泛型),类型检查(类型强转operatorT())。

template<typename _Operation>
class binder2nd
      /* unary_function 转换成一元函数,将属性传承下去
		template<typename _Arg, typename _Result>
        struct unary_function
        {
          typedef _Arg 	argument_type;   
          typedef _Result 	result_type;  
        };
    */
  : public unary_function<typename _Operation::first_argument_type, typename _Operation::result_type>
{
  protected:
    //暂存,为了重载调用运算符
    _Operation op;
    typename _Operation::second_argument_type value;

  public:
    //构造函数
    binder2nd(const _Operation& __x, const typename _Operation::second_argument_type& __y)
    : op(__x), value(__y) { }

    //重载调用运算符,真正起作用的部分
    typename _Operation::result_type operator()(const typename _Operation::first_argument_type& __x) const
    { return op(__x, value);}
    typename _Operation::result_type operator()(typename _Operation::first_argument_type& __x) const
    { return op(__x, value);}
};

//对外使用这个模板函数,对class binder2nd做了一层封装,返回了一个binder2nd对象
//目的是使用模板函数可以自动推导类型,不用像类模板那样必须指明类型,顺便把类型检查也做了
template<typename _Operation, typename _Tp>
  inline binder2nd<_Operation> bind2nd(const _Operation& __fn, const _Tp& __x)
  {
    typedef typename _Operation::second_argument_type _Arg2_type;
    //_Arg2_type(__x) 此处做编译期类型检查,是否可以转换成_Arg2_type类型
    return binder2nd<_Operation>(__fn, _Arg2_type(__x));
  } 

//调用
std::bind2nd(std::less<int>(), 1)(2); //2<1 false
//或者直接调用对象
binder2nd<less<int>> b(less<int>(), 1);
b(2); //2<1 false

仿函数适配器都是这个套路,首先向上传递参数类型(为了可能被别人adapter),以便将来算法使用这些类型。然后重载()运算符,在这个重载函数里面做一些操作,再调用传入的仿函数对象的operator()。

分配器

见C++动态内存分配与allocator | YuYoung’s Blog (yuyoung32.com)

© 2019 - 2023 · YuYoung's Blog