文章内容为侯捷老师的《C++标准库与泛型编程》的学习笔记
第二讲:容器源码探索
文章目录
源码之前,了无密码。
GP 编程
- Containers 和 Algorithm 团队各自忙自己的事情,其间通过 Iterator 进行沟通。
- Algorithm 通过 Iterator 确定操作的范围,Iterator 从 Container 取用元素。
template<class T>
inline const T& max(const T& a, const T& b)
{
return b > a ? b: a;
}
template<class T, class Compare>
inline const T& max(const T&a, const T& b)
{
return compare(a,b) ? b : a;
}
上述代码实现了两种排序方式,第一种是 普通数据类型的比较,当然也可以用来比较class,不过需要重载 运算符 <
,第二种则不需要,但需要提供额外的比较条件——compare 。
使用:
bool strLonger(const string& s1, const string& s2)
{
return s1.size() < s2.size();
}
cout << "max of zoo and hello: "
<< max(string("zoo"), string("hello")) << endl;
cout << "longest of zoo and hello:"
<< max(string("zoo"), string("hello"), strLonger) << endl;
链表不能使用std::sort
来进行排序,因为sort 底层在排序时,用到了随机迭代器,可以随意对迭代器进行++,而链表不具备这样的能力。它不是一个连续空间的数据结构。
所有的 algorithm ,其内最终涉及元素本身的操作,无非就是比大小。
操作符重载和模板
操作符重载。不同的操作符会有不同的操作数。比如 a++ 、a--
分别只需要一个操作数。而a+b
则需要两个操作数。
指针具有的操作,迭代器都会进行重载一遍,为了实现将迭代器当成指针使用的目的。比如一个链表的迭代器。
template<class T, class Ref, class Ptr>
struct __list_iterator {
typedef __list_iterator<T, Ref, Ptr> self;
typedef __list_node<T>* link_type;
link_type node;
reference operator*() const { return (*node).data; }
pointer operator->() const { return &(operator*()) ; }
self & operator++() { node = ( link_type)((*node).next) ); return *this; }
self operator++(int) { self tmp = *this; ++*this; return tmp;}//我比较纳闷,这里的int 有没有被用上。但源码的确是这样的。
};
在使用模板时,你要告诉编译器,你传入什么类型进入模板类中。使用尖括号明白的告诉编译器使用的类型,然后跟上变量名,最后在小括号中,将初始化数值传入类中。
complex<double> c1(2.5, 1.6);
把它们 (编程逻辑) 里面的类型暂定为一个符号,而不写死,等到真正使用的时候,才去把它确定下来。这个就是模板
分配器
调用alloc 会得到一块内存,但实际上只用到了一部分。如下图:
其中只用到了 蓝色部分,灰色是debug 时产生的。砖红色是一些cookie。绿色的是 为调整到某一个边界所额外开销。附加的这些东西是基本固定的,你要的某一块越大,它所附加东西的比例就越小。你要的东西越小,附加东西的比例就越大。
- cookie 记录申请内存块的大小,所以free 只需要一个指针即可回收内存
直接malloc带来的问题
一个string 的类型只有四个字节。当你往一个容器中放入100万个string 时,额外的开销会比100万个 string 还要多。
//分配512 ints
int *p = allocator<int>().allocate(512, (int*)0);
allocator<int>().decallocate(p, 512);
//allocator<int>() 是一个object
/* 扩展
cout << vector<int>().size() << endl;
cout << vector<int>(10).size() << endl;
cout << vector<int>(10, 100).front() << endl;
*/
良好分配器的解决方式
一个分配器有16个链表,每个链表负责不同的大小。第0个链表负责 8字节大小的内存,第n 负责 8*n 字节大小的内存,第16个负责128个字节的内存。所有需要内存的容器都向这个分配器要内存。如果分配器没有对应大小的内存,分配器才会去向操作系统要——要大块,然后做切割。割成一小块块的。切出来的一块块用链表串起来。结果是这切出来的每一个块都不带cookie。
其它的分配器
目前大多用的是 allocator
分配器,当然除了 常规的allocator
外,还提供了一些额外的分配器。比如:__poll_alloc
容器 - 结构与分类
容器会分为两大类:序列式容器和关联式容器。其中有一些容器是由一些容器衍生出来的,这里的衍生,并非继承而是复合。
比如 stack 和 queue 由 deque衍生而来,stack(queue) 中有一个 deque,它所表现出来的能力由deque 所提供,所以stack 和 queue 又是一种容器适配器。
set ,map, multimap,multiset 的功能由rb_tree(红黑树) 实现。
List
template <class T>
struct __list_node {
typedef void* void_pointer;
void_pointer prev;
void_pointer next;
T data;
};
template<class T, class Ref, class Ptr>
struct __list_iterator{
typedef T value_type;
typedef Ref reference;
typedef Ptr pointer;
typedef bidirectional_iterator_tag iterator_category;//使用一种标签来表明它的特性
typedef ptrdiff_t difference_type; //固定的长度,如果容器添加的元素超过这个长度,可能会爆掉
typedef __list_iterator<T, Ref, Ptr> self;
typedef __list_node<T>* link_type;
link_type node;
reference operator*() const { return (*node).data; }
pointer operator->() const { return &(operator *());}///等同于 ===> &((*node).data) 可实现元素类型-> 的效果
self& operator++ ()
{ node = (link_type)((*node).next); return *this; }
self operator++ (int)
{ self tmp = *this; ++*this; return tmp;}
/**
后置++的代码执行流程
1. 记录原值
self tmp = *this;
此处的不会调用operator* 来处理this,执行的是 copy cotr ,用以创建 tmp 并以 *this 为初值。
__list_iterator(const iterator& x) : node(x.node) {}
当*this 当做ctor 的参数后,就不调用operator* 的重载函数
2. 进行操作
*/
/**
关于 前置++ 的返回值为引用,后置++ 返回值为对象的原因:
向整型操作致敬,因为 C++ 允许整型两次前置++,而不允许两次后置++。
*/
};
template <class T, class Alloc = alloc>
class List {
protected:
typedef __list_node<T> list_node;
public:
typedef list_node* link_type;
typedef __list_iterator<T, T&, T*> iterator;
protected:
link_type node;
};
有趣的是,GCC4.9版本的 list 是使用继承来拥有 next 和 prev
template <typename _Tp>
struct _List_iterator{
typedef _Tp* pointer;
typedef _Tp* reference;
};
template<typename _Tp,
typename _alloc = std::allocator<_Tp>>
class list: protected _List_base<_Tp, _Alloc>
{
public:
typedef _List_iterator<_Tp> iterator;
};
struct _List_node_base {
_List_node_base * _M_next;
_List_node_base * _M_prev;
};
template <typename _Tp>
struct _List_node
: public _List_node_base {
_Tp _M_data;
};
/**
通过继承的方式来获取 头部指针和尾部指针
*/
Iterator需要遵循的原则
Iterators 迭代器 是处于容器和算法之间的桥梁,负责从容器中获取数据,也负责回答算法提出的问题。比如:负责告诉算法接受的迭代器 是什么样的数据类型。
所以迭代器中要设计出5种关联类型。
template <class _Iterator>
struct iterator_traits {
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;
};
比如List 的迭代器
template<class _Tp, class _Ref, class _Ptr>
struct _List_iterator {
typedef _List_iterator<_Tp,_Tp&,_Tp*> iterator;
typedef _List_iterator<_Tp,const _Tp&,const _Tp*> const_iterator;
typedef _List_iterator<_Tp,_Ref,_Ptr> _Self;
typedef bidirectional_iterator_tag iterator_category;
typedef _Tp value_type;
typedef _Ptr pointer;
typedef _Ref reference;
typedef ptrdiff_t difference_type;
};
Traits 特性,特征,特质
Iterator Traits 用来区分 class iterators 和 no-class iterators。
如果是class iterator T
,可直接T::valueType
,来获取迭代器的数据类型。
如果是class * iterator pT
。我无法通过pT->valueType;
所以要通过一个中间层,这个中间层,被称为萃取机。
/**
* 为了解决 传入的 iterator 是 class 还是 pointer,
* 下面制定了泛化版本 iterator_traits 和 偏特化版本 iterator_traits
*/
namespace my {
template <class T > //如果 T 是 class iterator
struct iterator_traits {
typedef typename T::value_type value_type;
};
template <class T> //如果 T 是 pointer to T
struct iterator_traits<T*> {
typedef T value_type;
};
template <class T> //如果 T 是 pointer to const T
struct iterator_traits<const T*> {
typedef T value_type;
};
}
完整的 iterator_traits。
template <class _Iterator>
struct iterator_traits {
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 <class _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;
};
template <class _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;
};
除了有迭代器的 Traits 外,还有
vector
template <class T, class Alloc = alloc>
class vector {
public:
typedef T value_type;
typedef value_type * iterator;
typedef value_type& reference;
typedef size_t size_type;
protected:
//不同于链表,所有连续空间存储元素的容器都可以使用指针作为迭代器。
iterator start;
iterator finish;
iterator end_of_storage;
public:
iterator begin() { return start;}
iterator end() { return finish;}
size_type size() const
{ return size_type(end() - begin());}
size_type capacity()
{ 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 *finish();}
};
deque
其中的 map 是一个vector, 其中的元素是一个个的指针,这些指针分别指向各个缓冲区的buff。
容器结构源码和容器迭代器源码
/**
* //sz 为元素A的大小,如果元素A大小 大于 512 则一个缓冲区放入一个元素。
//反则就放入 size_t(512 / sizeof(A)) 个数量
inline size_t __deque_buf_size(size_t n, size_t sz)
{ return n != 0 ? n :( sz < 512 ? size_t(512 / sz) :size_t(1) );}
**/
inline size_t _deque_buf_size(size_t n, size_t sz)
{
return n != 0 ? n : ( sz < 512 ? size_t(512/sz) : size_t(1));
}
template <class T, class Ref, class Ptr, size_t BufSize>
struct __deque_iterator {
typedef random_access_iterator_tag iterator_catogory; //随机访问迭代器的类型
typedef T value_type;
typedef Ptr pointer;
typedef Ref reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
typedef T** map_pointer;
typedef __deque_iterator self;
T* cur;//元素的位置
T* first;//缓冲区的头部
T* last;//缓冲区的尾部
map_pointer node;
reference operator*() const { return *cur; }
pointer operator->() const { return &(operator *());}
self & operator++()
{
++cur; //切换到下一个元素
if (cur == last) { //如果抵达缓冲区尾端
set_node(node + 1); //就切换到下一个节点
cur = first; //cur 再指向于 first cur 是指向缓冲区的元素,first 和 end 是缓冲区的首尾元素
}
return * this;
}
self operator ++(int)
{
self tmp = *this;
++*this;
return tmp;
}
difference_type operator-(const self& x) const
{
//buffer_size 等同于 _deque_buf_size
return difference_type(buffer_size()) * (node - x.node - 1) +
(cur - first) + (x.last - x.cur);
/**
node - x.node - 1 其中减一的原因是,x.node 所在的buff 上,存在元素未放满的情况
*/
}
reference operator[] (difference_type n) const
{ return *(*this + n);}
/// 依次修改 node first last 的位置
void set_node(map_pointer new_node)
{
node = new_node;
first = *new_node;
last = first + difference_type(buffer_size());
}
};
/**
BufSiz 指的是每个 buffer 容纳的元素个数
*/
template <class T, class Alloc = alloc,
size_t BufSize = 0>
class deque {
public:
typedef T value_type;
typedef value_type* pointer;
typedef __deque_iterator<T, T&, T*, BufSiz> iterator;//自定义类作为迭代器
typedef size_t size_type;
protected:
typedef pointer* map_pointer;
protected:
iterator start;//指向容器头部的迭代器 ,start.cur 是容器的第一个元素,*start 也是指向容器的第一个元素
iterator finish;//指向容器尾部的迭代器, finish.cur 是容器的最后一个元素,finish 是指向容器最后一个元素的下一个位置。要取最后一个元素,需要--finish.
map_pointer map;
size_type map_size;
public:
iterator begin() { return start; }
iterator end() { return finish; }
size_type size() { return finish - start;}
size_type max_size() const { return size_type(-1); }
};
queue
#include "deque.h"
/*
* queue 依赖 deque ,头尾可进可出的特性,实现了队列形式的先进先出
*/
template <class T, class _Sequence = deque<T> >//也可以使用其它容器作为 _Sequence 的参数,如果该容器可以实现 queue 中的成员函数。 ,比如 List
class queue {
public:
typedef typename _Sequence::value_type value_type;
typedef typename _Sequence::size_type size_type;
typedef _Sequence container_type;
typedef typename _Sequence::reference reference;
typedef typename _Sequence::const_reference const_reference;
protected:
_Sequence c;
public:
bool empty() const { return c.empty(); }
size_type size() const { return c.size(); }
reference front() { return c.front(); }
const_reference front() const { return c.front(); }
reference back() { return c.back(); }
const_reference back() const { return c.back(); }
void push(const value_type& __x) { c.push_back(__x); }//在尾部添加一个数据
void pop() { c.pop_front(); }//在头部删除一个元素
};
stack
/* *
* 同queue 类似,都是借用了 deque 的头尾都可进出的能力,实现了先进后出的栈
*/
#include "deque.h"
//GCC2.95
template <class _Tp, class _Sequence = deque<_Tp> >//也可以使用其它容器作为 _Sequence 的参数,如果该容器可以实现 stack 中的成员函数,比如 List 或 vector
class stack {
public:
typedef typename _Sequence::value_type value_type;
typedef typename _Sequence::size_type size_type;
typedef _Sequence container_type;
typedef typename _Sequence::reference reference;
typedef typename _Sequence::const_reference const_reference;
protected:
_Sequence _M_c;
public:
bool empty() const { return _M_c.empty(); }
size_type size() const { return _M_c.size(); }
reference top() { return _M_c.back(); }
const_reference top() const { return _M_c.back(); }
void push(const value_type& __x) { _M_c.push_back(__x); }//在尾部添加一个元素
void pop() { _M_c.pop_back(); }//在尾部删除一个元素
};
容器rb_tree
红黑树是平衡二分搜寻树中常被使用的一种。平衡二分搜寻树的特征:排列规则有利于 Search 和 insert,并保持适度平衡——无任何节点过深。
rb_tree 提供两种 insertion操作:insert_unique 和 insert_equal。前者表示节点的key 一定是在整个tree 中独一无二,否则安插失败。后者表示节点的key 可重复。独一无二的key 对应的就是 set 和 map,可以重复的key 对应的就是 multiSet 和 multiMap。
rb_tree 的结构
template <class Key,
class Value, //此处的Value 是 key+data ,封装起来的pair。也可以将key 当做 data,那体现出的容器就是Set
class KeyOfValue, //如何从value 中拿到key
class Compare, //排序比大小的方式
class Alloc = alloc>
class rb_tree {
protected:
typedef __rb_tree_node<Value> rb_tree_node;
public:
typedef rb_tree_node* link_type;
typedef Value value_type;
typedef const value_type* const_pointer;
typedef value_type* pointer;
typedef size_t size_type;
typedef value_type& reference;
typedef const value_type& const_reference;
typedef _Rb_tree_iterator<value_type, reference, pointer> iterator;
typedef _Rb_tree_iterator<value_type, const_reference, const_pointer>
const_iterator;
protected:
size_type node_count;
link_type header;
Compare key_compare; //key 的大小比较规则:应该是一个函数对象,即仿函数
};
在GCC 7.3 版本下运行的rb_tree
//GCC
_Rb_tree <int, int, _Identity<int>, less<int> > itree;
cout << "itree.empty(): \t " <<itree.empty() << endl;
cout << "itree.size(): \t " << itree.size() << endl;
for (int i = 0; i < 10; ++i) {
itree._M_insert_unique(std::move(i));
}
itree._M_insert_equal(5);
cout << "itree.empty(): \t " <<itree.empty() << endl;
cout << "itree.size():\t " << itree.size() << endl;
cout << "itree.count():\t " << itree.count(5) << endl;
在上面涉及到了两个函数对象。下面是它们在GCC 2.9 中的部分实现。
/* 仿函数,形态是一个类,。没有data ,但是行为上是一个函数 */
template <class Arg, class Result>
struct unary_function{
typedef Arg argument_type;
typedef Result result_type;
};
template<class T>// identity 的数学含义是证同性函数,即不做修改,保持原值。 identity n. 身份;同一性
struct identity : public unary_function<T,T> {
const T& operator()(const T& x) const {return x;}
};
template <class Arg1, class Arg2, class Result>
struct binary_function {
typedef Arg1 first_argument_type;
typedef Arg2 second_argument_type;
typedef Result result_type;
};
template <class T>
struct less : public binary_function<T, T, bool > {//模板类的继承,与常规类继承类似,不同的地方是传参数给基类。
bool operator() (const T& x, const T& y) { return x < y;}
};
顺便提一下仿函数
auto fun = My::less<int>();
cout << ( fun(5, 6) ? "5 < 6" : "5 > 6") << endl;
采用上面的less 并放入一个 My 的namespace 中,进行调用。开始是想通过 My::less<int>(5,6);
来使用,但被编译器告知constexpr My::less<int>::less()
,所以把 My::less<int>()
,当做一个类来看——创建对象,然后对象调用operator() (const T&x)
,问题解决。
Set
set/multiset 以 rb_tree 为底层结构,因此有元素自动排序的特性。排序的依据 key, 而 set/multiset 元素的 value 和 key 合一。
我们无法使用 set/multiset 的 iterator改变元素值,不仅是因为 key 有严谨的排列规则。而且set/multiset 的迭代器是采用的是底层 rb_tree 中的 const_iterator,就是为了禁止 用户 对元素赋值。换句话说,无法修改已插入Set 中的元素。
部分源码如下:
template <class Key,
class Compare = less<key>,
class Alloc = alloc>
class Set {
public:
typedef Key key_type;
typedef Key value_type;
typedef Compare key_compare;
typedef Compare value_compare;
private:
typedef rb_tree<key_type, value_type,
identity<value_type>, key_compare, Alloc> rep_type;
rep_type t;
public:
typedef typename rep_type::const_iterator iterator;
};
Map
类同于 Set 同样拥有 元素自动排序 的特性,排序的依据是 key。我们无法使用 map/multimap 的 iterator 改变元素的key,但可以用它来改变元素的 data。实现的原理是,map/multimap 内部自动将用户指定 的key 设置为const 类型,如此虽然迭代器不是 const 但也能禁止用户修改 元素的key 值。
源码如下:
template <class Key, class T,
class Compare = less<Key>,
class Alloc = alloc>
class Map {
public:
typedef Key key_type;
typedef T data_type;
typedef pair<const Key, T> value_type;//通过const key 来限制 key 不能被外界修改
typedef Compare key_compare;
private:
typedef rb_tree<key_type, value_type, select1st<value_type>,
key_compare, Alloc> rep_type;
rep_type t;
public:
typedef typename rep_type::iterator iterator;
};
在使用Map 创建对象时
map <int, string> imap;->
map <int, string, less<int>, alloc> imap;->
templdate < int, pair<const int, string>, select1st<pair<cosnt int, string>>, less<int>, alloc>
class rb_tree;
容器 hashtable
无序关联容器
容器由 vector 和 链表构成。通过将元素转换为数字来除以当前 vector 的长度得到的余数就是在vector中安插的位置。另外,当vector 中安插的元素总个数大于vector 的size,容器会发生分裂现象。首先,vector的长度会扩展近似2倍,然后将原来的元素重新打散,再按照 元素转数字 的方式,重新安插元素到 新vector 中。
hashTable 的部分源码
gcc 2.95版本
/* 一个单项链表 */
template <class _Val>
struct _Hashtable_node
{
_Hashtable_node* _M_next;
_Val _M_val;
};
template <class _Val, class _Key, class _HashFcn,
class _ExtractKey, class _EqualKey, class _Alloc>
struct _Hashtable_iterator {
typedef hashtable<_Val,_Key,_HashFcn,_ExtractKey,_EqualKey,_Alloc>
_Hashtable;
typedef _Hashtable_node<_Val> _Node;
_Node* _M_cur;
_Hashtable* _M_ht;
};
/**
这里的 node 是指向一个 buckets vector 中一个bucket的链表节点,这个节点用于知晓 迭代器 查找元素是否走到链表的末尾。
当走到末尾时,迭代器有能力会切换到下一个 bucket 中,然后在新的bucket 中继续遍历链表。
*/
template <class _Val, class _Key, class _HashFcn,
class _ExtractKey, class _EqualKey, class _Alloc>
class hashtable {
public:
typedef _Key key_type;
typedef _Val value_type;
typedef _HashFcn hasher;//这个函数用于计算元素的hashCode,根据hashCode 决定元素的安插位置
typedef _EqualKey key_equal;// 这个函数用于表示 两个元素相等的条件
private:
hasher _M_hash;
key_equal _M_equals;
_ExtractKey _M_get_key;
vector<_Node*,_Alloc> _M_buckets;
size_type _M_num_elements;
public:
friend struct
_Hashtable_iterator<_Val,_Key,_HashFcn,_ExtractKey,_EqualKey,_Alloc>;
};
一些 hashFcn
函数的实现方式,如果在 hashtable 中添加其它元素时需要自行编写 HashFcn
。
template <class _Key> struct hash { };
#define __STL_TEMPLATE_NULL template<>
inline size_t __stl_hash_string(const char* __s)
{
unsigned long __h = 0;
for ( ; *__s; ++__s)
__h = 5*__h + *__s;
return size_t(__h);
}
__STL_TEMPLATE_NULL struct hash<char*>
{
size_t operator()(const char* __s) const { return __stl_hash_string(__s); }
};
__STL_TEMPLATE_NULL struct hash<const char*>
{
size_t operator()(const char* __s) const { return __stl_hash_string(__s); }
};
....
hash function 的目的,就是希望根据元素值算出一个 hash code,使得元素经 hash code 映射之后能够【够乱够乱随机】地被安插于 hashtable 内。越是凌乱,越是不容易发生碰撞。