文章目录
- 零、标准模板库 STL
- 一、容器 (Container)
- 1.序列式容器
- (1)vector
- 2.五种遍历
- 10.vector的迭代器失效问题
- (2)deque
- (3)list
- 2.关联式容器
- (1)set
- 4.set的查找
- (2)`find()`
- 8.set中存储自定义类型:三种方法
- (2)multiset
- 7.multiset的特殊操作:bound系列函数
- (3)map
- (4)multimap
- (5)关联式容器总结
- 3.无序关联式容器
- (0)哈希
- (1)unordered_set
- 6.unordered_set针对自定义类型的改写
- (2)unordered_multiset
- (3)unordered_map
- (4)unordered_multimap
- (5)无序关联式容器总结
- 4.容器的选择
- 二、迭代器 (iterator)
- 1.概念
- 2.迭代器产生的原因
- 3.迭代器的类型
- (1)双向迭代器
- (2)随机访问迭代器
- 4.流迭代器
- (1)输出流迭代器
- (2)输入流迭代器
- 三、算法
- 1.概念
- (1)概述
- (2)头文件
- (3)分类
- (4)一元函数、一元断言/一元谓词
- 2.copy
- 3.for_each的使用
- 4.lambda表达式:匿名函数 (lambda函数)
- 5.remove_if的使用
- 四、适配器
- 1.迭代器适配器
- (1)反向迭代器:reverse_iterator
- (2)迭代器适配器:三组插入迭代器,是特殊的输出迭代器
- 2.容器适配器
- (1)stack (栈)
- (2)queue (队列)
- (3)priority_queue (优先级队列)
- 3.函数适配器
- (1)函数绑定器:bind1st、bind2nd、bind
- ①bind1st和bind2nd的使用
- ②bind函数的使用
- 7.占位符
- (2)mem_fun
- 五、函数对象
- 六、空间配置器 (allocator) 【面试加分项】
- 1.概述
- 2.四个函数
- 3.两级空间配置器
- (1)一级空间配置器
- (2)二级空间配置器
- 4.空间配置器的源码剖析
- (1)allocate()
- (2)deallocate()
- (3)construct()
- (4)destroy()
C++编程思想:
1.C语言的:面向过程编程
2.C++的:面向对象编程
3.STL的:泛型编程
零、标准模板库 STL
STL六大组件按顺序分别是:
①容器(Containers):数据结构,用于存储和组织数据。
②算法(Algorithms):操作容器中的元素的函数,如排序、搜索等。
③迭代器(Iterators):用于遍历容器中的元素。
④仿函数(Functors):行为类似函数的对象,通常用于自定义算法中的操作。
⑤适配器(Adapters):修改容器、迭代器或仿函数行为的工具。
⑥分配器(Allocators):负责内存分配和管理。
1.容器:用来存放数据,也称为数据结构。
①序列式容器:vector、list、deque
②关联式容器:set、map
③无序关联式容器:unordered_set、unordered_map
2.迭代器:泛型指针,用来访问容器中的元素。存在失效的情况。
3.算法:用来操纵容器中的元素。在STL中,这些算法都是普通函数(非成员函数)
4.适配器:当算法和容器不匹配时,用适配器进行匹配。起到适配的效果。
①容器的适配器:stack、queue、priority_queue
②迭代器的适配器
③函数适配器:bind、mem_fn、bind1st、bind2nd
5.函数对象 (仿函数):类重载了函数调用运算符(),对象就可以像函数一样使用。起到定制化操作。比如删除器deleter,对于智能指针可以定制删除器去回收FILE *
6.空间配置器 Allocator:进行申请和释放空间。所有与空间相关的操作都在该类中。(用法+原理+源码)
一、容器 (Container)
1.序列式容器
三种序列式容器 vector(动态数组)、deque(双端队列)、list(双向链表):
①初始化容器对象:都支持五种初始化方式
②遍历容器中的元素:vector、deque支持三种遍历,list支持两种遍历。list不支持下标访问。
③在容器的尾部进行插入和删除:都支持 push_back 和 pop_back
④在容器的头部进行插入和删除:仅deque和list支持,vector不支持
⑤在容器的任意位置插入和删除:vector、deque、list都支持四种insert()。对于list,每次插入完成后,迭代器都只与结点有关;deque,要看插入的是前一半还是后一半,因为元素挪动是不一样的,迭代器还指向原位置,可能*it输出不同;对于vector,插入时底层可能发生扩容,造成迭代器失效,进而产生bug。
⑥清空元素:clear()。三者都有
⑦获取元素个数:size()三者都有。
获取容器空间大小:capacity()只有vector有。
⑧回收多余空间:shrink_to_fit()只有vector和deque有。
⑨交互容器中的内容:swap()
。vector、deque、list都支持,swap函数只能用于相同类型的STL容器。
⑩更改容器的大小:resize()
。vector、deque、list都支持
11.获取第一个元素:front()
获取最后一个元素:end()
12.emplace系列函数:直接在容器中生成对象(只有一次构造),避免创建临时对象后再拷贝到容器中 (一次构造+一次拷贝)
(1)vector
1.初始化容器对象(五种):创建vector
//1.vector的创建和初始化
//1.创建无参对象
vector<int> vec;
//2.count个value
vector<int> vec2(3,6);
//3.迭代器范围
vector<int> vec3(vec2.begin(), vec2.end()); //[,)左闭右开的区间
//4.拷贝构造或移动构造函数
vector<int> vec4 = vec3;
vector<int> vec44 = std::move(vec4); //move后,vec4为空
//5.初始化列表 { }
vector<int> vec5{10,9,8,7,6}; //直接初始化
vector<int> vec55 = {10,9,8,7,6}; //拷贝初始化
vec4 = {10,9,8,7}; //赋值操作必须用等号
2.五种遍历
(1)下标
//1.下标
for(size_t idx = 0; idx != number.size(); ++idx){
cout << number[idx] << " ";
}
cout << endl;
(2)迭代器
//2.迭代器
vector<int>::iterator it; //未初始化迭代器
for(it = number.begin(); it != number.end(); ++it){
cout << *it << " ";
}
cout << endl;
vector<int>::iterator it2 = number.begin(); //初始化迭代器
for( ; it2 != number.end(); ++it2){
cout << *it2 << " ";
}
cout << endl;
for(auto it3 = number.begin(); it3 != number.end(); ++it3){ //初始化迭代器
cout << *it3 << " ";
}
cout << endl;
(3)增强for循环
//3.增强for循环
for(auto &elem : vec){ //引用:避免拷贝
cout << elem << " ";
}
cout << endl;
变成函数 (函数模板)
template <typename Container>
void display(const Container &con){
for(auto &elem : con){
cout << elem << " ";
}
cout << endl;
}
(4)输出流迭代器
using std::ostream_iterator;
//4.第四种遍历方式:利用输出流迭代器 (遍历容器中的元素)
copy(vec.begin(), vec.end(), ostream_iterator<int>(cout, " ")); //右值临时对象
cout << endl;
(5)for_each() + lambda表达式
//5.第五种遍历: for_each() + lambda表达式 //头文件<algorithm>
for_each(vec.begin(), vec.end(), [](int value){ cout << value << " "; });
//只用for_each,没配合lambda表达式就比较麻烦了。
void func(int value){
cout << value << " ";
}
void test(){
vector<int> vec= {1,4,7,9,5,2};
for_each(vec.begin(), vec.end(), func);
cout << endl;
}
3.尾部进行插入和删除 (三种序列式容器都支持)
push_back()
pop_back()
4.头部进行插入和删除 (仅deque、list支持,vector不支持头部增删)
push_front()
pop_front()
因为vector只有一段是开口的,内部是连续的,若在内部增删,则所有元素都需要向前或向后移动,时间复杂度为O(n)。
插入还可以用插入迭代器
5.vector的底层实现
三个指针: ( sizeof(vec) 等于 24 )
_M_start:指向第一个元素的位置
_M_finish:指向最后一个元素的下一个位置
_M_end_of_storage:指向当前分配空间的最后一个位置的下一个位置
6.vector的源码
vector的下标访问运算是不安全的,有越界的风险,但是at函数可以防止越界。所以vector中
下标访问
at函数
push_back
扩容:2倍
pop_back
7.获取vector第一个元素的首地址
8.vector的自动扩容:一个一个插入时,size()超过capacity()时,会两倍扩容。
9.在任意位置插入:insert()
(1)迭代器指向位置不变,所以输出的*it会改变。
(2)若发生了自动扩容,it还是指向旧空间,导致*it
输出可能是负数。这就是vector的迭代器失效问题。
(3)vector的insert的扩容:同resize()
①插入后 size() < capacity(),不需要扩容
②插入后 capacity() < size() < 2*capacity(),两倍扩容
③插入后 size() > 2*capacity(),则capacity()扩容到和size()一样大
10.vector的迭代器失效问题
1.vector的迭代器失效问题:
vector在进行插入后,底层发生了自动扩容。导致此时vector的内容已经转移到另一片空间,vector.end()已经改变。而其迭代器 vector<>::iterator it还指向原本的旧空间,就发生了迭代器失效的问题。
2.解决方案:每次插入后,或每次使用迭代器之前,重置迭代器。
it = vec.begin();
it += 2;
举例:
解决:重置迭代器
#include <iostream>
#include <vector>
using std::cout;
using std::endl;
using std::vector;
void test(){
vector<int> vec;
vec.reserve(2);
vec.push_back(111);
vec.push_back(222);
bool flag = true;
for(auto it = vec.begin(); it != vec.end(); ++it){
cout << *it << " ";
//打印出第一个数后,进入插入,发生扩容,重置迭代器。然后++it,打印第二个数
if(flag){
cout << "push_back(333)" << endl;
vec.push_back(333); //发生扩容,则迭代器失效
flag = false;
it = vec.begin(); //重置迭代器
}
}
cout << endl;
}
int main()
{
test();
return 0;
}
11.vector的删除:erase() (重要)
vector的erase()只有两种,没有set的删除指定元素。
erase(it)删除一个元素时,后面的元素会自动前移。
举例:vector删除连续重复元素:
//题意:删除vector中所有值为4的元素。
vector<int> vec = {1, 3, 5, 4, 4, 4, 4, 7, 8, 4, 9};
for (vector<int>::iterator it = vec.begin(); it != vec.end(); ++it){
if(4 == *it){
vec.erase(it);
}
}
//发现删除后有些4没有删除掉,可以推测出是什么原因吗?是那些4没有删除呢?
//答案:是因为vector删除的时候,后面的元素会自动前移一格。这时候再++it,
//就会漏掉删除位置后面的那个元素
//正确解法:
for (auto it = vec.begin(); it != vec.end();){
if (4 == *it){
vec.erase(it);
}else{
++it;
}
}
12.vector的元素清空:clear()
13.vector的回收多余空间:shrink_to_fit()
将capacity()减少到和size()相等。
14.交互两个vector中的内容:swap()
deque和list也支持swap()。
swap函数只能用于相同类型的STL容器。
15.vector更改容器的大小:resize()
deque、list也有resize()
resize()比capacity()大时,底层会发生扩容。
小于2倍capacity(),则两倍。大于两倍,则resize()。
16.vector的尾部插入自定义类型对象:emplace_back()
emplace_back()比起push_back()少一次拷贝构造,直接在容器内部构造对象,避免了临时对象的创建和拷贝操作。
一般情况是:构造临时对象、拷贝构造
emplace_back()的情况是:在容器内部直接构造对象。只有一次构造,没有拷贝。
deque、list也有emplace_back()
17.vector获取容器的第一个元素:front()
vector获取容器的最后一个元素:back()
(2)deque
经测试发现,deque的初始化、遍历、尾部插入和删除和vector相同。deque还支持头部增删。
1.deque的五种创建和初始化
//1.创建无参对象
deque<int> dq;
//2.count个value
deque<int> dq2(3,6);
//3.迭代器范围
deque<int> dq3(dq2.begin(),dq2.end());
//4.拷贝构造或移动构造函数
deque<int> dq4 = dq3;
deque<int> dq44 = std::move(dq4); //move后dq4为空
//5.初始化列表 { }
deque<int> dq5{11,12,13,14,15}; //直接初始化
deque<int> dq55 = {15,14,13,12,11}; //拷贝初始化
dq4 = dq5;
dq4 = {20,20,20}; //直接赋值必须用赋值号
5.deque的底层实现
deque是由多个片段组成的,片段内部是连续的,但是片段之间不连续的,分散的,多个片段被一个称为中控器的结构控制,所以说deque是在物理上是不连续的,但是逻辑上是连续的。
从继承图中可以看到:
(1)中控器其实是一个二级指针 _Tp** _M_map
,指向一个指针数组(即中控器数组),每个指针指向一个片段 (缓冲区)。size_t _M_map_size
表示中控器数组的大小。中控器数组满了也会扩容。
(2)deque的迭代器也不是一个简单类型的指针,其迭代器是一个类类型,deque有两个迭代器指针,一个指向第一个小片段,一个指向最后一个小片段。
其结构图如下:
_Tp** _M_map;
size_t _M_map_size;
deque,逻辑上是连续的,物理上片段是分散的
6.deque的源码
7.deque在中间位置插入:
在前面一半,移动前一半。
在后面一半,移动后一半。
迭代器指向是可能改变的,*it可能会变。
8.deque的元素清空:clear()
9.deque的回收多余空间:shrink_to_fit()
deque没有capacity()函数
10.deque的emplace:插入自定义类型对象,少一次拷贝构造
①emplace() 对应于 insert()
②emplace_back() 对应于 push_back()
③emplace_front() 对应于 push_front()
(3)list
list是双向链表。
经测试发现,list的构建、头部增删和vector相同。list支持头部增删。
特殊点:对于list,不支持下标访问运算符[]。
1.list的五种创建和初始化
//1.创建无参对象
list<int> ls;
//2.count个value
list<int> ls2(5,6);
//3.迭代器范围
list<int> ls3(ls2.begin(),ls2.end());
//4.拷贝构造或移动构造
list<int> ls4 = ls3;
list<int> ls44 = std::move(ls4);
//5.初始化列表 { }
list<int> ls5{1,2,3,4,5}; //直接初始化
list<int> ls55 = {5,4,3,2,1}; //构造初始化
5.list的底层实现
6.总结
①对于vector而言,前后元素的地址是完全连续的。
②对于deque而言,前后两个元素是逻辑上连续,物理上不连续
③对于list而言,前后两个元素是不连续的。
7.在容器的任意位置插入:在中间任意位置插入:insert()
插入完成后,list的迭代器指向不变,还是最初的元素。
//list的插入:尾部插入、首部插入、4种中间插入
void test(){
list<int> ls = {4,5,6,7};
//尾部插入
ls.push_back(8);
//头部插入
ls.push_front(1);
//遍历打印
display(ls); //1 4 5 6 7 8
//四种中间插入:insert()
//1.第一种中间插入:找一个迭代器位置,插入一个元素
auto it = ls.begin();
++it; //4
ls.insert(it, 2);
display(ls);
cout << "*it = " << *it << endl;
//2.第二种中间插入:找一个迭代器位置,插入count个元素
ls.insert(it, 2, 3);
display(ls);
cout << "*it = " << *it << endl;
//3.第三种中间插入:找一个迭代器位置,插入迭代器范围的元素
vector<int> vec = {999,1111};
ls.insert(it, vec.begin(), vec.end());
display(ls);
cout << "*it = " << *it << endl;
//4.第四种中间插入:找一个迭代器位置,插入大括号范围内的元素
it = ls.begin();
++it; //2
ls.insert(it, {500, 400, 300});
display(ls);
cout << "*it = " << *it << endl;
}
8.list的迭代器,只能++it,不支持it += 2。只能一次一次偏移。
9.list的删除
//list的删除
void test2(){
//删除重复连续元素:删除list中所有的2
list<int> ls = {1,2,2,2,3,4,5,2,2,2,6,7,8,2,2,9};
for(auto it = ls.begin(); it != ls.end(); ){
if(*it == 2){
it = ls.erase(it);
}else{
++it;
}
}
display(ls);
}
10.list清空函数:clear()
11.list没有shrink_to_fit(),list也没有capacity()。因为有size()。
12.list的特殊操作
(1)反转:reverse()
list<int> ls{1,2,3,4,5,6};
ls.reverse(); //list反转
display(ls); //6,5,4,3,2,1
(2)排序:sort()
ls.sort(); //无参,默认从小到达
ls.sort(std::less<int>()); //从小到大。要加小括号,代表是创建一个对象
ls.sort(std::greater<int>()); //从大到小。要加小括号,代表是创建一个对象
函数参数里传的是对象,模板参数里传的是类型
加小括号,代表是创建一个对象。
自定义比较逻辑:
(3)去除连续重复元素:unique()
直接使用,只能去除连续重复的元素。间隔的重复元素无法去除。
若想要去除所有重复元素,需要先排序。
ls.sort();
ls.unique();
(4)合并链表:merge()
如果要求合并后自动有序(升序),则要求两个链表合并前也各自有序(升序)。
两个链表合并之后,被合并的链表就为空了。
(5)移动元素:splice()
①全部移动
number.splice(it, other); //1.全部移动
②移动一个元素
number.splice(it, other, it2); //2.移动一个指定位置的一个元素
③将迭代器范围内的元素进行移动
number.splice(it, other, it2, it3); //左闭右开 [,),右边取不到
代码链接:https://github.com/WangEdward1027/STL/blob/main/list/list_splice.cpp
举例:LRU算法,可以直接使用splice()
2.关联式容器
(1)set
#include <set>
using std::set;
//set的类模板共有3个模板参数,后两个模板参数有默认值
template< class Key,
class Compare = std::less<Key>,
class Allocator = std::allocator<Key> >
class set
1.四种初始化方式。
比起vector少了第二种,插入count个相同元素。因为set会去重。
2.两种遍历方式
比起vector少了第一种。set不支持取下标。
3.set的特点:
①去重,key值唯一
②按key值升序排序
③set的底层实现:红黑树
4.set的查找
(1)count()
返回set中,该元素的个数,为0或1
(2)find()
若能找到该元素,返回指向它的迭代器。
若找不到,返回尾后迭代器。
auto it = myset.find(7);
if(it != myset.end()){
cout << "查找成功" << *it << endl;
}else{
cout << "查找失败,该元素不在set中" << endl;
}
5.set的插入:insert()
三种插入,比起vector少了插入count个元素
①set插入一个元素
pair<set<int>::iterator, bool> ret = s.insert(7);
if(ret.second){
cout << "插入成功: " << *ret.first << endl;
}else{
cout << "插入失败,该元素存在set中" << endl;
}
②set插入迭代器范围的元素
//2.插入迭代器范围的元素
cout << "set迭代器范围的元素" << endl;
vector<int> vec{8,9,10};
s.insert(vec.begin(), vec.end());
display(s);
③set插入大括号范围的元素
s.insert({11,12,13,14,15});
多个返回结果:tuple (可变参数)
6.set的三种删除:erase()
①删除指定元素
s.erase(10); //删除元素10
②删除迭代器指定位置
s.erase(it);
③删除迭代器范围的元素
s.erase(it,it2);
代码链接:https://github.com/Edward/STL/blob/main/set/set_insert.cpp
7.set不支持下标访问,不支持通过*it 进行修改。
因为set的底层是红黑树。
(RBT是一个稳定的数据结构,为了维持稳定性,所以不支持修改,read-only)
报错太多,可以使用错误重定向,然后搜索error
错误重定向:2>
8.set中存储自定义类型:三种方法
方法一:模板的特化版本:模板特化 (优先于方法二)
方法二:运算符重载的版本:重载operator<运算符,可以比较Point类型
方法三:函数对象的版本:自己写Compare类,创建set的的时候<>里需要写第二个模板参数。若传第二个参数则一定走方法三,若不传则一定不走。
代码链接:https://github.com/WangEdward1027/STL/blob/main/set/set_custom_type.cpp
方法一:
写库的人,写法:
特化写法:
//方法一:模板特化的版本:模板特化
//如果第二个模板参数不传,走std::less,则模板特化的优先级高于重载operator<
//库里的std::less源码是这样写的
/* namespace std{ */
/* template<class T> */
/* struct less */
/* { */
/* bool operator()(const T &lhs, const T &rhs) const{ */
/* return lhs < rhs; */
/* } */
/* }; */
/* } */
//我们对其进行类模板特化:类模板的全特化
namespace std{
template<>
struct less<Point>
{
bool operator()(const Point &lhs, const Point &rhs) const{
/* return lhs < rhs; */
cout << "template<> struct less<Point>" << endl;
if(lhs.getDistance() < rhs.getDistance()){
return true;
}else if(lhs.getDistance() == rhs.getDistance()){
if(lhs.getX() < rhs.getX()){
return true;
}else if(lhs.getX() == rhs.getX()){
if(lhs.getY() < rhs.getY()){
return true;
}else{
return false;
}
}else{
return false;
}
}else{
return false;
}
}
};
}
方法二:重载operator<运算符
//方法二:运算符重载的版本:重载operator<运算符,可以比较Point类型
//全局普通函数声明为友元形式重载operator<
bool operator<(const Point &lhs, const Point &rhs){
cout << "bool operator<"<< endl;
//先比距离,再比横坐标,再比纵坐标
if(lhs.getDistance() < rhs.getDistance()){
return true;
}else if(lhs.getDistance() == rhs.getDistance()){
if(lhs._ix < rhs._ix){
return true;
}else if(lhs._ix == rhs._ix){
if(lhs._iy < rhs._iy){
return true;
}else{
return false;
}
}else{
return false;
}
}else{
return false;
}
}
hypot:可以直接得到两个数的平方和再开根
#include <math.h>
float getDistance() const{
return hypot(_ix, _iy); //求点到原点的距离
}
方法三:自定义比较类型
//方法三:函数对象的版本:自己写Compare类
struct ComparePoint{
bool operator()(const Point &lhs, const Point &rhs) const {
cout << "struct ComparePoint" << endl;
if(lhs.getDistance() < rhs.getDistance()){
return true;
}else if(lhs.getDistance() == rhs.getDistance()){
if(lhs._ix < rhs._ix){
return true;
}else if(lhs._ix == rhs._ix){
if(lhs._iy < rhs._iy){
return true;
}else{
return false;
}
}else{
return false;
}
}else{
return false;
}
}
};
void test(){
/* set<Point> number = { */
set<Point, ComparePoint> number = {
Point(1,0),
Point(0,1),
Point(1,1),
Point(1,1),
Point(2,0),
};
display(number);
}
(2)multiset
#include <set>
using std::multiset;
1.四种创建
和set一样
2.两种遍历
3.特点
①multiset:key值可以重复的set
②multiset不支持下标,底层是红黑树
4.查找
①count()
②find()
5.插入:insert()
必定成功,返回值就是迭代器
6.删除:erase()
7.multiset的特殊操作:bound系列函数
①lower_bound()
:返回第一个大于等于(不小于)所给定的key值的迭代器
②upper_bound()
:返回第一个大于所给的的key值的迭代器
③equal_range()
:返回等于给的key值的范围。是两个迭代器,返回一个 std::pair,其中包含两个迭代器:first:指向第一个大于等于(不小于) value 的元素。second:指向第一个大于 value 的元素。即pair<lower_bound(),upper_bound()>。
8.针对于自定义类型的写法
对于multiset而言,也需要实现第二个模板参数Compare,实现方法与set完全一样。即三种形式:模板的特化、运算符重载、函数对象。
(3)map
1.四种创建map:
(1)三种构建pair的方法:
①大括号
{1,"beijing"};
②pair< , >( ) 直接构建临时pair对象
pair<int,string>(4,"wd");
③make_pair
make_pair(2,"wuhan");
2.map的特征:
①存放的是key-value类型
②key值唯一,会进行去重
③按照key值进行升序排列
④map的底层也是红黑树
降序排序:
map<int,string,std::greater<int>> number = { };
3.查找
①count()
②find()
4.插入
(1)三种insert (和set一致)
①插入一个元素,返回值是pair
②插入迭代器范围内的元素,返回值是迭代器
③插入大括号范围内的元素,返回值是迭代器
(2)emplace插入
5.map的删除操作
①按键删除 (erase(const Key& key))
②按迭代器删除 (erase(iterator position))
③按迭代器范围删除 (erase(iterator first, iterator last)
// 删除键为"banana"的元素
int numRemoved = wordFrequency.erase("banana");
// 删除指向"banana"的迭代器所指向的元素
auto it = wordFrequency.find("banana");
if (it != wordFrequency.end()) {
wordFrequency.erase(it);
}
// 删除从"banana"到"date"之前的元素
auto first = wordFrequency.find("banana");
auto last = wordFrequency.find("date");
if (first != wordFrequency.end() && last != wordFrequency.end()) {
wordFrequency.erase(first, last);
}
6.map的下标操作
(1)取下标,mymap[key],得到value
(2)key值存在,就是查找。key值不存在,就会插入key和空的value
(3)可以根据下标进行修改
(4)map的下标操作,只重载了非const版本的operator[]。则const Map无法使用下标访问。
number = "test2"; //修改
运算符重载,本质
number[6] = "test2"; //修改
number.operator[](6).operator=("test2");
7.map<Key,Value> 若Key是自定义类型,Key不能进行比较大小,则和set针对自定义类型一样,用三种方法进行改写:模板特化、运算符重载、传函数对象
(4)multimap
1.multimap:Key值不唯一,可以重复
2.与map的不同:
(1)插入必定成功
(2)因为Key值不唯一,故无法通过Key值取下标。
(5)关联式容器总结
1.元素是有序的。
2.底层使用的都是红黑树,查找的时间复杂度为O(logn)。
3.set与map中的key是唯一的,不能重复。
multiset、multimap中的key是不唯一的,可以重复。
4.关联式容器中只有map支持下标访问,而set、multiset、multimap不支持下标访问。
map下标传递的是Key类型,返回值是Value类型。并且下标访问运算符没有重载const版本。
5.关联式容器对自定义类型的改写
①模板的特化
②函数对象的形式
③重载operator<
3.无序关联式容器
(0)哈希
1.哈希函数
通过key值计算出位置值
size_t index = H(key)//由关键字获取所在位置
2.哈希函数的构建方式
定址法: H(key) = a * key + b
平方取中法: key^2 = 1234^2 = 1522756 ------>227
数字分析法: H(key) = key % 10000;
除留取余法: H(key) = key mod p (p <= m, m为表长)
3.哈希冲突
就是对于不一样的key值,可能得到相同的地址,即:H(key1) = H(key2)
H(key1) = H(key2), key1 != key2
4.解决哈希冲突
①线性探测再散列法
②平方探测法
③拉链法(链地址法,也是STL中使用的方法)
5.装填因子 (load factor)
(1)
装载因子
α
=
(
实际装载数据的长度
n
)
/
(
表长
m
)
装载因子 α = (实际装载数据的长度n) / (表长m)
装载因子α=(实际装载数据的长度n)/(表长m) 【装载因子 = 元素的个数 / 表的长度,一般α在50%-75%比较完美】
(2)装填因子大,则元素个数多,冲突的概率高,但空间的利用率也比较高
装载因子小,则元素个数少,冲突的概率低,但空间的利用率也比较低
6.哈希表的设计思想
用空间换时间,注意数组本身就是一个完美的哈希,所有元素都有存储位置,没有冲突,空间利用率也达到极致。
(1)unordered_set
1.unordered_set的基本特征
(1)存放的是key类型,key值唯一,不可重复
(2)key值没有顺序
(3)底层使用的是哈希表
2.查找 (和set一致)
(1)count()
(2)find()
3.插入 (和set一致)
4.删除 (和set一致)
(1)删除一个元素
(2)删除迭代器范围
5.unordered_set不支持下标。不支持用迭代器修改元素。
6.unordered_set针对自定义类型的改写
unordered_set的第二个模板参数Hash,如果针对的是自定义类型,需要进行自己改写,改写的方式是:模板的特化、函数对象的形式。
没有对std::hash<Key>进行特化,改写Hash和KeyEqual:
方法一:模板特化
方法二:重载==运算符
方法三:函数对象
一、Hash的改写:两种方法
(1)方法一:Hash用模板特化
(2)方法二:Hash用函数对象
二、KeyEqual的改写:三种方法
unordered_set的第三个模板参数KeyEqual,如果针对的是自定义类型,需要进行自己改写,改写的方式是:模板的特化、函数对象的形式、运算符的重载。
(1)方法一:模板特化
(2)方法二:重载==运算符
(3)方法三:函数对象 + 传参数
(2)unordered_multiset
1.unordered_multiset的基本特征
(1)存放的是key类型,key值不唯一,可以重复
(2)key值是没有顺序的
(3)底层使用的是哈希。查找的时间复杂度为O(1)。
2.其他功能
unordered_multiset的查找
3.针对自定义类型
和unordered_multiset的改写方式一样,对第二个模板参数Hash(两种方法)、第三个模板参数KeyEqual(三种方法)进行改写。
(3)unordered_map
1.unordered_map的特征:
(1)存放的是key-value类型,key值唯一,不能重复
(2)key值没有顺序
(3)底层使用的是哈希
2.其他操作
(1)unordered_map的初始化、遍历、查找count
find
、插入insert
、删除操作erase
、取下标与map完全相同。
(2)unordered_map也支持下标操作:通过下标访问、不存在则直接插入,通过下标进行修改。仅支持非const版本的operator[]。
3.unordered_map针对自定义类型:
(4)unordered_multimap
1.unordered_map的特征:
(1)存放的是key-value类型,key值不是唯一的,可以重复
(2)key值没有顺序
(3)底层使用的是哈希
2.其他操作
unordered_multimap不支持下标访问
(5)无序关联式容器总结
1.元素是没有顺序的
2.底层使用的都是哈希表,查找的时间复杂度为O(1)
3.基本操作
4.无序关联式容器对自定义类型的改写
①模板的特化
②重载运算符
③函数对象的形式
4.容器的选择
1.元素是不是有序的
(1)元素有顺序:
①首先选择的是关联式容器。
②最不应该选择无序关联式容器。
③其次选择序列式容器:list有成员函数sort、vector与deque在算法库<algorithm.h>中也有sort函数进行排序。序列式容器可以保留插入时的顺序。
2.容器能不能取下标
(1)可以取下标的:
①序列式容器:vector、deque
②关联式容器:map
③无序关联式容器:unordered_map
(2)不能取下标:
①list
②除了map的关联式容器
③除了unordered_map的无序关联式容器
④优先级队列只能取top()
3.容器中的元素的查找的时间复杂度
(1)序列式容器:O(n)
(2)关联式容器:O(log₂n),红黑树
(3)无序关联式容器:O(1),哈希表
4.迭代器的类型不同
(1)随机访问迭代器:vector、deque 【可以用下标随机访问、一次移动多格 +=、-=】
(2)双向迭代器:list、4种关联式容器 【只能++、–】
(3)前向迭代器:4种无序关联式容器【只能++】
5.元素是否可以重复
(1)元素要求可以重复:序列式容器、multi系列容器
(2)元素要求不可以重复:set、map、unordered_set、unordered_map
6.使用场景
(1)vector (向量)
适用场景:尾部插入删除,随机访问。
(2)deque (双端队列)
适用场景:首尾插入删除,随机访问
(3)list (双向链表)
适用场景:容器中间插入删除,不需要随机访问
(4)set (集合)
适用场景:存储不重复元素,快速查找。唯一键值集合。
(5)multiset (多重集合)
(6)map (映射)
适用场景:存储键值对,快速查找。需要保证键的唯一性。
(7)multimap
(8)unordered_set (无序集合)
(9)unordered_multiset
(10)unordered_map (无序映射)
(11)unordered_multimap
二、迭代器 (iterator)
1.概念
迭代器可以理解为广义的直至,具备指针的功能:可以进行移动、可以解引用获取内容
迭代器(iterator)模式又称为游标(Cursor)模式,用于提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。或者这样说可能更容易理解:Iterator模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的方法)访问聚合对象中的各个元素。
2.迭代器产生的原因
更好地访问容器中的元素
Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。
3.迭代器的类型
1.迭代器的分类:
①输入迭代器(InputIterator):输入流迭代器
②输出迭代器(OutputIterator):输出流迭代器
③前向迭代器(ForwardIterator)
④双向迭代器(BidirectionalIterator)
⑤随机访问迭代器(RandomAccessIterator)。
2.每个迭代器类型对应的操作:
3.五种迭代器的关系图:继承图
(1)双向迭代器
1.典型的双向迭代器包括:list
、set
和map
的迭代器。
2.双向迭代器(Bidirectional Iterator)
3.双向迭代器允许在容器中进行双向遍历,即可以向前和向后遍历。双向迭代器支持以下操作:
①递增(++iter 或 iter++):将迭代器移动到下一个元素。
②递减(–iter 或 iter–):将迭代器移动到上一个元素。
③解引用(*iter):访问迭代器当前指向的元素。
④比较操作符(== 和 !=):检查两个迭代器是否相等。
(2)随机访问迭代器
1.典型的随机访问迭代器包括vector
、deque
和原生数组的迭代器。
2.随机访问迭代器(Random Access Iterator)
3.随机访问迭代器除了支持双向迭代器的所有操作外,还支持在常数时间内进行任意位置的访问。随机访问迭代器支持以下额外的操作:
①加法(iter + n):将迭代器向前移动n个位置。
②减法(iter - n):将迭代器向后移动n个位置。
③迭代器差(iter1 - iter2):计算两个迭代器之间的距离。
④关系操作符(<、>、<= 和 >=):比较两个迭代器的位置。
⑤下标操作符(iter[n]):访问迭代器当前位置偏移n个位置的元素。
4.流迭代器
流迭代器:与输入输出流进行交互的迭代器。
流迭代器是特殊的迭代器,可以将输入/输出流作为容器看待。
(1)输出流迭代器
输出流迭代器:ostream_iterator
输出流迭代器就是输出迭代器
#include <iterator>
using std::ostream_iterator;
//遍历容器中的元素
//1.创建左值对象
ostream_iterator<int> osi(cout, " "); //创建一个输出流迭代器,将数据写入std::cout
copy(vec.begin(), vec.end(), osi); //使用标准库算法将容器内容写入输出流
//2.创建右值临时对象
copy(vec.begin(), vec.end(), ostream_iterator<int>(cout, " "));
copy的源码里,的operator=里有输出流运算符。会把容器遍历。相当于第四种遍历方法。
把元素复制到第三个参数中
(2)输入流迭代器
输入流迭代器:istream_iterator
输入流迭代器就是输入迭代器
vector<int> vec;
istream_iterator<int> isi(cin);
copy(isi, istream_iterator<int>(), std::back_inserter(vec));
三、算法
1.概念
(1)概述
算法中包含很多对容器进行处理的算法,使用迭代器来标识要处理的数据或数据段、以及结果的存放位置,有的函数还作为对象参数传递给另一个函数,实现数据的处理。这些算法可以操作在多种容器类型上,所以称为“泛型”,泛型算法不是针对容器编写,而只是单独依赖迭代器和迭代器操作实现。而且算法库中的算法都是普通函数(自由函数)。
(2)头文件
泛型算法不针对一种容器
#include <algorithm> //泛型算法
#include <numeric> //泛型算术算法
(3)分类
1.非修改式的算法:不改变容器的内容,count()、find()、for_each() 等。
2.修改式的算法:可以修改容器中的内容,如copy()、swap()、unique()、remove_if()、transform()、random_shuffle()等。
3.排序函数:**sort()**等。
4.二分搜索:lower_bound、upper_bound
5.集合操作:set_intersection、set_union
6.堆相关的操作:push_heap、make_heap
7.取最值:max、min
8.数值操作:acculate、计算两个容器的内部乘积等
9.未初始化的内存操作:uninitialized_copy
(4)一元函数、一元断言/一元谓词
①一元函数:函数的参数只有一个;
②一元断言/一元谓词:函数的参数只有一个,并且返回类型是bool类型。
③二元函数:函数的参数有两个;
④二元断言/二元谓词:函数的参数两个,并且返回类型是bool类型。
//一元断言/一元谓词
bool func(int value)
{
return value > 5;
}
//一元函数
void func(int value)
{
cout << value << " ";
}
2.copy
3.for_each的使用
template<class InputIt, class UnaryFunction>
UnaryFunction for_each( InputIt first, InputIt last, UnaryFunction f )
{
for( ; first != last; ++first){
f(*first);
}
return f;
}
第五种遍历:
#include <algorithm> //for_each的头文件
void func(int value){
cout << value << " ";
}
void test(){
vector<int> vec= {1,3,5,7,9};
//将for_each函数中的第一个参数到第二个参数范围中的元素,传入到第三个参数中
for_each(vec.begin(), vec.end(), func);
cout << endl;
}
4.lambda表达式:匿名函数 (lambda函数)
1.lambda表达式的形式:[](){}
①[ ]:捕获列表,捕获外部变量。只读属性,非要修改需要加&。
多个特定变量用,
分割
全局变量不需要捕获,直接使用
[=]
:按值捕获所有变量
[&]
:按引用捕获所有变量
[&,x]
:混合捕获,按引用捕获所有变量,特定变量x按值捕获
[this]
:捕获当前类的this指针
②( ):函数的参数列表。没有参数的lambda表达式,可以省略 ( )。
③{ }:函数的函数体
[capture](params) opt -> returnType
{
body;
}
2.提出原因:
为了避免func和for_each不在同一个文件,C++为了避免这种跨文件查询的麻烦,提出了lanmda表达式。lambda表达式可以看作是仿函数。
//1.引入lambda表达式的好处:原本的函数指针,现在声明和实现可以写在一起
//2.lambda表达式的形式: [](){}
#include <iostream>
#include <vector>
#include <algorithm>
using std::cout;
using std::endl;
using std::vector;
void func(int value){
cout << value << " ";
}
void test(){
vector<int> vec = {1,3,5,7,9};
for_each(vec.begin(), vec.end(), func);
cout << endl;
}
//为了避免func在不同的文件中,考虑用lambda表达式,就可以把声明和实现写在一起了
void test2(){
vector<int> vec = {2,4,6,8,10};
//将func用lambda表达式实现
for_each(vec.begin(), vec.end(), [](int value){ cout << value << " "; });
cout << endl;
}
int main()
{
test();
test2();
return 0;
}
3.demo
//lambda.cpp
(1)捕获:按值捕获、按引用捕获
(2)lambda表达式中捕获的是const版本的变量,若要修改:
①按引用捕获,可在lambda表达式内修改原变量的值
②加mutable关键字,可在lambda表达式内修改副本
(3)函数的返回类型
4.lambda表达式的接收:
使用变量接收lambda表达式,以期可以在别处调用lambda表达式
5.捕获类中的数据成员
6.lambda表达式本质是仿函数:
还原网址:把代码还原成编译器的角度
5.remove_if的使用
1.函数原型
//remove_if()的第三个参数,传一元断言
template <class ForwardIterator, class UnaryPredicate>
ForwardIterator remove_if (ForwardIterator first, ForwardIterator last, UnaryPredicate pred);
{
first = std::find_if(first, last, p);
if (first != last)
for(ForwardIt i = first; ++i != last; )
if (!p(*i))
*first++ = std::move(*i);
return first;
}
2.应用:remove_if + erase
(1)原理:符合条件的就前移,不符合条件的进行覆盖。最后扫描到末尾,返回待删除元素的首迭代器,把后面的元素都删掉。
(2)效果:将满足第三个参数(一元断言)的元素都删除
(3)优势:(不管是什么容器)速度快,底层是覆盖,不需要移动元素
auto it = remove_if(vec.begin(), vec.end(), [](int value)->bool{
return value > 5;
});
vec.erase(it, vec.end());
//lambda表达式可省略函数返回值类型,编译器会根据return语句自动推导
auto it = remove_if(vec.begin(), vec.end(), [](int value){ return value % 2 == 0; });
vec.erase(it, vec.end());
四、适配器
1.迭代器适配器
迭代器适配器(Iterator Adapters):
①reverse_iterator:反向迭代器。
②back_insert_iterator:通过push_back插入元素。
③front_insert_iterator:通过push_front插入元素。
④insert_iterator:通过insert插入元素。
(1)反向迭代器:reverse_iterator
rbegin()
:指向最后一个元素
rend()
:指向第一个元素的前面一个位置
举例:反向遍历vector
//反向迭代器
void test2(){
vector<int> vec = {1,2,3,4,5,6,7,8,9};
vector<int>::reverse_iterator rit = vec.rbegin();
for( ; rit != vec.rend(); ++rit){
cout << *rit << " ";
}
cout << endl;
}
(2)迭代器适配器:三组插入迭代器,是特殊的输出迭代器
1.back_inserter
是函数模板,返回类型是back_insert_iterator
,而back_insert_iterator是类模板,底层调用了push_back函数来插入元素。
2.front_inserter
是函数模板,返回类型是front_insert_iterator
,而front_insert_iterator是类模板,底层调用了push_front函数来插入元素。
3.inserter
是函数模板,返回类型是insert_iterator
,而insert_iterator是类模板,底层调用了insert函
数来插入元素。
举例:copy函数 + 插入迭代器,也实现了容器的插入。
1.插入尾部:back_inserter()
void test(){
vector<int> vec = {1,2,3,4,5};
list<int> ls = {6,7,8,9,10};
//将list中的元素插入到vector的尾部
copy(ls.begin(), ls.end(), back_inserter(vec));
//用输出流迭代器对容器进行输出
copy(vec.begin(), vec.end(), ostream_iterator<int>(cout, " "));//创建临时对象
cout << endl;
}
2.插入头部:front_inserter()
void test2(){
vector<int> vec = {1,2,3,4,5};
list<int> ls = {6,7,8,9,10};
//将vector中的元素插入到list的头部: 头插,会形成逆序的效果
copy(vec.begin(), vec.end(), front_inserter(ls));
//用输出流迭代器对容器进行输出
copy(ls.begin(), ls.end(), ostream_iterator<int>(cout, " "));//创建临时对象
cout << endl;
}
3.插入中间:inserter()
//插入中间
void test3(){
vector<int> vec = {9,7,5,3,1};
set<int> st = {10,8,6,4,2};
//将vector中的元素插入到set
auto it = st.begin();
copy(vec.begin(), vec.end(), inserter(st, it));
//用输出流迭代器对容器进行输出
copy(st.begin(), st.end(), ostream_iterator<int>(cout, " "));//创建临时对象
cout << endl;
}
2.容器适配器
容器适配器(Container Adapters):
①stack:栈,后进先出(LIFO)。
②queue:队列,先进先出(FIFO)。
③priority_queue:优先队列,元素按优先级排序。
容器适配器没有迭代器。
(1)stack (栈)
vector、deque、list都可以
(2)queue (队列)
要求头部可以删除:deque、list可以,vector不可以
(3)priority_queue (优先级队列)
1.模板参数
要求随机访问迭代器:vector、deque可以,list不可以
2.操作:
(1)初始化:无参构造、拷贝或移动构造、迭代器范围。不支持用大括号。
(2)遍历:不支持下标访问、不支持迭代器、不支持增强for循环。只能不停的top()和pop(),直至为空。
while(!pque.empty()){
cout << pque.top() << " ";
pque.pop();
}
(3)top()
:值最大的元素
3.优先级队列的底层实现:大顶堆,采用堆排序:
当有新元素插入时,会将堆顶与新插入的元素进行比较。
如果堆顶比新插入元素要小,即满足std::less,那么会进行置换,将新的元素作为新的堆顶。
若堆顶比新插入的元素要大,即不满足std::less,就不会进行置换。
3.函数适配器
函数适配器(Function Adapters):
(1)函数绑定器:
①bind1st:绑定二元函数的第一个参数(已在 C++11 中被弃用)。
②bind2nd:绑定二元函数的第二个参数(已在 C++11 中被弃用)。
③bind:通用的参数绑定器,用于绑定任意数量的参数,推荐在现代 C++ 中使用。
(2)函数对象(仿函数)适配器:
①not1:一元仿函数取反。
②not2:二元仿函数取反。
③ptr_fun:将普通函数指针转换为函数对象。【函数指针适配器】
④mem_fun:将成员函数指针转换为函数对象。【成员函数适配器】
⑤mem_fun_ref:与 std::mem_fun 类似,但适用于对象的引用。
(1)函数绑定器:bind1st、bind2nd、bind
①bind1st和bind2nd的使用
1.头文件
#include <functional>
2.模板形式
template< class F, class T > std::binder1st<F> bind1st( const F &f, const T &x );
template< class F, class T > std::binder2nd<F> bind2nd( const F &f, const T &x );
模板形式中,两个函数绑定器的第一个参数就是一个函数,第二个参数就是一个数字,如果F是一个二
元函数(普通二元函数或者二元谓词),我们可以绑定F的第一个参数(bind1st)或者第二个参数(bind2nd),达到我们想要的效果(使用二元谓词的效果)
3.问题提出:
如果remove_if的第三个参数是二元断言,如何解决:二元断言转一元断言,需要固定一个参数
4.解决:
(1)bind1st:固定二元函数对象的第一个参数
(2)bind2nd:固定二元函数对象的第二个参数
ReturnValue Func(Args1, Args2);
//要删除所有大于5的元素
//bind1st:固定住第一个参数
auto it = remove_if(vec.begin(), vec.end(), bind1st(std::less<int>(), 5));
vec.erase(it, vec.end());
//要删除所有大于5的元素
//bind2nd:固定住第二个参数
auto it = remove_if(vec.begin(), vec.end(), bind2nd(std::greater<int>(), 5));
vec.erase(it, vec.end());
断言放第三个参数,相当于条件。满足条件的返回值为true。再配合remove_if()进行删除。
②bind函数的使用
1.bind的作用:
创建一个新的可调用对象,该对象将某些参数绑定到一个已有函数或函数对象上。
std::bind 允许你绑定函数的一部分参数,生成新的函数对象,该对象可以在需要的地方调用。
2.作用:
(1)可变参数,可以绑定n元函数对象。
(2)bind函数的使用相比于bind1st以及bind2nd更加的具有通用性,因为后者只能绑定一个参数,而bind可以绑定任意个参数。
(3)bind可以绑定到普通函数、成员函数、数据成员。
3.bind与bind1st、bind2nd的关系:
bind1st、bind2nd在C++11中被废弃,转而采用更为强大灵活的bind。
4.bind的头文件
#include <functional>
using std::bind;
5.引用折叠
F是&&:既可以传左值,又可以传右值
如果F写左值,则没有引用折叠,只能传左值,不能传右值。
C++11之前没有右值引用,解决方法是 const 类型 &
,既可以传左值又可以传右值。
6.实例
(1)bind绑定普通函数
//测试一个三元函数
int multiply(int x, int y, int z){
cout << "multiply(int x, int y, int z)" << endl;
return x * y * z;
}
//bind绑定普通函数
void test4(){
//bind: 固定第一个参数,并保留两个占位符
auto func = bind(multiply, 100, _1, _2);
cout << func(10,1) << endl;
}
(2)bind绑定成员函数
class Example
{
public:
//成员函数的第一个参数,是隐藏的this指针, Example * const this
int add(int x, int y){
cout << "int Example::add(int,int)" << endl;
return x + y;
}
};
//bind可以绑定一元函数、二元函数、甚至n元函数
//既可以绑定普通函数,也可以绑定成员函数
void test(){
//1.bind绑定二元普通函数
auto f = bind(add, 1 , 2);
cout << "f() = " << f() << endl;
//2.bind绑定三元普通函数
auto f2 = bind(&multiply, 3, 4, 5);
cout << "f2() = " << f2() << endl;
//3.bind绑定成员函数(三元函数)
Example ex;
auto f3 = bind(&Example::add, &ex, 10, 20); //成员函数就必须加引用
cout << "f3() = " << f3() << endl;
//占位符
using namespace std::placeholders;
function<int(int,int)> f4 = bind(add, _2, 100); //尽量用_1,需要多写参数,而且没用
cout << "f4() = " << f4(1,2) << endl;
function<int(int)> f5 = bind(add, _1, 100);
cout << "f5() = " << f5(6) << endl;
}
(3)bind还可以绑定数据成员:类的数据成员,可以提升为函数
C++11,可以直接将数据成员在声明时进行初始化
7.占位符
1.头文件
#include <functional> // 包含 std::bind 和 std::placeholders
using std::bind;
using namespace std::placeholders; // 使用占位符
2.占位符
占位符的位置(占位符整体),是形参的位置。
占位符的数字,是对应的实参的位置。
bind()绑定某个函数,只绑定一部分。
占位符:_1,_2,_3。对应实参对应的位置。
3.bind 默认采用的是值传递,而不是引用传递。即使func的参数使用的是引用。
可以使用std::ref
或 std::cref
这两个引用包装器,传递引用。
4.bind绑定后,会改变函数的类型
函数的类型:函数的返回类型 + 函数的参数列表
add的第一个参数绑定为100,第二个参数用占位符_1,为f的第一个实参
5.用function类模板来接收bind的返回类型。
function可以存放函数类型,所以将function称为函数包装器(函数的容器)
function<int(int)> f = bind(add, _1 , 999);
f(100);
6.this指针也可以用占位符替代
8.bind绑定成员函数的时候传参,传递对象和传递地址的区别:
传&ex和ex在语法上都是一个效果,但是有些区别:
①&ex传的是一个指针的大小,但ex是传一个对象的大小。
②&ex若是多线程,可能ex已经销毁,&ex就成了空指针。但传ex就没问题,已经复制了一次对象。
void test()
{
Example ex;
function<int()> f = bind(&Example::add, &ex, 10, 20); //this指针对应位置传递 &ex (传递指针,对象的地址)
cout << "f() = " << f() << endl;
cout << endl;
function<int()> f2 = bind(&Example::add, ex, 30, 40); //this指针对应位置传递 ex (值传递,拷贝对象)
cout << "f2() = " << f2() << endl;
}
9.尽量用_1。不然会造成参数浪费。
10.lambda表达式的返回结果,也可以用function<>进行接收
11.注意:若lambda表达式捕获了声明周期不存在的引用,会发生错误。
不要捕获局部变量的引用,因为当变量离开作用域的时候,就是捕获了声明周期不存在的引用。
vector<function<void(const string &)>> vec;
void test()
{
int num = 100;
string name("wangdao");
/* function<void(const string &)> f = */
/* [&num, &name](const string &value){ */
/* cout << "num = " << num << endl; */
/* cout << "name = " << name << endl; */
/* cout << "value = " << value << endl; */
/* }; */
//局部变量的引用。在该作用域之外调用,进行捕获,会发生错误
vec.push_back([&num, &name](const string &value){
cout << "num = " << num << endl;
cout << "name = " << name << endl;
cout << "value = " << value << endl;
});
}
void test2(){
for(auto func : vec){
func("wuhan");
}
}
12.std::bind + std::function结合使用,实现静态多态
(1)面向对象的方式:继承 + 虚函数(纯虚函数),可以体现多态,动态多态
基于对象的方式:std::bind + std::function,也可以实现多态,静态多态
(2)std::bind改变函数的形态。
用std::function进行接收
(3)头文件
#include <functional>
using std::bind;
using std::function;
cb 是一个右值引用参数。然而,虽然它是右值引用类型,但在函数体内,cb 本身被视为左值。这是因为在C++中,所有的命名变量(包括右值引用)都是左值。
4_figure.cc
function可以接收右值,并把右值转为左值:
Figure fig;
/* function<void()> f = bind(&Rectangle::display, &rectangle, 100); */
/* fig.setDisplayCallback(std::move(f)); */
fig.setDisplayCallback(bind(&Rectangle::display, &rectangle, 100));
赋值改为初始化
cb是左值,但是可以用右值引用?
(2)mem_fun
mem_fn是成员函数适配器:成员函数调用for_each(),需要使用mem_fn进行适配。
//使用for_each进行打印
for_each(vec.begin(), vec.end(), mem_fn(&Number::print));
成员函数写法
bind写法
using namespace std::placeholders;
/* function<void(Number *)> f = bind(&Number::print, _1); */ //function<>传指针是错的
function<void(Number )> f = bind(&Number::print, _1); //必须传对象
for_each(vec.begin(), vec.end(), f);
/* for_each(vec.begin(), vec.end(), bind(&Number::print, _1)); */ //上面两行可以合为这一行
传&ex和ex,则function<>里不同
五、函数对象
1.狭义的函数对象
重载了函数调用运算符的类的对象称为函数对象。这使得函数对象可以像函数一样被调用。
2.广义的函数对象:所有可以与小括号进行结合展示出函数含义的都可以称为函数对象。
(1)重载了函数调用运算符()的类创建的对象
(2)函数名
(3)指向函数的指针
(4)function
(5)lambda表达式
3.举例:
1.list
2.set
六、空间配置器 (allocator) 【面试加分项】
1.概述
1.空间配置器的概述
先申请空间,然后在在该空间上构建对象。将空间的申请与对象的创建分离开。
2.特点:
(1)可以感知类型的空间分配器
(2)将内存的开辟/释放与对象的创建/销毁分开
3.头文件
#include <memory>
template< class T > struct allocator;
template<> struct allocator<void>;
4.对于STL的容器而言,一般都是申请一大块空间,然后在申请的空间上构建对象。如果每创建一个对象的同时申请一块空间,效率较低,时间复杂度高。
2.四个函数
1.allocate:申请空间
申请一块原始的、未初始化的空间 const void*。底层用的malloc。
2.construct:创建对象
源码:void constrct(pointer _p,const _Tp& _val) { new§ _Tp()_val};
底层用的new
3.destroy:销毁对象
源码:void destroy(pointer __p) { __p->~_Tp()};
4.deallocate:释放空间
底层用的free
//申请空间:申请的是原始的,未初始化的空间
T* allocate( std::size_t n );
//释放空间
void deallocate( T* p, std::size_t n );
//构建对象:在指定的未初始化的空间上构建对象,使用的是定位new表达式
void construct( pointer p, const_reference val );
//销毁对象
void destroy( pointer p );
2.STL中为何将对象的构建与空间的申请分开:
(1)因为在STL中,对象的创建并不是一个,有可能一次要创建多个对象。如vector<Point>vec2(vec)。
①如果创建一个对象就要申请一块空间,则空间的申请就非常的频繁。
②而且多次申请的空间,可能是不连续的,从而产生内存碎片。
(2)若销毁一个对象就释放一块空间,则空间的释放也会非常频繁。
3.两级空间配置器
1.源码
①第一个分支(一级空间配置器):底层直接走malloc申请空间【若编译时加了宏】
②第二个分支(二级空间配置器):若申请空间大小n大于128字节,底层还是会走malloc申请空间。若n<=128,执行16维的自由链表+内存池。
2.数据结构:
①16维的自由链表,下面可以挂接内存块。
②内存池用两个指针进行控制。
3.对于空间配置器而言,所申请的空间在内存的哪个位置?
答:堆空间
(1)一级空间配置器
一级空间配置器,要有宏
#ifdef __USE_MALLOC
第一级空间配置器使用类模板malloc_alloc_template ,其底层使用的是malloc/free进行空间的申请与释放。
(2)二级空间配置器
1.二级空间配置器分两个分支的设计目的:
①小空间进行频繁malloc申请,会在内核态与用户态之间进行频率切换,导致系统效率低。
②防止多次申请空间导致的内存碎片问题:多次申请的空间不连续,会造成内存外部碎片。
二级空间配置器:默认情况,没有宏的情况。
二级空间配置器使用类模板,default_alloc_template,其底层根据申请空间大小有分为两个分支进行:
①第一分支是当申请的空间大于128字节的时候,还是走__malloc_alloc_template
②当申请的空间小于128字节的使用,使用16维自由链表+内存池的结构进行。
128/8 = 16,下标从0到15。
if(n>128) {malloc;}
else {16维自由链表 S_freelist+内存池}
函数调用过程:
①allocate():对外暴露的申请空间的接口,但其不是直接申请空间,会调用_S_refill()。
②_S_refill()
:①会调用_S_chunk_alloc()申请空间。②以n为单位对返回的空间进行切割,然后挂接在对应的自由链表下。
③_S_chunk_alloc()
:真正申请空间 (递归调用)。可能将会将申请的结果一分为二,一部分进行返回,另一部分放入内存池,由两个指针_S_start_free、_S_end_free进行控制。
④_S_freelist_index():自由链表取下标
⑤_S_round_up():以8的整数倍向上取整
⑥_S_start_free:控制堆空间中内存池的开头
⑦_S_end_free:控制堆空间中内存池的结尾
_S_round_up():
4.空间配置器的源码剖析
查看源码,我们知道空间配置器会分为两级,即:两级空间配置器:
(1)第一级空间配置器使用类模板malloc_alloc_template ,其底层使用的是malloc/free进行空间的申请与释放。
(2)二级空间配置器使用类模板,default_alloc_template,其底层根据申请空间大小又分为两个分支,第一分支是当申请的空间大于128字节的时候,还是走malloc_alloc_template ,当申请的空间小于128字节的使用,使用内存池+16个自由链表的结构进行。
也就是由一个16维的数组组成,每一维会按照8的整数倍申请空间,比如:下标为3,也就是会按照32字节为基本单位申请空间,每次申请空间的大小都是32字节,而且每次申请的时候一次申请很大一片空间,然后按照32字节为一个等分,分成多个等分,然后挂接在下标为3的下面,形成链表形式,这样以后需要32字节的时候,直接在下标为3的下面取出一个节点,就是32字节即可。其他下标的处理方式完全一致。
(1)allocate()
一级空间配置器:底层调用malloc
二级空间配置器:两个分支
void* __default_alloc_template::_S_refill(size_t __n)
{
int __nobjs = 20; //第一次总是申请20倍的
char* __chunk = _S_chunk_alloc(__n, __nobjs);
_Obj* __STL_VOLATILE* __my_free_list;
_Obj* __result;
_Obj* __current_obj;
_Obj* __next_obj;
int __i;
举例:
//1、如果想申请32字节的时候,堆空间与内存池是充足的
//2、如果想申请64字节的时候,堆空间与内存池是充足的
//3、如果想申请96字节的时候,堆空间与内存池是充足的
//4、如果想申请72字节的时候,堆空间与内存池没有连续的72字节
//5、如果想申请64字节,内存池里有72字节,则会把64字节分配出来,不足64倍数的8字节会留在内存池。
//6、如果想申请64字节,内存池里有130字节,则会把128字节从内存池取出,64分配给申请者,64挂在自由链表下,不足64倍数的2字节会留在内存池。
//7、如果想申请64字节,内存池里不足64字节。则会优先向堆空间申请,进行malloc。若堆空间内存不足导致malloc失败,则会沿着自由链表向后借。
1.申请32字节
向上取整,得到8的整数倍。
申请32字节,实际申请1280字节,其中640切割为20个32字节的挂接在自由链表下,另外640字节在堆区作为内存池。
2.申请64
先返回内存池中的640B,进行分割
堆空间内存池的640B被全部用完
3.申请96B
1920被切割为20等份(20*96 = 1920)后挂在自由链表下,2000B在内存池。
4.申请72字节。假设此时内存池(为0)和堆空间都内存不足,没有连续的72字节。
循环向后遍历自由链表,向后面更大的借内存空间。比如这次申请72B,但是往后借到了96B。
然后进行分割,分割出72B,剩下的24B由_S_start_free和_S_end_free进行控制,丢入内存池。
(2)deallocate()
一级空间配置器:
二级空间配置器:
用头插法,将要delete的结点,重新链接回自由链表下进行重复使用。
(3)construct()
(4)destroy()
对象的销毁:就是执行析构函数