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

文章目录


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.典型的双向迭代器包括: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中的元素插入到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::refstd::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()

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

  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员爱德华

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值