侯捷 C++ STL标准库和泛型编程 | 学习笔记
侯捷 C++ STL标准库和泛型编程 | 学习笔记
1 STL概述
STL —— Standard Template Library,标准模板库
C++ Standard LIbrary,C++标准库中包含STL(即STL+一些小东西)
1.1 头文件名称
- C++标准库的 header files 不带
.h
,例如:#include<vector>
- 新式 C header files 不带
.h
,例如:#include<cstdio>
- 老式 C header files 带
.h
仍然可用,例如:#include<stdio.h>
新式 header 内的组件封装于 namespace std
老式 header 内的组件不封装于 namespace std
1.2 STL基础介绍
STL六大部件:容器(Containers)、分配器(Allocators)、算法(Algorithms)、迭代器(Iterators)、仿函数(Functors)、适配器(Adapters)
- 容器:放数据
- 分配器:是来支持容器将数据放到内存里
- 算法:是一个个函数来处理存放在容器里的数据
- 迭代器:就是来支持算法操作容器的
- 仿函数:作用类似函数,例如相加相减等等
- 适配器:有三种,分别将容器,迭代器,仿函数来进行一个转换
实例:
- 首先是创建一个 container(vector)
- allocator 来帮助 container 来分配内存(一般会忽略不写)
- 用一个 Algorithm 来操作数据(count_if 是数出满足条件的个数)
- iterator 就是一个泛化的指针,来告诉 Algorithm 要处理哪里的数据
- 用一个 functor 来判断数据(less 其有两个参数传入,第一个 < 第二个就为真)
- 先用一个 function adapter(bind2nd)绑定了第二个参数为 40;再用一个 function adapter(not1)来对整个判断结果进行否定
判断条件 predicate 为:not1(bind2nd(less<int>(), 40))
—— 表示 >= 40 数为真
前闭后开:[ ),基本所有容器都有
begin()
end()
,但 begin 是指向的容器的第一个元素,而 end 是指向的容器最后一个元素的下一个例子:遍历容器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 ...
Container<T> c;
Container<T>::iterator i = c.begin();
for (; i != c.end(); ++i)
{
...
}
//但在C++11中可以用新语法简写
...
Container<T> c;
for (auto elem : c)
{
...
}
1.3 typename
在模板参数的关键字使用中与 class
是一样的
在类型前面加上 typename
:
1 | template <typename T> |
在这个例子中,typename
用于告诉编译器 T::NestedType
和 T::SomeType
是类型名称而不是成员变量
typename
是一个用于明确指定符号是一个类型的关键字,以帮助编译器正确解析代码并避免歧义,如果不使用 typename
,编译器可能会认为符号是一个值而不是类型,导致编译错误。
2 OOP vs. GP
OOP —— Object-Oriented programming 面向对象编程
将数据和操作关联到一起
例如容器 List,其自带了一个
sort()
,因为链表的存储空间不是连续的,Iterator 不能实现加减操作,所以不能使用全局的::sort()
GP —— Generic Programming 泛式编程
将数据和操作分开
- 容器和算法的团队就可以各自闭门造车,其间通过 Iterator 联通即可
- 算法通过 Iterator 确定操作范围,并通过 Iterator 取用容器的元素
- 所有的算法,其内的最终涉及元素的操作都是比大小
3 容器
3.1 容器结构分类
分类:序列式容器 Sequence Container,关联式容器 Associative Container
序列式容器:按照放入的次序进行排列
- Array 数组,固定大小
- Vector 向量,会自动扩充大小
- Deque 双向队列,双向都可以扩充
- List 链表,双向链表
- Forward-List 链表,单向链表
关联式容器:有 key 和 value,适合快速的查找
STL中实现使用红黑树(高度平衡二叉树)和哈希表
Set,key 就是 value,元素不可重复
Map,key 和 value 是分开的,元素不可重复
Multi~,元素是可以重复的
Unordered~,HashTable Separate Chaining
其中 Array,Forward-List,Unordered~ 都是C++11的
3.2 序列式容器
3.2.1 array
测试
1 |
|
运行结果:
随机数据填充容器:47ms;排序和搜索:187ms
深度探索
C++TR1下(比较简单):
1 | template <typename _Tp, std::size_t _Nm> |
GCC4.9下(复杂且无益处):
1 | // GCC4.9通过多个typedef以下面的逻辑创建的array里的data |
3.2.2 vector
测试
1 |
|
这是 array 在后面插入元素,其中若空间 capacity 不够,其会进行两倍扩充——即空间不够时会将原来的空间 *2
1 | c.push_back(string(buf)); |
运行结果:
随机数据填充容器:3063ms;直接搜索:0ms(运气很好);排序后二分查找:2765ms
深度探索
GCC2.9下:
一共3个指针:start
,finish
,end_of_storage
所以 sizeof(vector<int>)
是12
1 | template <class T, class Alloc = alloc> |
vector 每次成长会大量调用元素的拷贝构造函数和析构函数,是一个大成本
1 | void push_back(const T& x) |
GCC4.9下变得复杂:
且迭代器也变得乱七八糟,舍近求远,何必如此!!
3.2.3 list
测试
1 | // 同理 |
注意:
c.sort();
是容器自带的排序函数,如果容器自带肯定是要比全局的排序函数好的list 同样也是用
c.push_back(string(buf));
往里添加元素的
运行结果:
随机数据填充容器:3265ms;直接搜索:16ms;排序:2312ms
深度探索
GCC2.9中
1 | // list class |
除了 array,vector 这样是连续存储的容器,其他容器的 iterator 都是智能指针,其有大量的操作符重载 —— 模拟指针
基本上所有的 iterator 都有下面5个 typedef 和一大堆操作符重载
1 | // iterator class |
注意:self operator++(int){...}
的 self tmp = *this;
中,由于先调用了 =
唤起了 copy ctor 用以创建 tmp 并以 *this
为初值,所以不会唤起 operator*
—— *this
已经被解释为 ctor 的参数
下面的 ++*this;
同理
与 int 类似:iterator 可以连续前++,但不能连续后++
所以前++是返回引用,后++返回值
因为要符合前闭后开原则,所以在 list 尾端加上了一个空白节点
GCC4.9中做出了改进:
- 迭代器模板参数从三个 –> 只有一个
- 节点 class 中的前后指针类型从
void*
–>_LIst_node_base*
在GCC4.9中 sizeof(list<int>)
是 8
在GCC2.9中 sizeof(list<int>)
是 4
3.2.4 forward_list
测试
1 | // 同理 |
注意:forward_list 只有
c.push_front();
且没有forward_list.back()
forward_list.size()
运行结果:
随机数据填充容器:3204ms;直接搜索:15ms;排序:2656ms
深度探索
与 list 相似,略
3.2.6 deque
测试
类似vector,两边都能扩充,实际上是分段连续的
其是通过 map(是一个vector,但在扩充时会 copy 到中间)里的指针指向各个 buffer,buffer 里再存数据,每个 buffer 的大小一致,每次扩充都是扩充一个指针指向一个新的 buffer
map其实是vector,它扩充的时候会增长为原来的2倍,移动原数据到新内存空间的时候,它会放到新内存空间的中间,方便扩充
比如:原来大小为8,扩充为16,那原来这8个放在 5-12这个位置,前面和留后面留着扩充
1 | void test_deque(long& value) |
运行结果:
随机数据填充容器:2704ms;直接搜索:15ms;排序:3110ms
下面的 stack 和 queue 内部都是一个 deque,所以技术上这两个可以看作容器适配器 Container Adapter
深度探索
GCC2.9下
1 | template <class T, class Alloc = alloc, size_t BufSiz = 0> |
注意:第三个模板参数
size_t BufSiz = 0
有一个函数:如果不为0,则 buffer size 就是传入的数据
如果为0,表示预设值,那么
如果
sz = sizeof(value_type)
< 512,传回512/sz
如果sz = sizeof(value_type)
>= 512,传回1
迭代器四个指针,cur
指向当前元素,first
指向当前 buffer 的第一个元素,last
指向当前 buffer 的最后一个元素的下一个,node
指向当前 buffer 在 map(控制中心)的指针
1 | // deque迭代器 |
deque 中的 insert 函数:
1 | iterator insert(iterator position, const T& x) |
deque 模拟连续空间(deque iterator 的功能):
-
:两个位置之间的距离——前闭后开的元素个数两个位置之间的距离 = buffer_size * 两个位置之间 buffer 的数量 + 末尾位置到 buffer 前端的长度 + 起始位置到 buffer 末尾的长度
++
/--
:注:下面带参数的是后++(i++)+=
/+
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23self& operator+=(difference_type n)
{
difference_type offset = n + (cur - first);
if (offset >= 0 && offset < difference_type(buffer_size()))
// 若+了之后在缓冲区大小范围内
cur += n; // 直接移动迭代器 n 步
else
{
difference_type node_offset = offset > 0 ? offset / difference_type(buffer_size())
: -difference_type((-offset - 1) / buffer_size()) - 1;
// 计算偏移的节点数,offset > 0判断是为了之后的-=/-
// 这里(-offset - 1)后除buffer_size()再-1是为了offset==buffer_size()的情况
set_node(node + node_offset); // 调整节点,使迭代器指向正确的节点
cur = first + (offset - node_offset * difference_type(buffer_size())); // 调整迭代器位置
}
return *this;
}
self operator+(difference_type n) const
{
self tmp = *this; // 复制当前迭代器
return tmp += n; // 返回向前移动 n 步后的迭代器副本
}-=
/-
:1
2
3
4
5
6
7// -就等于+负的
self& operator-=(difference_type n) { return *this += -n; }
self operator-(difference_type n) const
{
self tmp = *this;
return tmp -= n;
}[]
:1
2reference operator[](difference_type n) const
{ return *(*this + n); }
GCC4.9下:其实没必要这样
G2.91 允许指派 buffer_size
G4.53 不允许了
3.2.7 stack,queque
测试
stack:
queue:
stack,queue 是通过
push()
和pop()
来放取元素的,且无iterator 的操作
深度探索
stack 和 queue 内部默认用 deque 来实现,所以有时候不会将这两个认为容器而是一个适配器
- 底层函数可以使用 list 和 deque(deque默认更快)
- queue 不能用 vector,stack 可以用 vector
- set,map 都不能用
用时编译器可以通过的,但在具体使用函数时,若遇到底层容器没有这个函数时,就会报错
1 | // queue |
stack,queue 都不允许遍历,也不提供 iterator
3.3 关联式容器
3.3.0 RB-Tree
红黑树(Red-Black Tree)是一种自平衡的二叉搜索树 BST(AVL 是另一种),其设计目标是在最坏情况下也能保证基本的动态集合操作(如插入、删除和查找)的时间复杂度为O(log n)。红黑树通过一系列的颜色和性质约束来维持其平衡性,这些约束包括:
- 节点是红色或黑色:每个节点都有一个颜色属性,可以是红色或黑色。
- 根节点是黑色:确保树的根始终为黑色,这有助于维持树的平衡。
- 所有叶子节点都是黑色:这里的叶子节点指的是树中的空节点(NIL节点),它们被假定为黑色。
- 红色节点的子节点必须是黑色(也称为“不能有两个连续的红色节点”):这个性质确保了在任何路径上,红色节点不会紧密相连,从而限制了树的高度。
- 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点:这个性质被称为“黑色平衡性质”,它确保了树在整体上保持平衡。
rb-tree 提供遍历操作和 iterators,按中序遍历遍历,便可以得到排序状态
不能用 iterator 去改变元素的 key(其有严谨的排列规则)
rb-tree 提供两种 insertion 操作:
insert_unique()
和insert_equal()
,前者表示 key 独一无二,后者表示 key 可重复
GCC2.9下:
1 | template<class Key, // key的类型 |
GCC4.9下:
_M_color 是 “枚举”(Enumeration)
3.3.1 set / multiset
set/multiset的value和key合一,value就是key
测试
1 | void test_multiset(long& value) |
安插元素是使用
insert()
,其位置由红黑树决定
容器自己有
c.find()
,其会比全局的::find()
快
运行结果:
随机数据填充容器:6609ms(其在填充的时候就进行排序了);直接搜索 ::find()
:203ms;c.find()
:0ms
深度探索
以 rb-tree 为底层结构,因此有——元素自动排序,key 与 value 和一
set / multiset 提供遍历操作和 iterators,按中序遍历遍历,便可以得到排序状态
禁止用 iterator 去改变元素的值(其有严谨的排列规则)
set的key 独一无二,其
insert()
操作用的 rb-tree 的:insert_unique()
multiset 的 key 可以重复,其
insert()
操作用的 rb-tree 的:insert_equal()
GCC2.9下:
1 | // set |
3.3.2 map / multimap
key是key,val是val
测试
1 | void test_multimap(long& value) |
c.insert(pair<long, string>(i, buf));
中 key 是从1~1000000,value 是随机取的,将其组合为 pair 插入
运行结果:
随机数据填充容器:4812ms(其在填充的时候就进行排序了);c.find()
:0ms
深度探索
以 rb-tree 为底层结构,因此有——元素自动排序
map/ multimap 提供遍历操作和 iterators,按中序遍历遍历,便可以得到排序状态
不能用 iterator 去改变元素的key(其有严谨的排列规则),但可以用 iterator 去改变元素的 data
因此 map / multimap 将 user 指定的 key_type 设定成
const
map的key 独一无二,其
insert()
操作用的 rb-tree 的:insert_unique()
multimap 的 key 可以重复,其
insert()
操作用的 rb-tree 的:insert_equal()
GCC2.9下:
1 | template <class Key, // key的类型 |
map 的插入元素有特殊写法:
c[i] = string(buf)
,其中i
就是 key;multimap没有map 的
[]
功能:访问元素: 如果指定的键存在于映射中,
map[key]
将返回与该键关联的 data;如果键不存在,map[key]
将自动创建一个新的键值对,key 为指定的 key,data 为默认 data,并返回这个默认 data
3.3.3 HashTable
元素的位置 = key % bucket大小
bucket vector 的大小为质数
当元素个数大于 bucket 的总数时,bucket vector 扩充并重新打散放在新计算的 bucket 中(rehashing 很花时间)—— bucket 一定比元素多
在扩充时,按 vector 扩充为2倍大小,但会选择靠进这个数的一个质数做新的大小
GCC2.9下:
1 | template <class Value, // Value里包含key和date |
Hash函数:
给传进来的参数生成一个编号
偏特化写不同类型的 hash 函数,下图都是数值类型,直接返回就可以
下图对 c 风格的字符串做了处理(也可以自己设计),来生成 hash code
注意:老版本STL没有提供现成的 string 类型的 hash 函数
3.3.4 unordered容器
测试
1 | void test_unordered_multiset(long& value) |
运行结果:
随机数据填充容器:4406ms;直接搜索 ::find()
:109ms;c.find()
:0ms;前二十个 bucket 中只有一个有24个元素
深度探索
4 分配器
4.1 测试
分配器都是与容器共同使用的,一般分配器参数用默认值即可
1 | list<string, allocator<string>> c1; |
不建议直接用分配器分配空间,因为其需要在释放内存时也要指明大小
1 | int* p; |
4.2 源码解析
VC6下:allocator 中有 allocate
,deallocate
其分别用函数 ::operator new
和 ::operator delete
来调用 c 中的 malloc 和 free
1 | pointer allocate(size_type _N, const void*){...} // 后面一个参数只是用来指明类型的 |
这里经过包装还是调用的 malloc 和 free,其执行效率变慢;且如果申请的空间比较小,会有较大比例的额外开销(cookie,调试模式所需空间等等)
GCC2.9 下:其容器都是调用的名叫 alloc 的分配器
其从0到15有一共16个链表,分别代表8字节到168字节,例如 #0 的位置用 malloc 要一大块内存,然后做切割,切成一块一块的8字节空间*不带cookie,用单向链表穿起来;当要申请6字节的大小的空间时,其就会到 #0 中占用一块 —— 节省空间
在 GCC4.9 中各个容器又用回了 allocator,而上面的 alloc 变成了
__poll_alloc
5 迭代器
迭代器必须能回答算法的所有提问,才能搭配该算法的所有操作
5.1 迭代器的设计准则
Iterator 必须提供5种 associated type(说明自己的特性的)来供算法来识别,以便算法正确地使用 Iterator
1 | template <class T, class Ref, class Ptr> |
但当 Iterator 并不是 class 时,例如指针本身,就不能 typedef
了 —— 这时就要设计一个 Iterator Traits
Traits:用于定义类型特征的信息,从而在编译时根据类型的不同进行不同的操作或处理 —— 类似一个萃取机(针对不同类型做不同操作:偏特化)
1 | // I是class iterator进 |
除了 Iterator Traits,还有很多其他 Traits
5.2 迭代器的分类
迭代器的分类对算法的效率有很大的影响
- 输入迭代器 input_iterator_tag:istream迭代器
- 输出迭代器 output_iterator_tag:ostream迭代器
- 单向迭代器 forward_iterator_tag:forward_list,hash类容器
- 双向迭代器 bidirectional_iterator_tag: list、红黑树容器
- 随机存取迭代器 random_access_iterator_tag:array、vector、deque
用有继承关系的class实现:
- 方便迭代器类型作为参数进行传递,如果是整数的是不方便的
- 有些算法的实现没有实现所有类型的迭代器类别,就要用继承关系去找父迭代器类别
1 | struct input_iterator_tag {}; |
5.3迭代器对算法的影响
1.算法 distance 将会按照迭代器的类别进行不同的操作以提升效率
- 如果迭代器可以跳,直接
last - first
即可 - 如果迭代器不能跳,就只能一步一步走来计数
两者的效率差别很大
但如果迭代器类别是
farward_iterator_tag
或者bidirectional_iterator_tag
,该算法没有针对这种类型迭代器实现,就可以用继承关系来使用父类的实现(继承关系——“is a” 子类是一种父类,当然可以用父类的实现)
2.算法 copy 将经过很多判断筛选来找到最高效率的实现
其中用到了 Iterator Traits 和 Type Traits 来进行筛选
has trivial op=() 是指的有不重要的拷贝赋值函数(例如复数用的自带的拷贝赋值函数)
注意:由于 output_iterator_tag(例如 ostream_iterator)是 write-only,无法用
*
来读取内容,所以在设计时就需要再写个专属版本
在源码中,算法都是模板函数,接受所有的 iterator,但一些算法只能用特定的 iterator,所以其会在模板参数的名称上进行暗示:
6 算法
算法的标准样式:需要传进去两个指针
6.1 算法源码
算法一般最后一个参数会允许我们传入一个函数对象,使得原来的函数对元素的操作有一定的规则
6.1.1 accumulate
两个版本:
元素累加到 init 上
1
2
3
4
5
6
7template <class InputIterator, class T>
T accumulate(InputIterator first, InputIterator last, T init)
{
for (; first != last; ++first)
init = init + *first; // 累加到init
return init;
}元素累运算到 init 上
1
2
3
4
5
6
7template <class InputIterator, class T, class BinaryOperation>
T accumulate(InputIterator first, InputIterator last, T init, BinaryOperation binary_op)
{
for (; first != last; ++first)
init = binary_op(init, *first); // 累运算到init上
return init;
}
这里可以用任意的二元操作(可以是函数,也可以是仿函数)
测试:
1 |
|
6.1.2 for_each
让范围里的所有元素都依次做同一件事情
Function 可以是函数也可以是仿函数
1 | template <class InputIterator, class Function> |
与C++11中的 range-based for statement 差不多
6.1.3 replace…
replace
:范围内的所有等于 old_value 的,都被 new_value 取代1
2
3
4
5
6
7
8
9template <class ForwardIterator, class T>
void replace(ForwardIterator first, ForwardIterator last,
const T& old_value, const T& new_value)
{
for (; first != last; ++first)
{
if (*first == old_value) *first = new_value;
}
}replace_if
:范围内所有满足pred()
为 true 的元素都被 new_value 取代1
2
3
4
5
6
7
8
9template <class ForwardIterator,class Predicate, class T>
void replace_if(ForwardIterator first, ForwardIterator last,
Predicate pred, const T& new_value)
{
for (; first != last; ++first)
{
if (pred(*first)) *first = new_value;
}
}replace_copy
:范围内的元素全部 copy 到新地方,其中所有等于 old_value 的,都被替代为 new_value1
2
3
4
5
6
7
8
9
10template <class InputIterator, class OutputIterator, class T>
OutputIterator replace_copy(InputIterator first, InputIterator last,
OutputIterator result, const T& old_value, const T& new_value)
{
for (; first != last; ++first, ++result)
{
*result = (*first == old_value) ? new_value : *first;
}
return result;
}
6.1.4 count…
count
:在范围中计数值等于 value 的个数1
2
3
4
5
6
7
8
9
10
11template <class InputIterator, class T>
typename iterator_traits<InputIterator>::difference_type // 返回类型
count (InputIterator first, InputIterator last, const T& value)
{
typename iterator_traits<InputIterator>::difference_type n = 0;
for (; first != last; ++first)
{
if (*first == value) ++n;
}
return n;
}count_if
:在范围中计数满足条件pred()
的个数1
2
3
4
5
6
7
8
9
10
11template <class InputIterator, class Predicate>
typename iterator_traits<InputIterator>::difference_type // 返回类型
count_if (InputIterator first, InputIterator last, Predicate pred)
{
typename iterator_traits<InputIterator>::difference_type n = 0;
for (; first != last; ++first)
{
if (pred(*first)) ++n;
}
return n;
}
- 容器不带成员函数
count()
:array,vector,forward_list,deque- 容器自带成员函数
count()
:set / multiset,map / multimap,unordered_set / unordered_multiset,unordered_map / unorderd_multimap —— 所有关联式容器
6.1 5 find…
find
:在范围内找到值等于 value 的元素1
2
3
4
5
6template <class InputIterator, class T>
InputIterator find(InputIterator first, InputIterator last, const T& value)
{
while (first != last && *first != value) ++first;
return first;
}find_if
:在范围内找到满足pred()
的元素1
2
3
4
5
6template <class InputIterator, class Predicate>
InputIterator find_if(InputIterator first, InputIterator last, Predicate pred)
{
while (first != last && !pred(*first)) ++first;
return first;
}
都是循序查找,效率低
- 容器不带成员函数
find()
:array,vector,forward_list,deque- 容器自带成员函数
find()
:set / multiset,map / multimap,unordered_set / unordered_multiset,unordered_map / unorderd_multimap —— 所有关联式容器
6.1.6 sort
源码复杂
测试:
1 | // 函数 |
- 容器不带成员函数
sort()
:array,vector,deque,所有关联式容器(本身就排好序了)- 容器自带成员函数
sort()
:list,forward_list(只能用自带)
reverse iterator:
其中用的是 reverse_iterator —— iterator adapter
6.1.7 binary_search
二分查找是否存在目标元素(并不给予位置),使用前必须先排序;其主要使用 lower_bound()
来找到能放入 val 的最低位置,再判断该元素是否存在
1 | template <class ForwardIterator, class T> |
lower_bound()
:用于在有序序列中查找==第一个大于等于==该值的元素(包括目标值本身),并返回一个指向该位置的迭代器
- 如果目标值在序列中多次出现,返回第一个出现的位置
- 如果目标值在序列中不存在,它将返回指向比目标值大的第一个元素位置,或者返回
last
upper_bound()
:用于在有序序列中查找==第一个大于==该值的元素(不包括目标值本身),并返回一个指向该位置的迭代器
- 如果目标值在序列中多次出现,返回第一个大于目标值的位置
- 如果目标值在序列中不存在,它将返回与
lower_bound()
一样的位置一样是前闭后开的原则,且他们都用的是二分查找的方法
7 仿函数
仿函数专门为算法服务,设计成一个函数/仿函数是为了能传入算法
STL中的每个仿函数都继承了 binary_function
/ unary_function
—— 融入到STL中
STL规定每个 Adaptable Function(之后可以改造的函数)都应该继承其中一个(因为之后 Function Adapter 将会提问)
1 | // 一个操作数的操作,例如“!” |
仿函数是我们自己可能会写的,所以自己写的时候,如果想要融入STL,就要继承上面的两个之一
8 适配器
- 适配器 Adapter 只是一个小变化,比如改个接口,函数名称等等
- 其出现在三个地方:仿函数适配器,迭代器适配器,容器适配器
- 可以使用继承 / 复合的两种方式实现,STL中都用复合
其思想就是将该记的东西记起来,然后看要怎么样去改造它,以便日后使用
8.1 容器适配器
stack,queue 都是属于 deque 的 Adapter
比如 stack 中将 deque 的 push_back
改名为 push
8.2 函数适配器
8.2.1 binder2nd
binder2nd —— 绑定第二参数
这个例子的OP就是less<int>
1.先是传给bind2nd函数
2.bind2nd生成binder2nd对象
3.对象里面记录op是less<int>,第二参数是40
4.然后都搞定以后返回一个函数对象op(其实返回的是op调用小括号重载运算符,这是一个函数对象)给到count_if作为第三参数,然后继续执行程序
1 | // 数范围内所有小于40的元素个数 |
当然还有:binder1st —— 绑定第二参数
新型适配器:bind
,代替了 bind1st
,bind2nd
,binder1st
,binder2nd
8.2.2 not1
not1 —— 否定
1 | // 数范围内所有大于等于40的元素个数 |
8.2.3 bind
C++11提供的 Adapter,其可以绑定:
- functions
- function objects
- member functions
- data members
测试函数 / 对象
1 | // functions |
占位符 placeholders:
1 using namespace std::placeholders;提供了
_1
,_2
,_3
,·······下面的的
_1
指的是被绑函数中的第一个参数
binding functions / function objects 测试
单纯将两个整数
10
,2
绑定到my_divide
1
2auto fn_five = bind(my_divide, 10, 2);
cout << fn_five() << endl; // 5.0用
_1
占据第一参数,第二参数绑定2,即x/2
1
2auto fn_half = bind(my_divide, _1, 2);
cout << fn_half(10) << endl; // 5.0用
_1
占据第一参数,_2
占据第二参数,即y/x
1
2auto fn_invert = bind(my_divide, _2, _1);
cout << fn_invert(10, 2) << endl; // 0.2给
bind
指定了一个模板参数int
,将my_divide
的返回类型变为int
,即int(x/y)
1
2auto fn_rounding = bind<int>(my_divide, _1, _2);
cout << fn_rounding(10, 3) << endl; // 3
binding member functions / data members 测试
MyPair ten_two {10, 2};
用C++11的新语法定义一个实例当函数是类的成员函数时,直接使用函数名并不能获取其地址(因为成员函数隐含地需要一个类的实例来调用),而是需要使用特定的语法来获取指向成员函数的指针。这时,
&
的使用就显得尤为重要了。对于普通的全局函数或静态成员函数,直接使用函数名和加&
都可以获取其地址。绑定 member functions,由于成员函数有
this
,所以_1
就相当于this
,即x.multiply()
1
2auto bound_memfn = bind(&MyPair::multiply, _1);
cout << bound_memfn(ten_two) << endl; // 20绑定 data members,绑定是谁的数据
把实例
ten_two
绑定到a
,即ten_two.a
1
2auto bound_memdata = bind(&MyPair::a, ten_two);
cout << bound_memdata() << endl; // 10用占位符绑定,即
x.a
1
2auto bound_member_data2 = bind(&MyPair::b, _1);
cout << bound_member_data2(ten_two) << endl;
8.3 迭代器适配器
8.3.1 reverse_iterator
注意:对逆向迭代器取值,就是取其所指正向迭代器的前一个位置
1 | template <class Iterator> |
8.3.2 inserter
对于 copy(InputIterator first, InputIterator last, OutputIterator result)
,其会不管 OutputIterator
后是否有充裕空间,对 result
开始依次赋值
但如果使用 inserter
,就会有如下用 copy
实现的插入的效果
1 | list<int> foo, bar; |
注:其是 output_iterator_tag
其实现原理核心就是 —— 对 =
的操作符重载
1 | insert_iterator<Container>& |
8.4 X适配器
8.4.1 ostream_iterator
其会将 copy
变为一个输出工具,分隔符是 ,
1 | vector<int> vec = { 1,2,3,4,5,6,7,8,9,10 }; |
其核心依然是操作符重载,这样就相当于 cout<<*first;
cout<<",";
1 | basic_ostream<charT,traits>* out_stream; |
其中 out_stream
存的 cout
,delim
存的 ,
8.4.2 istream_iterator
例一:
在创建 iit
的时候就已经把所有的键盘输入读进去了,之后就是一个一个取出来赋值给 value 的操作
1 | double value1, value2; |
例二:
从 cin
读 data,插入到目的容器
1 | istream_iterator<double> eos; // end of stream iterator |
原理依旧是大量的**操作符重载 **—— 就可以改变原函数的作用
1 | basic_istream<charT, traits>* in_stream; |
9 STL周围
9.1 万用Hash Function
Hash Function的常规写法:其中 hash_val
就是万用Hash Function
1 | class CustumerHash |
还可以直接用函数实现,或者写一个
hash
的特化版本
原理:
通过三个函数重载实现从给入数据中逐一提取来不断改变 seed
1 | // 第一个函数 首先进入该函数 |
最后的seed就是hash code
C++11中 variadic templates:
从传入的内容(任意个数,任意元素类型)分为一个和其他,递归再分为一个和其他······
0x9e3779b9:是黄金比例!
C++11及其后续版本并没有为每个类型都自动提供哈希函数,但它确实为一些标准类型提供了std::hash
的特化,并且允许开发者为用户定义的类型提供自己的std::hash
特化。
9.2 Tuple
可以将一些东西组合在一起
9.2.1 用例
创建
tuple
1
2
3
4
5tuple<string, int, int, complex<double>> t;
tuple<int, float, string> t1(41, 6.3, "nico");
auto t2 = make_tuple(22, 44, "stacy");输出
tuple
1
2
3// 输出t1中的第一个
cout << get<0>(t1) << endl; // 41
cout << t << endl; // 在VS2022上并没有<<的重载 需要自己写一个重载运算
1
2
3
4
5
6t1 = t2;
if(t1 < t2) // 以特定的方式进行的比较 里面的东西拿出来一个一个比
{
...
}绑定解包
1
2
3
4
5
6tuple<int, float, string> t3(77, 1.1, "more light");
int i;
float f;
string s;
tie(i, f, s) = t3; // i == 77, f == 1.1, s == "more light"// tuple里有多少类型 tuple_size< tuple<int, float, string> >::value; // 3 // 取tuple里面的类型,前面一堆代表float tuple_element<1, TupleType>::type fl = 1.0; // float fl = 1.0;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
##### 9.2.2 原理
依然是使用 *variadic templates*,通过递归继承,不断从 `...` 中提取内容
```cpp
// 空的tuple
template <> class tuple<> {}; // 直到取完
// tuple主体
template <typename Head, typename... Tail>
class tuple<Head, Tail...>
: private tuple<Tail...> // 递归继承
{
typedef tuple<Tail...> inherited;
public:
tuple() {}
tuple(Head v, Tail... vtail)
: m_head(v), inherited(vtail...) {}
...
protected:
Head m_head; // 每次取出的元素
};
👈🏻不断的继承就可以实现不同类型的组合了
其余函数:
1 | ... |
一般不这么用
出去head的41,剩下的都是tail部分,所以出来的是6.3
9.3 type traits
9.3.1 用例
GCC2.9中:
默认的 __type_traits
进行了一系列泛化的设定(trivial 是不重要的意思)
1 | struct __true_type {}; |
还会通过特化来实现针对不同类型的设定,例
1 | template <> struct __type_traits<int> |
C++11中:
有了很多个 type traits,可以回答更多问题
测试:
1 | cout << is_void<T>::value << endl; |
不论是什么类型都可以自动检测它的 traits,非常厉害!(里面有虚函数——就能自动检测出它有多态性)
9.3.2 原理
模板的作用
例 is_integral
依然是采用的一种问答的方式实现的
1 | template <typename _Tp> |
首先 remove_cv
(const
和 volatile
)
1 | // 通过偏特化实现remove const |
再通过 __is_intagral_helper
进行问答
1 | // 通过偏特化实现 |
其他深入 class 内部的一些 traits 比如是否有虚函数,是否是一个类,是否是POD等等,其实现可能都与编译器有关
9.4 move
moveable class 中有:
1 | // move ctor |
是==浅拷贝==,并且把之前的指向去除了
对于 vector 这样的容器,其用 move 就只是 swap 了三根指针,非常快!
move 之后原来的东西不能再使用
拿数据插入容器,用临时对象,编译器看到就会自动使用 move 版本的
MyString C11(C1);
时,创建了一个实例 C11,编译器就不知道是否能用 move,就需要自己MyString C12(move(C1));
使用 move,但注意之后==一定不能用原来的C1
==
&&
(右值引用)这是C++11引入的特性,右值引用用于处理临时对象或将资源所有权转移给其他对象,以提高性能和资源管理