其中comp为函数指针。
通过C语言的函数指针调用函数,不能内联,效率低(因为有函数调用开销)
template<typename T>
bool myless(T a, T b)
{
return a < b;
}
template<typename T, typename Compare>
bool compare(T a, T b, Compare comp)
{
return comp(a,b); // 函数指针调用函数
}
int main()
compare(10, 20, myless<int>);
1、通过函数对象对用operator(),可以省略函数的调用开销,比通过函数指针调用函数效率高(函数指针不能inline)
2、因为函数对象是用类生成的,所以可以添加相关的成员变量,用来记录函数对象使用时更多可用的信息。
3、使用函数对象时,不需要事先定义一个类。
template<typename T>
class myless(T a, T b)
{
public:
bool operator()(T a, T b) // 二元函数对象(两个参数)
{
return a < b;
}
}
template<typename T, typename Compare>
bool compare(T a, T b, Compare comp)
{
return comp(a,b); // operator() (a,b)
}
int main()
compare(10, 20, myless<int>());
例子1:优先级队列:
priority_queue<int, vector<int>, greater<int>> ay;
当你使用 greater
时,你实际上是在创建一个 greater
的实例,而不是调用一个函数。在这种情况下,不需要使用括号。括号通常用于调用函数。
例子2:有序set:
set默认小到大,当需要他从大到小时,可以传入一个函数对象,如下:
set<int, greater<int>> set1;
例子3:排序算法:
std::sort(vec.begin(), vec.end(), std::greater<int>());
greater 是一个函数对象类,它定义了一个函数调用运算符 operator(),用于比较两个元素的大小。
然而,如果你要使用函数对象来进行比较操作,你需要使用括号来调用该函数对象的函数调用运算符 operator()。这个函数调用运算符是函数对象类中的一个成员函数,用于执行比较操作。例如:
greater<int> comp;
bool result = comp(5, 10); // 调用函数对象的函数调用运算符
在上面的示例中,comp 是greater
的一个实例,通过调用函数对象的函数调用运算符,可以将 5 和 10 进行比较。
需要注意的是, std::greater
是一个二元函数对象,可以被用于比较两个值的大小。
如果 std::greater() 是一个一元函数对象,它将无法正确地用作 std::sort 的比较器。因为 std::sort 需要一个接受两个参数的比较函数对象来进行排序操作。
但是在优先队列的模板参数中,我们只需提供函数对象类型,而不需要调用它的函数调用运算符。优先队列会在内部使用这个函数对象进行比较操作。因此,在优先队列的模板参数中,我们只需提供函数对象的类型 greater
,而不需要调用它。
函数绑定器(Function binders)是一种函数适配器
bind1st和bind2nd在STL中主要用于二元函数对象,将其中的一元绑定成一个固定的量,成为一元函数变量。
bind1st和bind2nd+二元函数对象 =》 一元函数对象
以greater
为例,如果我现在想在一个已经排序完成的数组vec中在正确的位置插入70,可以使用bind1st绑定
将70按顺序插入vec容器中--》找一个小于70的数字
operator()(const T &val)
greater a > b
less a < b
auto it1 = find_if(vec.begin(), vec.end(), bind1st(greater<int>(), 70));
或
auto it1 = find_if(vec.begin(), vec.end(), bind2nd(less<int>(), 70));
if(it1 != vec.end())
{
vec.insert(it1, 70);
}
bind是bind1st和bind2nd的升级,他们本身也是函数对象。
#include
#include
void printValues(int a, int b) {
std::cout << "a: " << a << ", b: " << b << std::endl;
}
int main() {
// 使用 std::bind1st
auto printA = std::bind1st(std::ptr_fun(printValues), 42);
printA(10); // 输出:a: 42, b: 10
// 使用 std::bind2nd
auto printB = std::bind2nd(std::ptr_fun(printValues), 20);
printB(30); // 输出:a: 30, b: 20
// 使用 std::bind
auto printC = std::bind(printValues, std::placeholders::_1, std::placeholders::_2);
printC(50, 60); // 输出:a: 50, b: 60
return 0;
}
lambda表达式底层就是函数对象的实现。
语法:
[捕获外部变量] (形参列表) -> 返回值 {操作代码}
lambda表达式省去了定义类的步骤。
返回值:
如果返回值不需要,那么"->返回值“是可以省略的。
捕获:
[ ]:不捕获任何外部变量
[=]:以传值的方式捕获外部所有变量
[&]:以传引用的方式捕获外部所有变量(形参修改了,外部也修改)
[this]:捕获外部的this
[=, &a]:以传值的方式捕获外部所有变量,但是a变量以传引用的方式捕获
[a, &b]:以传值的方式捕获外部变量a,以传引用的方式捕获外部变量b
int a = 10; int b = 20;
auto func3 = [a, b] () mutable
// 加上mutable使得a,b作为捕获参数可以在lambda表达式内部修改
// 但是因为a,b是作为值进行传递的,所以a和b的值并不会发生改变
{
int tmp = a;
a = b;
b = tmp;
}
func3();
请注意,[ ]中的参数属于函数对象的成员变量ma, mb,如下所示:
而重载括号运算符函数为常量函数,不能修改成员变量的值,所以一般[ ]中的参数在函数代码中为不能改变。
但如果是传的引用,就可以直接修改而不用加mutable,如下:
int a = 10; int b = 20;
auto func3 = [&] ()
// 加上&使得a,b可以作为引用传递,所以a和b的值会发生改变
{
int tmp = a;
a = b;
b = tmp;
}
func3();
例1:
前面提到,要在一个有序数组中找到第一个小于70的数字,如下:
auto it1 = find_if(vec.begin(), vec.end(), bind2nd(less<int>(), 70));
因为less是一个二元函数对象,所以需要bind2nd达到 a < 70 的效果。
但是这样的代码比较冗余,于是C++11提供了lambda表达式来简化代码,如下:
auto it1 = find_if(vec.begin(), vec.end(), [] (int val)->bool {return val < 48;} );
[ ]:捕获外部变量
(…):小括号重载函数的形参列表(一个int val形参,一元函数对象)
bool:函数返回值
{…}:函数代码
例2:
for_each可以遍历容器的所有元素,可以自行添加合适的函数对象,对容器的元素进行过滤。
下面的例子为过滤vec中的偶数。
for_each(vec.begin(), vec.end(),
[](int val)->void
{
if(val % 2 == 0) cout << val << ' ';
}
);
例3:
从大到小排序
sort(vec.begin(), vec.end(),
[] (int a, int b)->bool
{
return a > b;
}
);
函数对象的缺点:
函数对象使用在泛型算法参数传递、比较性质、自定义操作(优先级队列)
如果一个地方使用函数对象,就得定义出一个类,而且是只有小括号重载函数的简短类。灵活性太差。
lambda表达式的好处:
把lambda作为函数对象传入表达式,是非常精悍的。
auto func3 = [&] ()
// 加上&使得a,b可以作为引用传递,所以a和b的值会发生改变
{
int tmp = a;
a = b;
b = tmp;
}
不用auto:
// function 在<>中: <返回类型(输入类型1,输入类型2)>
map<int, function<int(int,int)>> caculateMap;
caculateMap[1] = [] (int a, int b)->int { return a+b; };
.....
智能指针:
如果我想用智能指针管理FILE*,但是FILE的内存需要用fclose(FILE*)而不是delete,所以需要进行自定义删除函数。
unique ptr<FILE, function<void(FILEx)>>
ptr1(fopen("data.txt","w"), [] (EILE *pf) fclose (pf); });
优先级队列:
1、题目:347.找出topK的元素
方式一:根据函数对象的调用,根据键值对的第二位来进行大小的比较排序,如下:定义:
priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pri_que;
仿函数mycomparison如下:
class mycomparison {
public:
bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) {
return lhs.second > rhs.second;
}
};
但是这个不灵活,且需要定义一个类,很冗余。
方式二:lambda表达式
using FUNC = function<bool(pair<int, int>& , pair<int, int>&)>;
priority_queue<pair<int, int>, vector<pair<int, int>>, FUNC> pri_que
( [] (pair<int, int>& lhs, pair<int, int>& rhs)->bool
{
return lhs.second > rhs.second;
} );
或
priority_queue<pair<int, int>, vector<pair<int, int>>,
decltype
(
[](const auto& lhs, const auto& rhs)
{
return lhs.second > rhs.second;
}
)> pri_que;
decltype获取 Lambda 表达式的类型。
2、自定义Data类,让priority_queue比较其中成员变量的大小。
using FUNC = function<bool(Data&, Data&)>;
priority_queue<Data, vector<Data>, FUNC> maxHeap
( [] (Data& d1, Data& d2)->bool
{
return d1.ma > d2.ma; //自定义规则
}
);
maxHeap.push(Data(10,20);
maxHeap.push(Data(10,30); // 非常灵活,对Data的成员变量可以设置任意规则进行比较
详情见:博客
根据右值,推导出右值的类型
指针专用。(之前的NULL指针和整数是混用的)
move移动语义函数和forward类型完美转发函数
详情见:博客
shared_ptr和weak_ptr
详情见:博客
增删插的效率很高
红黑树为底层,有序容器
哈希表为底层,增删插的效率为O(1)
array
vector 建议使用
farword_list双向链表
list 建议使用
概念见:博客
之前都是使用的操作系统的接口,如:
createThread pthread_create clone
这样对跨平台很不友好,兼容性差。
int main
{
// 创建了一个线程对象,传入一个线程函数,新线程就开始运行了
std::thread t1(threadHandle1, 2);
xxxxx 主程序
// 主线程等待子线程结束,主线程继续往下运行
//t1.join();
//把子线程设置为分离线程
t1.detach();
cout << "main thread done!" << endl;
当主线程运行完成时,如果查看当前进程还有未运行完成的子线程,进程就会异常终止
return 0;
}
子线程函数运行完成,线程结束
1、主线程等待子线程结束 join
2、把子线程设置为分离线程,不管子线程 detach
竞争态条件,有不是线程安全的代码=》临界区代码段=》保障原子操作=》互斥锁mutex
所以临界区代码需要=》操作=》互斥锁
锁+双重判断
场景:线程1必须依赖线程2的通知
例子:生产者消费者模型
1、创建线程安全的lockQueue
std::mutex mtx; // 定义互斥锁,做线程间的互斥操作
std::condition_variable cv; // 定义条件变量,做线程间的同步通信操作
// 生产者生产一个物品,通知消费者消费一个;消费完了,消费者再通知生产者继续生产物品
class Queue // 对queue重新封装一下
{
public:
void put(int val) // 生产物品
{
//lock_guard guard(mtx); // scoped_ptr 不能同时使用两把锁
unique_lock<std::mutex> lck(mtx); // unique_ptr
while (!que.empty())
{
// que不为空,生产者应该通知消费者去消费,消费完了,再继续生产
// 生产者线程进入#1等待状态(阻塞状态),并且#2把mtx互斥锁释放掉 消费者线程就能抢到这把锁 不释放锁 无法消费
cv.wait(lck); // lck.lock() lck.unlock
}
que.push(val);
/*
notify_one:通知另外的一个线程的
notify_all:通知其它所有线程的
通知其它所有的线程,我生产了一个物品,你们赶紧消费吧
其它线程得到该通知,就会从等待状态 =》 阻塞状态 =》 获取互斥锁才能继续执行
*/
cv.notify_all();
cout << "生产者 生产:" << val << "号物品" << endl;
}
int get() // 消费物品
{
//lock_guard guard(mtx); // scoped_ptr
unique_lock<std::mutex> lck(mtx); // unique_ptr
while (que.empty())
{
// 消费者线程发现que是空的,通知生产者线程先生产物品
// #1 进入等待状态 # 把互斥锁mutex释放
cv.wait(lck);
}
int val = que.front();
que.pop();
cv.notify_all(); // 通知其它线程我消费完了,赶紧生产吧
cout << "消费者 消费:" << val << "号物品" << endl;
return val;
}
private:
queue<int> que;
};
void producer(Queue *que) // 生产者线程 生产10个物品
{
for (int i = 1; i <= 10; ++i)
{
que->put(i);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
void consumer(Queue *que) // 消费者线程
{
for (int i = 1; i <= 10; ++i)
{
que->get();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main()
{
Queue que; // 两个线程共享的队列
std::thread t1(producer, &que);
std::thread t2(consumer, &que);
t1.join(); // 主线程等到两个子线程执行完 继续执行
t2.join();
return 0;
}
线程间互斥 : 临界区 原子类型 互斥锁 信号量
线程间同步 : 条件变量 信号量
1、std::mutex
std::mutex mtx;
mtx.lock()
mtx.unlock()
2、lock_guard
只能在简单的临界区代码段的互斥操作中,不可能用在函数参数传递或者返回函数中。
因为函数参数传递或者返回过程中都会用到拷贝构造和赋值,但是lock_guard拷贝构造和赋值(引用和右值引用)都被delete了。
lock_guard<std::mutex> guard(mtx)
3、unique_lock
不仅能在简单的临界区代码段的互斥操作中,还能用在函数参数传递(函数调用)中。
虽然unique_lock底层左值引用的拷贝构造和赋值被delete,但是它提供了右值引用的拷贝构造和赋值。
unique_lock<std::mutex> lck(mtx)
4、条件变量
std::condition_variable cv;
cv.wait(lck); //guard输入不了,因为lock_guard没有拷贝构造
// 使线程进入等待状态,并且把lck.unlock可以把mtx释放掉
cv.notify_all();
// 通知再cv上等待的线程,条件成立了,起来干活了
// 收到通知=》从等待状态到阻塞状态=》获取互斥锁了=》线程继续执行
notify_one();
// 通知再cv上等待的一个线程