目录
C++11简介
统一的列表初始化
{}初始化
std::initializer_list
声明
auto
decltype
nullptr
范围for循环
智能指针
STL中的一些变化
右值引用和移动语义
左值引用和右值引用
右值引用的意义
完美转发
lambda表达式
新的类功能
可变参数模版
包装器
function包装器
bind
线程库
线程函数参数
lock_guard和unique_lock
原子性操作库
两个线程交替打印
C++11简介
相比于C++98/03,C++11发生了较大的变化,大约新增了约140个新特性,以及修正了约600个缺陷,这使得C++更加强大。
统一的列表初始化
{}初始化
在C++98中,允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如:
struct Point
{int _x;int _y;
};int main()
{int array1[] = { 1,2,3,4,5 };Point p = { 1,2 };return 0;
}
C++11扩大了用大括号括起的列表的使用范围,使其可用于所有的内置类型和用户自定义类型,列表初始化时,可以添加等号(=),也可以不添加,但是不太建议去掉。
int arr1[]{ 1,2,3,4,5 };
int arr2[5]{ 0 };
Point p2{ 1,2 };
对于自定义类型创建对象时也可以用列表初始化方式调用构造函数初始化:
class A
{
public:A(int x, int y):_x(x), _y(y){}A(int x):_x(x), _y(x){}
private:int _x;int _y;
};
int main()
{A a5(6);//单参数的隐式类型转换A a3 = 1;A a4 {2};//多参数的隐式类型转换//C++支持的列表初始化,这里{1,2}先调用构造函数初始化,再调用拷贝构造赋值给a1 A a1 = { 1,2 };A a2 { 3,4 };return 0;
}
std::initializer_list
//vector可以这样初始化
vector<int> v1(10, 1);
//但是vector不能用{}初始化
vector<int> v2 = {1,2,3,4,5};
为什么vector用不了{}这样构造呢?{...}里面可能有1/2/3/4/....个元素,但是vector的构造函数参数列表不知道到底该设置多少个,要写无数个构造函数才行,就像下面这样:
所以,为了支持这样的初始化,引入了initializer_list,
因此,我们现在可以将{...}的常量数组转化为initializer_list。
initializer_list的本质是两个指针,一个first指向第一个元素,一个last指向最后一个元素的下一个,为了验证这样一个事实,我们算一下i1的大小,在32位平台下,i1的大小是8(2个指针的大小):
initializer_list也支持简单的迭代器begin和end,迭代器的类型是const T*,由于它指向常量数组,所以是const T*,不支持修改。
所以,为了解决本节刚开始提出的问题,C++11使用了这样的方式解决:
vector(initializer_list<T> i1);
这个构造一劳永逸的解决了问题,不用像上面那样麻烦的方式解决。
initializer_list可以接收常量数组,本质是两个指针,一个指向数组的开始,一个指向数组最后的下一个。这样,就算{...}里有5个8个,都可以传给initializer_list。
当 X自定义 = Y类型,这个时候就会隐式类型转换,需要有 X(Y mm),即X支持Y为参数类型的构造就可以。
在上面图中,{1,2,3,4,5}会被识别成initializer_list,下面的那个,等号右侧生成一个临时对象,再去拷贝构造给v4。
再举一个例子,创建map对象时,也可以用{}进行创建:
声明
auto
在C++11中,auto用于自动推断类型,这样要求必须进行显示初始化。
int main()
{int i = 10;auto p = &i;cout << typeid(p).name() << endl;map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };//map<string, string>::iterator it = dict.begin();auto it = dict.begin();return 0;
}
decltype
关键字decltype将变量的类型声明为表达式指定的类型。它的功能和typeid有点像,typeid可以拿到对象的类型,但是得到的只是一个单纯的字符串,不能用来定义对象,如果想定义一个和typeid一样的值,这就做不到了。
但是decltype可以做到:
但是,有人可能会认为,我用一个auto不也可以吗?但是,我们换一个场景:
我想用ret3的返回值类型去初始化class B的模版参数,这时候typeid就不行了,但是可以用decltype来确定ret3的类型,
B<decltype(ret3)> b;
虽然auto和decltype虽然可以方便我们写代码,但是有些地方增加代码读起来的难度,所以慎用auto和decltype!
nullptr
由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
范围for循环
我们在之前的学习中,已经多次使用了范围for,这里不再赘述。
只有一点需要注意,我们最好把&加上,即for(auto& e:v)或者for(const auto& e:v)(在不改变值的情况下加const),这样可以防止深拷贝,提高效率。
智能指针
我们之后单独学习这块。
STL中的一些变化
新容器
array:对越界检查更严格,但是可以用vector来替代,所以用处不大,是一个挺失败的设计。
forward_list:单向链表,相比list节省一个指针,但是也不好用。
容器内部
几乎在每个容器内部,都增加了initializer_list、移动构造(这个很有用),
还有emplace、empalce_back:
右值引用和移动语义
左值引用和右值引用
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
什么是左值和右值
左值和右值的区分关键,是能否取地址。可以取地址的是左值,不能取地址的是右值。
因此,a、b、c均是左值。
左值不一定是值,也可能是表达式。如*p也是左值,因为它可以取地址。
类似的,v[1]也是左值,它可以取地址。
右值也是一个表达数据的表达式,它不能取地址,如字面常量、匿名对象、临时对象。
什么是左值引用和右值引用
引用就是取别名,左值引用就是给左值取别名,右值引用就是给右值取别名,
右值引用就是用&&表示:
自定义的类型右值主要有两种,一类是匿名对象,一类是传值返回的临时对象。
左值引用不能给右值取别名,但是const 左值引用可以,
const string& ref1 = string("1111");
右值引用也不能给左值取别名,但是可以给move以后得左值取别名。
左值引用总结:
1. 左值引用只能引用左值,不能引用右值。
2. 但是const左值引用既可引用左值,也可引用右值。
右值引用总结:
1. 右值引用只能右值,不能引用左值。
2. 但是右值引用可以move以后的左值。
右值引用的意义
引用的意义是减少拷贝,提高效率, 比如引用传参,
void func1(const string& s);
如果不用引用,那传参时就要拷贝,像map这种大一点的容器拷贝的代价很大,
还有传引用返回,
string& func2();
左值引用的场景主要就是引用传参和传引用返回。但是,左值引用返回值的问题没有彻底解决,因为如果是func2中局部对象,不能用引用返回,它的生命周期就在func2里面,出了作用域就销毁了,同时,也不能用右值引用返回,右值引用也不能解决生命周期的问题,
例如,bit::string to_string(int value)函数中可以看到,这里只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。
to_string的返回值用左值引用和右值引用都不太行,因为返回值str出了作用域都会被销毁,在C++11中,另辟蹊径,增加了一个参数为右值引用的构造函数--移动构造,(对比:参数为左值引用的构造函数为拷贝构造),移动构造和拷贝构造构成函数重载。
在没有移动构造时,拷贝构造string(const string& s)既可以接收左值,又可以接收右值;但是在有了移动构造时,编译器会找最匹配的。
在C++11中,将右值做了区分,分为纯右值和将亡值,内置类型的那种是纯右值,自定义类型的那种是将亡值,在移动构造中,可以这样写:
//移动构造
//右值(将亡值)
string(string&& s):_str(nullptr)
{cout << "string(string&& s) -- 移动构造" << endl;swap(s);
}
既然已经是将亡值,那么不妨把我的和将亡值交换一下,移动将亡值的资源,这样就不用拷贝构造了,效率就上来了
有人说右值引用延长了str的生命周期,这种说法是不正确的,str出了作用域就会被销毁,确切的说是延长了资源的生命周期。
如果是下面这种形式,如果没有移动拷贝和移动赋值,那就是一次拷贝构造和一次赋值拷贝,编译器不敢将这两步合二为一(因为这是两个不同的操作),
如果有了移动拷贝和移动赋值,那就是一次移动拷贝和一次移动赋值,
移动构造、移动赋值和我们之前拷贝构造的现代写法有点像,但是这两种有本质的区别,现代写法是让一个临时对象去构造并转移它的资源,并没有提高效率,而移动构造、移动赋值给我的右值就是一个将亡值,直接转移这个将亡值的资源,代价很小。
我们再来看一些关于右值引用的其他问题:
我们知道,std::string("111111111111")本身是右值,但是右值引用本身(s1)是左值,因为只有右值引用本身处理成左值,才能实现移动构造和移动赋值,转移资源(右值不能转移资源)。这样的意思,是为了移动构造和移动赋值的转移资源的逻辑是自洽的。
我们来看一下C++11中其他地方用到右值引用的,
由于s1是左值,所以push_back不能调用移动拷贝,只能做深拷贝,
但是,如果想上图这样,将匿名对象放到push_back的参数中,就会调用移动构造。在push_back这类函数时,使用匿名对象就更好了,这样就不会设计到深拷贝,只需要移动构造,提高效率。可见,右值引用不仅在返回值有意义,也在参数值有意义。
也可以这样,这样就会先发生隐式类型转换,将const char*转换为string,转换时会产生临时对象,是右值,临时对象具有常性,会调用移动构造。
最后一个问题,如果list里的值类型是int(内置类型)或者日期类(浅拷贝自定义类型),还会涉及到移动拷贝和移动构造吗?
不会涉及,上面效率提升,针对的是自定义类型的深拷贝的类,因为深拷贝的类才有转移资源的移动系列函数;对于内置类型和浅拷贝自定义类型,没有移动系列函数。
完美转发
模版中的&&万能转发
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }//函数模版里面,这里叫做万能引用
template<typename T>
void PerfectForward(T&& t)
{Func(t);
}void PerfectForward(int& t)
{Fun(t);
}int main()
{PerfectForward(10); // 右值int a;PerfectForward(a); // 左值PerfectForward(std::move(a)); // 右值const int b = 8;PerfectForward(b); // const 左值PerfectForward(std::move(b)); // const 右值return 0;
}
函数模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力。
但是,我们看程序运行的结果,
因为在函数参数t接收后,后续都退化成了左值,所以都会调用Fun(int& x)和Fun(const int& x)这两个函数,但是这不是我们想要的结果,我们希望能够在传递的过程中保持它的左值或者右值的属性,那么就需要用到完美转发:
std::forward 完美转发在传参的过程中保留对象原生类型属性。
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }//函数模版里面,这里叫做万能引用
//实参传左值,就推成左值引用,实参传右值,就推成右值引用
template<typename T>
void PerfectForward(T&& t)
{//std::forward<T>(t)在传参的过程中保持了t的原生类型属性Fun(std::forward<T>(t));
}void PerfectForward(int& t)
{Fun(t);
}int main()
{PerfectForward(10); // 右值int a;PerfectForward(a); // 左值PerfectForward(std::move(a)); // 右值const int b = 8;PerfectForward(b); // const 左值PerfectForward(std::move(b)); // const 右值return 0;
}
什么时候用完美转发呢?在函数模版里,想要达到传什么就保持它的属性,就用完美转发。这是一道常见的面试题。
lambda表达式
在C++98中,如果想要对一个自定义类型进行排序,需要用到仿函数,用户自定义排序的比较规则,比如,有这样一个自定义类型,
struct Goods
{string _name; // 名字double _price; // 价格int _evaluate; // 评价Goods(const char* str, double price, int evaluate):_name(str), _price(price), _evaluate(evaluate){}
};
我们有存储Goods类型的vector,想要对这个vector按照价格排序,既可以排升序,又可以排降序,可以使用algorithm这个头文件中的sort算法,但是如果需要对商品按照它的某一项属性排序,如价格,就需要自己写一个类来定义仿函数,
struct Goods
{string _name; // 名字double _price; // 价格int _evaluate; // 评价Goods(const char* str, double price, int evaluate):_name(str), _price(price), _evaluate(evaluate){}
};struct ComparePriceLess
{bool operator()(const Goods& g1, const Goods& g2){return g1._price < g2._price;}
};struct ComparePriceGreater
{bool operator()(const Goods& g1, const Goods& g2){return g1._price > g2._price;}
};int main()
{vector<Goods> v = { {"苹果",2.3,2},{"香蕉",3.2,4}, {"西瓜",0.9,3} };sort(v.begin(), v.end(), ComparePriceGreater());return 0;
}
但是,上面的方法有点复杂,每次都要自己写一个类,所以C++为了解决这样的问题,引入了lambda表达式。
lambda表达式实际是一个匿名函数,lambda表达式书写格式:
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }
各部分说明:
[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。
mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
注意:在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
对捕捉列表和mutable的进一步说明:
为了解决传值捕捉在函数内部改变不会影响外面的问题,引入了引用捕捉,引用捕捉是在捕捉列表变量前加&(这其实和取地址&有些冲突,但是在lambda表达式捕捉列表中我们认为&是引用捕捉) :
捕捉列表描述了上下文中哪些数据可以被lambda使用,以及使用的方式传值还是传引用。
[var]:表示传值捕捉变量var
[=]:表示传值方式捕捉父作用域的所有变量
[&var]:表示引用方式捕捉变量var
[&]:表示引用方式捕捉父作用域的所有变量
[this]:表示传值方式捕捉当前的this指针
除了上面传值和传引用捕捉方式以外,还有混合捕捉(一部分传值捕捉,一部分传引用捕捉):
因此,我们可以写lambda表达式作为一个匿名函数传给sort进行排序:
int main()
{vector<Goods> v = { {"苹果",2.3,2},{"香蕉",3.2,4}, {"西瓜",0.9,3} };auto priceLess = [](const Goods g1, const Goods g2)->bool {return g1._price < g2._price; };sort(v.begin(), v.end(), priceLess);//也可以直接传lambda表达式sort(v.begin(), v.end(), [](const Goods g1, const Goods g2){return g1._price < g2._price;});sort(v.begin(), v.end(), [](const Goods g1, const Goods g2){return g1._price > g2._price;});sort(v.begin(), v.end(), [](const Goods g1, const Goods g2){return g1._evaluate < g2._evaluate; });sort(v.begin(), v.end(), [](const Goods g1, const Goods g2){return g1._evaluate > g2._evaluate;});sort(v.begin(), v.end(), [](const Goods g1, const Goods g2){return g1._name < g2._name;});sort(v.begin(), v.end(), [](const Goods g1, const Goods g2){return g1._name > g2._name;});return 0;
}
其中,priceLess的类型,我不知你不知只有编译器知,见下图,是随机生成的类型。
其实,lambda表达式就是仿函数,lambda编译时,会生成对应的仿函数,上面的随机名字就是仿函数类的名称。
实际上,lambda表达式和范围for很类似,范围for替代成了迭代器,lambda替代成了仿函数。
新的类功能
在前面我们学习了右值引用,其实,右值引用的特点是和左值引用的特点进行了区分,是左值就匹配左值引用,是右值就匹配右值引用。移动语义是,当右值匹配到右值引用的时候,会调用移动构造和移动拷贝。它们针对的是深拷贝的自定义类型对象,如string、vector、list等,可以转移资源,提高效率。
原来的C++类中,有6个默认成员函数,构造函数、析构函数、拷贝构造函数、拷贝赋值重载、取地址重载、const取地址重载,前4个最重要,后两个用处不大。
C++11 新增了两个:移动构造函数和移动赋值运算符重载。
对于移动构造函数和移动赋值运算符重载,有一些需要注意的:
如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)。
如果提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
我们调试以下代码可以得到验证:
class Person
{
public:Person(const char* name = "张三", int age = 18):_name(name), _age(age){}/*~Person(){}*/
private:ghs::string _name;int _age;
};int main()
{Person s1;//默认拷贝构造Person s2 = s1;Person s3 = std::move(s1);Person s4;s4 = std::move(s2);return 0;
}
强制生成默认函数的关键字default
C++可以更好地控制要使用的默认函数。假设要使用某个默认的函数,但是因为某些原因这个函数没有默认生成。比如:当我们提供了拷贝构造,就不会生成移动构造,那么就可以使用default关键字指定移动构造生成。
class Person
{
public:Person(const char* name = "张三", int age = 18):_name(name), _age(age){}//强制生成Person(Person& p) = default;Person& operator=(Person& p) = default;Person(Person&& p) = default;Person& operator=(Person&& p) = default;~Person(){}
private:ghs::string _name;int _age;
};int main()
{Person s1;//默认拷贝构造Person s2 = s1;Person s3 = std::move(s1);Person s4;s4 = std::move(s2);return 0;
}
禁止生成默认函数的关键字delete:
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
继承和多态中的final与override关键字
这个我们在继承和多态章节已经进行了详细学习。
可变参数模版
在C++11之前,模版中的参数数量是固定的,但是在C++11中引入了可变参数模版,这无疑是一大进步。
我们先来看一个基本可变参数的函数模版:
template <class ...Args>
void ShowList(Args... args)
{}
其中,Args是一个模版参数包,代表0~N个类型,args是根据模版参数包定义的一个形参参数包。上面的参数Args前面有省略号,所以它是一个可变模版参数,我们把带省略号的参数成为参数包,包含0~N个模版参数。但是我们无法直接获取参数包args中的每个参数,只能通过展开参数包的方式来获取参数包中的每个参数。由于语法不支持args[i]的方式获取可变参数,所以我们用一些方法来获取参数包的每个值。
递归函数展开参数包
void _CppPrintf()
{cout << endl;
}
template<class T,class ... Args>
void _CppPrintf(const T& val, Args... args)
{cout << val << endl;_CppPrintf(args...);
}
template <class ... Args>
void CppPrintf(Args... args)
{_CppPrintf(args...);
}
int main()
{CppPrintf(1, 'A', std::string("sort"));return 0;
}
其编译时递归推导过程如下:
还有另外一个奇葩的推导方式:
STL中的emplace相关接口函数
可以看出,emplace_back对深拷贝类型有一定的优化,但是不那么明显,效率没有提升很多
其实,emplace_back对需要浅拷贝的效率更高,因为push_back浅拷贝类型,只能调用拷贝构造,即需要一次构造+一次拷贝构造,而emplace_back只需要调用一次构造就行。
总之,emplace_back可以直接构造,而无需调用拷贝构造!
当然,emplace_back除了上面的用多个参数进行构造,也可以用单参数构造,
因此,当使用emplace时,实参建议的选择顺序是:参数包 > 右值 > 左值。
包装器
function包装器
function包装器,也叫做适配器。C++中function本质是一个类模版,也是一个包装器。
ret = func(x);
上面的func可能是什么呢?可能是函数名、函数指针、仿函数或者lambda表达式,这些都是可调用对象!
但是,它们都多少有些问题:
函数指针 -> 类型定义复杂
仿函数对象 -> 要定义一个类,用的时候有点麻烦,不适合类型统一
lambda -> 没有类型概念
template<class F, class T>
T useF(F f, T x)
{static int count = 0;cout << "count:" << ++count << endl;cout << "count:" << &count << endl;return f(x);
}
double f(double i)
{return i / 2;
}
struct Functor
{double operator()(double d){return d / 3;}
};
int main()
{// 函数名cout << useF(f, 11.11) << endl;// 函数对象cout << useF(Functor(), 11.11) << endl;// lamber表达式cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;return 0;
}
运行以上程序,发现useF函数模版实例化了三份,这看起来有点麻烦,为了解决以上问题,引出了包装器。
std::function在头文件<functional>
//类模版原型
template <class Ret, class... Args>
class function<Ret(Args...)>;
模版参数说明:
Ret:被调用函数返回值
Args:被调用函数的形参
实际上,function的底层是仿函数,在调用时会调用operator()。
int f(int a, int b)
{return a + b;
}
struct Functor
{
public:int operator() (int a, int b){return a + b;}
};//不是定义可调用对象,而是包装可调用对象
int main()
{//空对象function<int(int, int)> fc1;//包装函数指针function<int(int, int)> fc2 = f;//包装仿函数对象function<int(int, int)> fc3 = Functor();//包装lambdafunction<int(int, int)> fc4 = [](int x, int y) {return x + y; };cout << fc2(1, 2) << endl;//fc2本质是调用了operator()cout << fc2.operator()(1, 2) << endl;cout << fc3(1, 2) << endl;cout << fc4(1, 2) << endl;return 0;
}
那我们不禁要问,为什么要给函数指针、仿函数、lambda外面套一个壳再使用呢,它们本身也是可以调用的啊。
包装器的一些玩法:逆波兰表达式求解
class Solution {
public:int evalRPN(vector<string>& tokens) {stack<int> st;//命令->动作(函数)map<string,function<int(int,int)>> m={{"+",[](int x,int y){return x+y;}},{"-",[](int x,int y){return x-y;}},{"*",[](int x,int y){return x*y;}},{"/",[](int x,int y){return x/y;}},};for(auto& e : tokens){if(m.count(e)) {function<int(int,int)> f = m[e];//操作符运算int right = st.top();st.pop();int left = st.top();st.pop();st.push(f(left,right));}else{//操作数入栈st.push(stoi(e));}} return st.top(); }
};
另外,如果我们想要包装成员函数指针,需要&类型::函数名这样调用。成员函数指针又分静态成员函数和非静态成员函数:
int main()
{//成员函数的函数指针 &类型::函数名//包装静态成员函数function<int(int, int)> f1 = &Plus::plusi;cout << f1(1, 2) << endl;//包装非静态成员函数,不能直接Plus::plusd,需要在前面加取地址符&,静态函数前可加可不加&/*function<double(double, double)> f2 = &Plus::plusd;cout << f2(1.1, 2.2) << endl;*///包装器的参数要和成员函数的参数一致,成员函数第一个参数是一个隐含的this指针function<double(Plus*, double, double)> f3 = &Plus::plusd;Plus plus;cout << f3(&plus,1.1, 2.2) << endl;//但是,上面包装非静态的成员函数有点麻烦,还需要定义一个类对象//是通过指针&plus或者对象Plus()去调用plusd,所以这里传指针和对象都可以function<double(Plus, double, double)> f4 = &Plus::plusd;cout << f4(Plus(), 1.1, 2.2) << endl;return 0;
}
bind
std::bind是一个函数模版,用于调整可调用对象的参数个数或者顺序。
bind函数可以看做一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来适应原对象的参数列表。
调用bind的一般形式:auto newCallable = bind(callable,arg_list);
newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,给callable提供实参,当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。
fn是要绑定的函数,args是要对应的参数,包含像_1、_2这样的名字,这些名字是占位符,_1为newCallable中第一个参数,_2为newCallable中第二个参数,依次类推。Ret是返回值的类型,我们可以显示实例化bind,以此来控制返回值的类型。
但是,调整顺序的意义不大,了解即可。
除了调整顺序以外,我们还可以调整参数个数:
class Sub
{
public:int sub(int a, int b){return a - b;}
};int main()
{//绑定,调整参数个数,把第一个参数用Sub()绑定死auto f4 = bind(&Sub::sub, Sub(), placeholders::_1, placeholders::_2);cout << f4(10, 5);cout << endl;auto f5 = bind(&Sub::sub, &sub, placeholders::_1, placeholders::_2);cout << f5(10, 5);return 0;
}
这样,我们得到绑定后的对象为f4,调用f4时,只需要传两个未绑定的参数a和b即可。
再来看一个例子:
void fx(const string& s, int x, int y)
{cout << s << " -> [血量:" << x << " 蓝:" << y <<"]" << endl;
}
int main()
{fx("赵云", 80, 46);fx("赵云", 78, 34);fx("赵云", 54, 13);return 0;
}
我们可以这样调fx,这很正常,但是调fx时每次都要传“赵云”这个参数,其实,我们可以绑定第一个参数一直是“赵云”,这样只需要输入参数x和y即可:
int main()
{auto f1 = bind(fx1, "赵云", placeholders::_1, placeholders::_2);f1(100, 89);f1(98, 76);return 0;
}
除了绑定第一个参数,我们还可以绑定第二个参数,比如在游戏中开挂,无论使用哪个角色,血量一直保持在100:
int main()
{auto f2 = bind(fx1, placeholders::_1, 100, placeholders::_2);f2("孙尚香", 46);f2("关羽", 93);return 0;
}
我们需要记住的是,_1代表第一个实参,_2代表第二个实参。
实际上,bind的返回值是一个类,里面重载了operator(),实际上会调用仿函数。
bind的返回除了传给auto,也可以传给function(因为bind的返回值是一个类,里面重载了仿函数),
线程库
我们之前了解的多线程问题,都是和平台相关的,比如windows和linux下都有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了,使得C++在多线程编程时不需要依赖第三方库。要使用标准库中的线程,必须包含<thread>头文件。
注意:
1.当创建线程对象后,如果使用无参构造,没有提供线程函数,该对象没有对应任何线程。
get_id()的返回值类型是id类型,id类型为std::thread命名空间下疯转的一个类。
2.当创建一个线程对象后,并且给线程关联函数,该线程就会被启动,与主线程一起运行。线程函数一般有3种提供方式:
- 函数指针
- lambda表达式
- 函数对象
函数指针:
void print(int n, int k)
{for (int i = k; i < n; i++){std::cout << i << " ";}std::cout << std::endl;
}int main()
{std::thread t1(print, 100, 0);std::thread t2(print, 200, 100);std::cout << t1.get_id() << std::endl;std::cout << t2.get_id() << std::endl;t1.join();t2.join();std::cout << std::this_thread::get_id << std::endl;return 0;
}
lambda表达式:
int main()
{int x = 0;std::mutex mtx;std::thread t1([&] {mtx.lock();for (int i = 0; i < 10000; i++){++x;}mtx.unlock();});std::thread t2([&]{mtx.lock();for (int i = 0; i < 10000; i++){++x;}mtx.unlock();});t1.join();t2.join();std::cout << x << std::endl;return 0;
}
可以使用this_thread类中的get_id来获取主线程id。
3.线程不支持拷贝,不允许拷贝构造和赋值,但是支持移动构造和移动赋值。
线程函数参数
线程函数参数是以值拷贝的方式拷贝到线程栈空间中的,因此,即使线程参数为引用类型,在线程中也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。
如果想要通过形参改变外部实参,必须借助std::ref()函数或者传入指针。
lock_guard和unique_lock
在多线程中,为了保证线程安全,需要通过锁的方式控制。
如果把锁放到for循环里面,也是线程安全的,但是这样会导致线程之间频繁切换,效率低。
上述代码中,其缺陷是,锁控制不好时,可能会造成死锁,比如在锁中间代码返回,或者在锁的范围内抛异常。因此,C++采用RAII的方式对锁进行了封装,即lock_guard和unique_lock。
那如果在func中前半部分希望加锁,而后半部分不希望加锁,只需要用{}把前半部分括住,定义一个局部域,LockGuard的生命周期就在这个{}局部域了。
而unique_lock和lock_guard类似,只不过功能更丰富一些,支持手动加锁解锁。
原子性操作库
传统解决线程安全的方法是对共享资源进行加锁保护,虽然加锁可以解决问题,但是加锁的缺陷是,只要要有一个线程在对sum++,其他线程就会被阻塞,影响效率,而且可能造成死锁。
为此,C++11中引入了原子操作,需要包含<atomic>库。
在C++11中,程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥访问。程序员可以使用atomic类模版,定义出需要的任意原子类型。
atomic<T> t; //声明一个类型为T的原子类型变量t
两个线程交替打印
要使两个线程交替打印,需要使用到条件变量:
条件变量condition_variable用于进行进程之间的互相通知。
为了使得线程交替打印,要保证线程1先执行,线程2后执行(哪怕把线程2放到前面)。
这里利用了wait,
由于条件变量wait不是线程安全的,因此要给wait传互斥锁,调用wait的线程被阻塞,直到被notified,wait的作用是使进程间做到同步。在wait阻塞进程时,当前进程会先把锁解掉,允许在这个锁上阻塞的线程继续走。当这个进程被唤醒后,这个进程会解阻塞,并获取到这个锁。
notify_one会唤醒在这个条件变量上等待的一个线程,如果没有现成在上面等待,什么都不做,如果有多个线程在等待,会选择其中任意一个。
下面是两个线程交替打印的代码:
{std::mutex mtx;std::condition_variable c;int n = 100;bool flag = true;std::thread t1([&]() {int i = 0;while (i < n){std::unique_lock<std::mutex> lock(mtx);//flag=false t1就阻塞//flag=true t1就不会阻塞while (!flag){c.wait(lock);}std::cout << i << std::endl;flag = false;i += 2; // 偶数c.notify_one();}});std::thread t2([&]() {int j = 1;while (j < n){std::unique_lock<std::mutex> lock(mtx);//只要flag==true,t2就阻塞//只要flag==false,t2就不会阻塞while (flag)c.wait(lock);std::cout << j << std::endl;j += 2; // 奇数flag = true;c.notify_one();}});t1.join();t2.join();return 0;
}