C++(week13): C++基础: 标准模板库 STL

文章目录

  • 零、标准模板库 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.占位符
        • function
      • (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}; //构造初始化

2.list的删除
(1)erase()
①删除单个元素:

iterator erase(iterator pos);

②删除一个范围内的元素:

iterator erase(iterator first, iterator last);

(2)remove():删除所有等于指定值的元素

void remove(const T& value);

举例:删除连续重复函数

//list的删除
void test2()
{//删除连续重复元素:删除list中所有的2list<int> ls = {1,2,2,2,3,4,5,2,2,2,6,7,8,2,2,9};#if 0for(auto it = ls.begin(); it != ls.end();  ){if(*it == 2){it = ls.erase(it);}else{++it;}}#endifls.remove(2);display(ls);
}

3.在容器的任意位置插入:在中间任意位置插入: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; //4ls.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; //2ls.insert(it, {500, 400, 300});display(ls);cout << "*it = " << *it << endl;
}

4.list的迭代器是双向迭代器,不是随机访问迭代器,只能++it,不支持it += 2。只能一次一次偏移。


5.list清空函数:clear()


6.list没有shrink_to_fit(),list也没有capacity()。因为有size()。


7.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); //左闭右开 [,),右边取不到

在这里插入图片描述


举例:LRU算法,可以直接使用splice()
在这里插入图片描述

代码链接:https://github.com/WangEdward1027/STL/blob/main/list/list_splice.cpp


8.list的底层实现
在这里插入图片描述


9.总结
①对于vector而言,前后元素的地址是完全连续的。
②对于deque而言,前后两个元素是逻辑上连续,物理上不连续
③对于list而言,前后两个元素是不连续的。



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.典型的双向迭代器包括:listsetmap的迭代器。

2.双向迭代器(Bidirectional Iterator)

3.双向迭代器允许在容器中进行双向遍历,即可以向前和向后遍历。双向迭代器支持以下操作:
①递增(++iter 或 iter++):将迭代器移动到下一个元素。
②递减(–iter 或 iter–):将迭代器移动到上一个元素。
③解引用(*iter):访问迭代器当前指向的元素。
④比较操作符(== 和 !=):检查两个迭代器是否相等。


(2)随机访问迭代器

1.典型的随机访问迭代器包括vectordeque和原生数组的迭代器。

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中的元素插入到setauto 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 (队列)

1.特点
要求头部可以删除:deque、list可以,vector不可以

先进先出:队列 queue
在这里插入图片描述

2.接口
在这里插入图片描述


(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 thisint 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::refstd::cref这两个引用包装器,传递引用。
在这里插入图片描述


4.bind绑定后,会改变函数的类型

函数的类型:函数的返回类型 + 函数的参数列表
在这里插入图片描述

add的第一个参数绑定为100,第二个参数用占位符_1,为f的第一个实参
在这里插入图片描述


function

头文件

#include <functional>
using std::function;

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)lambda表达式
(5)function

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()

对象的销毁:就是执行析构函数
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/876520.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【前端 15】Vue生命周期

Vue生命周期 在Vue.js中&#xff0c;了解组件的生命周期对于开发者来说是至关重要的。Vue的生命周期指的是Vue实例从创建到销毁的一系列过程&#xff0c;每个阶段都对应着特定的生命周期钩子&#xff08;或称为生命周期方法&#xff09;&#xff0c;允许我们在不同的时间点加入…

【网络安全】AWS S3 Bucket配置错误导致敏感信息泄露

未经许可&#xff0c;不得转载。 文章目录 前言技术分析正文 前言 AWS&#xff08;Amazon Web Services&#xff09;是亚马逊公司提供的一个安全的云服务平台&#xff0c;旨在为个人、公司和政府机构提供计算能力、存储解决方案、内容交付和其他功能。作为全球领先的云服务提供…

Autodesk Revit v2025 激解锁版下载及安装教程 (三维建模软件)

前言 Revit是欧特克公司知名的三维建模软件&#xff0c;是建筑业BIM体系中使用最广泛的软件之一&#xff0c;其核心功能是三维建筑模型参数化设计、渲染效果图、算量&#xff0c;土建建模、机电建模、用来帮助工程师在施工前精确模拟阶段。 一、下载地址 下载链接&#xff1…

体育赛事中的AI运用

7月24日&#xff0c;国际奥委会&#xff08;IOC&#xff09;举办了新闻发布会&#xff0c;宣布计划在2024年巴黎奥运会上展示一系列创新的人工智能&#xff08;AI&#xff09;技术。这次会议不仅是对即将到来的奥运赛事的预热&#xff0c;也深入探讨了人工智能在体育领域可能带…

快速重装系统

挑选系统 https://d1506.xy58.net/202002/Js_GhostWin7z_x64_2020T.iso WIN11镜像 安装PE启动U盘安装工具 本地安装

【机器学习】深入理解损失函数(Loss Functions)

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 深入理解损失函数(Loss Functions)什么是损失函数?常见损失函数类型1. 均方误差…

【C++】set的使用

&#x1f525;个人主页&#xff1a; Forcible Bug Maker &#x1f525;专栏&#xff1a; STL || C 目录 &#x1f308;前言&#x1f308;关于set&#x1f525;容量函数emptysize &#x1f525;Modifiersinserteraseclear &#x1f525;Operationsfindcountlower_bound和upper_…

Lesson 51 A pleasant climate

Lesson 51 A pleasant climate 词汇 Greece n. 希腊 Greek a. 希腊的&#xff0c;希腊语 搭配&#xff1a;Greek gift 不怀好意的礼物 例句&#xff1a;他的电脑是不怀好意的礼物。    His computer is a Greek gift. climate n. 气候 长时间&#xff0c;不容易更改的 we…

一键将桌面资料存到d盘的工具,小巧、绿色、免费、免安装

为了提升我们的系统稳定性以及资料的安全性&#xff0c;建议大家将电脑桌面的资料默认路径设置为D盘或其他磁盘&#xff0c;这样不仅会减少系统盘的占用空间&#xff0c;在系统盘出现故障时我们还可以通过pe工具备份桌面的资料。虽然我们也可以通过一些操作来修改桌面文件以及我…

文章解读与仿真程序复现思路——电网技术EI\CSCD\北大核心《考虑电动汽车动态拥堵的配电网灵活性资源双层优化调度 》

本专栏栏目提供文章与程序复现思路&#xff0c;具体已有的论文与论文源程序可翻阅本博主免费的专栏栏目《论文与完整程序》 论文与完整源程序_电网论文源程序的博客-CSDN博客https://blog.csdn.net/liang674027206/category_12531414.html 电网论文源程序-CSDN博客电网论文源…

嵌入式Python、决策树算法、SQLite、Flask、树莓派、机器学习:基于算法自主决策的智能家居系统(代码示例)

项目概述 随着物联网技术的快速发展&#xff0c;智能家居系统越来越普及&#xff0c;成为现代家庭生活的重要组成部分。本文将介绍一个基于Raspberry Pi和Arduino的智能家居算法控制系统的硬件平台。该系统能够通过传感器采集环境数据&#xff0c;并利用机器学习算法进行分析与…

大数据的数据质量有效提升的研究

大数据的数据质量有效提升是一个涉及多个环节和维度的复杂过程。以下是从数据采集、处理、管理到应用等方面&#xff0c;对大数据数据质量有效提升的研究概述&#xff1a; 一、数据采集阶段 明确采集需求&#xff1a;在数据采集前&#xff0c;需明确数据需求&#xff0c;包括…

VMware、Docker - 让虚拟机走主机代理,解决镜像封禁问题

文章目录 虚拟机全局代理配置找到 VMnet8 的 IPv4 地址代理相关配置虚拟机代理配置 Docker 代理配置修改镜像修改 Docker 代理配置 虚拟机全局代理配置 找到 VMnet8 的 IPv4 地址 a&#xff09;打开此电脑&#xff0c;输入 “控制面板”&#xff0c;然后回车. b&#xff09;之…

【计算机毕业设计】850汽车售后服务信息管理系统

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

【计算机方向】五本“三区水刊”重磅推荐!几乎不拒收,国人发文友好!

本期将为您带来五本计算机SCI 妥妥毕业神刊&#xff01; AUTONOMOUS AGENTS AND MULTI-AGENT SYSTEMS International Journal on Document Analysis and Recognition COMPUTATIONAL INTELLIGENCE IET Biometrics ACM Transactions on Asian and Low-Resource L…

C语言 | Leetcode C语言题解之第283题移动零

题目&#xff1a; 题解&#xff1a; void swap(int *a, int *b) {int t *a;*a *b, *b t; }void moveZeroes(int *nums, int numsSize) {int left 0, right 0;while (right < numsSize) {if (nums[right]) {swap(nums left, nums right);left;}right;} }

Layui表格合并、表格折叠树

1、核心代码&#xff1a; let tableMerge layui.tableMerge; // 引入合并的插件&#xff0c;插件源文件在最后let tableData [{pid: 0,cid: 111,sortNum: 1, // 序号pName: 数据父元素1,name: 数据1,val: 20,open: true, // 子树是否展开hasChild: true, // 有子数据opt: 数据…

代码随想录训练第三十天|01背包理论基础、01背包、LeetCode416.分割等和子集

文章目录 01背包理论基础01背包二维dp数组01背包一维dp数组(滚动数组) 416.分割等和子集思路 01背包理论基础 背包问题的理论基础重中之重是01背包&#xff0c;一定要理解透&#xff01; leetcode上没有纯01背包的问题&#xff0c;都是01背包应用方面的题目&#xff0c;也就是…

Pytorch深度学习实践(8)多分类任务

多分类问题 多分类问题主要是利用了Softmax分类器&#xff0c;数据集采用MNIST手写数据集 设计方法&#xff1a; 把每一个类别看成一个二分类的问题&#xff0c;分别输出10个概率 但是这种方法存在一种问题&#xff1a;不存在抑制问题&#xff0c;即按照常规来讲&#xff0c…

stm32h7串口发送寄存器空中断

关于stm32串口的发送完成中断UART_IT_TC网上资料挺多的&#xff0c;但是使用发送寄存器空中断UART_IT_TXE的不太多 UART_IT_TC 和 UART_IT_TXE区别 UART_IT_TC 和 UART_IT_TXE 是两种不同的 UART 中断源&#xff0c;用于表示不同的发送状态。它们的主要区别如下&#xff1a; …