98中的一个例子
如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法
#include <algorithm>
#include <functional>
int main()
{int array[] = {4,1,8,5,3,7,0,9,2,6};// 默认按照小于比较,排出来结果是升序std::sort(array, array+sizeof(array)/sizeof(array[0]));// 如果需要降序,需要改变元素的比较规则std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());return 0;
}
排序一个单链表
如果待排序元素为自定义类型,需要用户定义排序时的比较规则:
struct Goods
{string _name;double _price;
};
struct Compare
{bool operator()(const Goods& gl, const Goods& gr){return gl._price <= gr._price;}
};
int main()
{Goods gds[] = { { "苹果", 2.1 }, { "相交", 3 }, { "橙子", 2.2 }, { "菠萝", 1.5 } };sort(gds, gds + sizeof(gds) / sizeof(gds[0]), Compare());return 0;
}
每次为了实现一个algorithm算法, 都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便
lambda表达式
函数中声明函数
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods&left, const Goods& right){return left._price < right._price; });
lambda表达式语法
lambda表达式书写格式:
- [capture-list] (parameters) mutable -> return-type { statement }
- lambda表达式各部分说明
[capture-list]
: 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。(parameters)
:参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。->return-type
:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。{statement}
:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
[捕捉列表](参数列表)mutable->返回值类型{//函数体};
[]{}; //最简单的lambda表达式
- 捕获列表说明
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。- [var]:表示值传递方式捕捉变量var
- [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
- [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
- [this]:表示值传递方式捕捉当前的this指针
注意:
- 父作用域指包含lambda函数的语句块
- 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量 [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量 c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复 - 在块作用域以外的lambda函数捕捉列表必须为空。
- 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。(主函数只能捕获主函数声明得变量)
int a = 3, b = 4;
[=,&g_a]{return a + 3; }; //验证4
- lambda表达式之间不能相互赋值,即使看起来类型相同,找不到operator=()。
auto f1 = []{cout << "hello world" << endl; };
auto f2 = []{cout << "hello world" << endl; };
//f1 = f2; // 编译失败--->提示找不到operator=()
// 允许使用一个lambda表达式拷贝构造一个新的副本
auto f3(f2);
f3();
// 可以将lambda表达式赋值给相同类型的函数指针
PF = f2;
PF();//执行相应得表达式
// 最简单的lambda表达式, 该lambda表达式没有任何意义[]{};// 省略参数列表和返回值类型,返回值类型由编译器推导为intint a = 3, b = 4;[=]{return a + 3; }; //想要在a的基础上+3返回。//但是这个lambda表达式没有用//因为没有取名字// 省略了返回值类型,无返回值类型//不知道lambda表达式的类型就用auto//[&]以引用的方式捕获当前主函数的变量 a=3;b=13;//[=]以值得方式进行捕获 a=3;b=4;auto fun1 = [=](int c)mutable{b = a + c; };fun1(10);cout << a << " " << b << endl;// 各部分都很完善的lambda函数// [=] 以值的方式捕获所有的变量// [&] 以引用的方式捕获所有的变量// [=,&b] 对于b变量以引用得方式捕获,对于其它变量用值的方式auto fun2 = [=, &b](int c)->int{return b += a + c; };cout << fun2(10) << endl;// 复制捕捉x// 以值得方式捕获x,函数内修改不会影响外部int x = 10;auto add_x = [x](int a) mutable { x *= 2; return a + x; };cout << add_x(10) << endl;return 0;
仿函数与lambda表达式的联系
函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了operator()运算符的类对象
class Rate
{
public:Rate(double rate) : _rate(rate){}double operator()(double money, int year){return money * _rate * year;}
private:double _rate;
};
int main()
{// 函数对象double rate = 0.49;Rate r1(rate); //定义一个对象将利率传进去r1(10000, 2); //对象调用自身的方法,跟函数调用比较像都是 名字()// 仿函数//=捕获rateauto r2 = [=](double monty, int year)->double{return monty*rate*year; };r2(10000, 2);return 0;
}
函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到。
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载operator()。
lambda表达式的应用
int array[] = { 1, 2, 3, 4, 5 };
for_each(array, array + 5, [](int&c){c *= 2; });
for_each(array, array + 5, [](int c){cout << c<<" "; });
线程库
#include<thread>
void ThreadFunc(int a)
{cout << "Thread1" << a << endl;
}
class TF
{
public:void operator()(){cout << "Thread3" << endl;}
};int main()
{TF tf;//线程函数尾函数指针thread t1(ThreadFunc, 10);//线程函数为lambda表达式thread t2([]{cout << "Thread2" << endl; });//线程函数为函数对象thread t3(tf);t1.join();t2.join();t3.join();cout << "Main thread!" << endl;system("pause");return 0;
}
线程之间不能互相赋值,也不能拷贝
线程的启动
C++线程库通过构造一个线程对象来启动一个线程,该线程对象中就包含了线程运行时的上下文环境,比如:线程函数、线程栈、线程起始状态等以及线程ID等,所有操作全部封装在一起,最后在底层统一传递给_beginthreadex() 创建线程函数来实现 (注意_beginthreadex是windows中创建线程的底层c函数)。std::thread()创建一个新的线程可以接受任意的可调对象类型(带参数或者不带参数),包括lambda表达式(带变量捕获或者不带),函数,函数对象,以及函数指针。
#include<thread>
void ThreadFunc1(int& x)
{cout << &x << " " << x << endl;x += 10;
}void ThreadFunc2(int*x)
{*x += 10;
}int main()
{int a = 10;//在线程函数中对a修改,不会影响外部实参,因为:线程函数虽然是引用方式,但其实际引用的是线程栈中的拷贝thread t1(ThreadFunc1, a);t1.join();cout << &a <<" "<< a << endl;//地址的拷贝thread t3(ThreadFunc2, &a);t3.join();cout << a << endl;system("pause");return 0;
}
线程的结束
1. join()方式
join():会主动地等待线程的终止。在调用进程中join(),当新的线程终止时,join()会清理相关的资源,然后返回,调用线程再继续向下执行。由于join()清理了线程的相关资源,thread对象与已销毁的线程就没有关系了,因此一个线程的对象每次你只能使用一次join(),当你调用的join()之后joinable()就将返回false了。主线程会阻塞
2. detach()
detach:会从调用线程中分理出新的线程,之后不能再与新线程交互。就像是你和你女朋友分手,那之后你们就不会再有联系(交互)了,而她的之后消费的各种资源也就不需要你去埋单了(清理资源)。此时调用joinable()必然是返回false。分离的线程会在后台运行,其所有权和控制权将会交给c++运行库。同时,C++运行库保证,当线程退出时,其相关资源的能够正确的回收
原子性操作
多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。比如:
#include <iostream>
using namespace std;
#include <thread>
unsigned long sum = 0L;
void fun(size_t num)
{for (size_t i = 0; i < num; ++i)sum++;
}
int main()
{cout << "Before joining,sum = " << sum << std::endl;thread t1(fun, 10000000);thread t2(fun, 10000000);//两个线程每个每回都循环10000000次,每次加1//如果没问题应该是20000000t1.join();t2.join();cout << "After joining,sum = " << sum << std::endl;system("pause");return 0;
}
通过加锁保证线程安全
#include <iostream>
using namespace std;
#include <thread>
#include<mutex>
unsigned long sum = 0L;mutex m;
void fun(size_t num)
{for (size_t i = 0; i < num; ++i){m.lock();sum++;m.unlock();}
}
int main()
{size_t begin = clock();cout << "Before joining,sum = " << sum << std::endl;thread t1(fun, 10000000);thread t2(fun, 10000000);//两个线程每个每回都循环10000000次,每次加1//如果没问题应该是20000000t1.join();t2.join();cout << "After joining,sum = " << sum << std::endl;size_t end = clock();cout << end - begin << endl; //计算时间system("pause");return 0;
}
加锁后,能够保证线程安全,但是耗费的时间就比较多,而且有可能导致死锁
原子操作
原子操作:一但开始,不能被打断
对于内置类型
对于自定义类型
使用atomic模板,定义出需要的任意原子类型
atomic<T> t;
注意事项
原子类型通常属于“资源型”数据,多个线程只能访问单个原子类型的拷贝,因此在c++11中原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造,移动构造以及operator=等,于是,标准库已经将atmoic模板类中的拷贝构造,移动构造,赋值运算符的重载默认删除了
#include <iostream>
using namespace std;
#include <thread>
#include<atomic>//unsigned long sum = 0L;
atomic_long sum{0}; //定义原子类型变量void fun(size_t num)
{for (size_t i = 0; i < num; ++i){sum++;}
}
int main()
{size_t begin = clock();cout << "Before joining,sum = " << sum << std::endl;thread t1(fun, 10000000);thread t2(fun, 10000000);//两个线程每个每回都循环10000000次,每次加1//如果没问题应该是20000000t1.join();t2.join();cout << "After joining,sum = " << sum << std::endl;size_t end = clock();cout << end - begin << endl; //计算时间system("pause");return 0;
}
时间更短,也能保证线程安全