【C++杂货铺】C++11新特性
秋刀鱼的滋味@ 2024-08-04 15:35:01 阅读 60
目录
🌈 前言🌈
📁 C++11介绍
📁 统一初始化列表
📁 声明
📂 auto
📂 decltype
📂 返回类型后置
📂 范围for
📂 模板别名
📂 nullptr
📁 智能指针
📁 右值引用
📂左值,右值的概念
📂 左值引用和右值引用的比较
📂 右值引用的使用场景
📂 万能引用和完美转发
📁 新的类功能
📂 强制生成默认函数的关键字default
📂 禁止生成默认函数的关键字delete
📂 final和override关键字
📁 可变模板参数
📂 STL容器emplace相关接口函数
📁 lambda表达式
📂 lambda表达式语法
📂 函数对象和lambda表达式
📁 包装器
📂 function包装器
📂 bind包装器
📁 线程库
📂 thread 类介绍
📂 线程函数参数
📂 原子性操作库
📂 互斥锁
📂 条件变量condition_variable
📁 总结
🌈 前言🌈
本期【C++杂货铺】介绍的是C++11新特性,其中如目录所示的所有内容,包含了概念定义,如何使用等方面,快速入手C++11带来的新内容。
此外,像一些比较重要的内容,受制于文章篇幅限制,不能完全介绍,需要搭配其他文章进行学习,例如智能指针,线程库等,本文只能介绍使用,不详细介绍底层,其他日常频繁使用,较为重要,简单易懂,能快速掌握的本文将会详细介绍。
C++11作为C++最常使用的标准,掌握C++11是非常有必要的。
本文参考书籍:《C++ Primer Plus》(第六版 ) 中文版
📁 C++11介绍
在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于C++03(TC1)主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。从C++0x到C++11,C++标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言, C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个重点去学习。C++11增加的语法特性非常篇幅非常多,我们这里没办法一 一讲解,所以本节课程主要讲解实际中比较实用的语法。
📁 统一初始化列表
在C++98中,{}可以用来对数组或结构体进行统一的列表初始值设定。如:
{
int _x;
int _y;
};
int main()
{
int arr1[5] = {1,2,3};
int arr2[] = { 0 };
Point p = {1,2};
return 0;
}
C++11扩大了{}的使用范围,使其可用于所有的内置类型和用户自定义的类型。使用列表初始化时,可以添加 = ,也可以不添加。
class A
{
public:
A(int x,int y)
:_x(x)
,_y(y)
{}
private:
int _x;
int _y;
};
int main()
{
int i = { 1 };
double d{ 1.1 };
A a = { 1,2 };
A b{ 2,3 };
return 0;
}
此外,列表初始化也可以用于new 表达式中:
int* arr1 = new int[5]{ 1 }; // 1 0 0 0 0
int* arr2 = new int[5] { 1,1,1,1,1 }; //1 1 1 1 1
1. 缩窄
初始化列表可以防止缩窄,即禁止将数值赋值给无法存储它的数值变量。但是允许转为更宽的类型。
char c = {1.1} not allwed
double d = { 1 } allowed
2. std::initializer_list
C++11提供了对模板类initializer_list的支持。这个类包含成员函数begin()和end(),可用于获悉列表的范围。
可以用于构造函数,也可以用于operator=进行赋值
map<int, string> m({ { 1,"a" }, { 2,"b" } });
vector<int> v = { 1,2,3 };
v = { 3,2,1 };
还可以将initializer_list用于常规函数的参数:
void func(const initializer_list<int>& list)
{
for (auto i = list.begin(); i != list.end(); ++i)
{
cout << *i << endl;
}
}
int main()
{
func({ 1,2,3,4,5 });
return 0;
}
📁 声明
📂 auto
auto 是一个存储类型说明符,C++11将其用于实现自动类型推断。这就要求显示实例化,让编译器能够将变量的类型设置为初始值的类型。
int main()
{
int a = 10;
auto ca = a;
auto pa = &a;
return 0;
}
此外,auto还可以用于简化模板声明
int main()
{
set<int> s = { 1,2,3,4,5 };
for (auto it = s.begin(); it != s.end(); ++it)
{
cout << *it << endl;
}
return 0;
}
此外,auto还可以配合引用
int main()
{
int a = 10;
auto& ra = a;
auto& rra = ra;
//ra rra 都是a的别名
auto b = ra;
//b是一个新的int变量,用ra的值进行初始化
}
📂 decltype
decltype将变量的类型声明为表达式指定的类型。
int main()
{
//y -> int
decltype(1 * 10) y;
cout << typeid(y).name() << endl;
//pa -> int*
int a = 10;
decltype(&a) pa;
cout << typeid(pa).name() << endl;
}
这在定义模板时特别有用,因为只有等模板被实例化时才能确定具体的类型:
template<class T1, class T2>
void func(T1 t1, T2 t2)
{
decltype(t1 * t2) t;
cout << typeid(t).name() << endl;
}
int main()
{
func(10, 20);
return 0;
}
decltype的工作原理比auto更复杂,根据使用的表达式,指定的类型可以为引用和const
int main()
{
//func(10, 20);
int n = 10;
int& rn = n;
const int& cra = n;
decltype(n) n1;//int
decltype((n)) n2 = n;//int&
decltype(rn) n3 = n;//int&
decltype(cra) n4 = n;//const int&
n = 20;
return 0;
}
📂 返回类型后置
在函数名和参数列表后面指定返回类型
auto f(int x) -> int
就可读性而言,这个语法好像是倒退了,但是您能够使用decltype来指定模板函数的返回类型。
template<class T1,class T2>
auto f(T1 t1, T2 t2) -> decltype(t1* t2)
{
return t1 * t2;
}
int main()
{
cout << f(3, 2) << endl;
return 0;
}
📂 范围for
对于一个有范围的集合而言,程序员来说明循环的范围是多余的,有事还容易犯错误,因此C++11引入了基于范围的for循环。
for循环的括号由 冒号 : 分为两部分,第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
int arr[] = {1,2,3};
for(auto e : arr)
{
cout << it << endl;
}
除此之外,范围for和普通for循环没有任何区别,都可以在内内部进行break 和 continue。
范围for的使用条件:
1. for循环迭代的范围必须是确定的。对于数组而言,就是数组第一个元素和最后一个元素的范围。对于类而言,应该提供begin和end方法,begin和end就是for循环迭代的范围。
2. 迭代对象要实现++和==等操作。
📂 模板别名
对于冗长且复杂的标识符,如果能创建其别名就很方便,为此,C++提供了typedef。
typedef std::vector<int>::iterator itType
C++11提供了另一种用于模板部分具体化,但是typedef不能
template<class T>
using arr12 = std::array<T,12>;
std::array<double,12> a1
|
v
arr12<double> a2;
📂 nullptr
空指针是不会指向有效数据的指针。以前,C++在源代码中使用0表示这种指针,但内部表示可能不同。这带来一些问题,因为这使得0既能表示指针常量,也能表示整形常量。所以C++11引入新的关键字nullptr,用于表示空指针。nullptr是指针类型,不能转化为整形常量。但为了向后兼容,C++11仍允许0表示空指针,因此表达式0 == nullptr 为true,但使用nullptr更为安全。
📁 智能指针
这里我们智能指针只做出大概讲解,在《C++杂货铺》专栏中,会出一篇关于智能指针的文章,详细介绍概念,使用以及底层原理等。
如果程序中使用new从堆分配内存,等到不需要时,应使用delete将其释放。C++引入了智能指针auto_ptr,来帮助自动完成这个过程。随后的编程(尤其是STL时)表明,需要更精致的机制。基于程序员的编程体验和BOOST库提供的解决方案,C++11摒弃了auto_ptr,并新增了三种智能指针:unique_ptr,shared_ptr 和 weak_ptr。
所有新增的智能指针都能与STL容器和移动语义协同工作。
📁 右值引用
📂左值,右值的概念
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现在赋值符号的左边,右值不能出现在赋值符号的左边。但是const 修饰的左值不能赋值,但也可以取地址。总体来说,能取到地址的就是左值,不能取到地址的就是右值
左值/右值引用,都是给对象取别名。
传统C++引用(现称为左值引用)使得标识符关联到左值。左值是一个表示数据的表达式,程序可以获取到地址。
C++11新增了右值引用,使用&&表示。右值引用可以关联到右值,即出现在赋值表达式右边,但不能取地址。右值包含字面常量,临时对象,匿名对象,函数返回值,x+y表达式等。
int x = 10;
int y = 20;
int&& r1 = 13;
int&& r2 = x + y;
int&& r3 = std::sort(1);
r2关联的是当时计算x+y得到的结果,也就是r2关联的是33,即使后来修改了x或y,也不会影响r2。
需要注意的是右值不能取地址,但是给右值取别名后,会导致右值被存储到特定的位置,且可以获得该位置的地址。(即右值引用可以取到地址)也就是说不能将&运算符用于13,但是可以将其存放到右值引用中,通过数据与特定的地址关联,使得可以通过右值访问该数据。但是也可以使用const去引用
但是右值引用的使用场景并不在此,这个也不重要。
📂 左值引用和右值引用的比较
左值引用:
1. 左值引用只能引用左值,不能引用右值。
2. const 左值引用 既可以引用左值,也可以引用右值。
int a = 10;
int& ra1 = a;
const int& ra2 = 10;
const int& ra3 = a;
右值引用:
1. 右值引用只能右值,不能引用左值。
2. 但是右值引用可以引用move以后的左值。
int&& ra = 10;
int a = 10;
int&& ra1 = a; // error
int&& ra2 = std::move(ra);
📂 右值引用的使用场景
左值引用既可以引用左值,又可以引用右值,那右值引用有什么作用呢?我们先来看看左值引用的短板。
左值引用可以作为参数和函数返回值,可以提高效率。但是当函数的返回对象是一个局部变量是,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。传值返回会导致知道1次的拷贝构造(旧的编译器可能会造成两次拷贝构造)
右值引用和移动语义解决了上述问题,C++11中引入了移动语义(移动赋值,移动构造)的概念,移动构造的本质就是将参数右值的资源窃取过来,占为己有,那么就不用深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。
移动构造没有开辟新的空间,拷贝数据,所以效率就提高了。
不仅有移动构造,还有移动赋值
<code>// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
int main()
{
bit::string ret1;
ret1 = bit::to_string(1234);
return 0;
}
// 运行结果:
// string(string&& s) -- 移动语义
// string& operator=(string&& s) -- 移动语义
有些场景下,可能需要右值引用去引用左值来实现移动语义。当需要右值yinyong引用一个左值时可以通过move函数将左值转化为右值。在C++11中,std::move()位于头文件,它并不搬运任何东西,唯一的功能就是将一个左值强制转换为右值引用,然后实现移动语义。
📂 万能引用和完美转发
模板中&&不代表右值引用,而是万能引用,既可以接受左值,也能接受右值。模板的万能引用只是提供了能同时接受左值和右值的能力。
但是引用的类型的唯一作用就是限制了接受的类型,后续使用中都会退化为左值。这里需要重点理解,右值引用本身就是一个左值,可以被赋值,可以取到地址。但如果右值引用作为参数传递,编译器会认为这是一个左值,右值引用和左值引用同时存在,匹配左值。
因此,如果我们希望能在传递过程中保持它的左值或者右值属性,就需要我们学习完美转发
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)
{
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;
}
如果,右值引用再去调用函数,形参都是左值接受。
void Func(int& x)
{
cout << "func:左值引用" << endl;
}
void Func(const int& x)
{
cout << "func:const 左值引用" << endl;
}
void Func(int&& x)
{
cout << "func:右值引用" << endl;
}
void Func(const int&& x)
{
cout << "func:const 右值引用" << endl;
}
void Fun(int& x)
{
cout << endl;
Func(x);
cout << endl;
cout << "左值引用" << endl;
}
void Fun(const int& x)
{
cout << endl;
Func(x);
cout << endl;
cout << "const 左值引用" << endl;
}
void Fun(int&& x)
{
cout << endl;
Func(x);
cout << endl;
cout << "右值引用" << endl;
}
void Fun(const int&& x)
{
cout << endl;
Func(x);
cout << endl;
cout << "const 右值引用" << endl;
}
// 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
// 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
// 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
// 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
cout << endl;
int a;
PerfectForward(a); // 左值
cout << endl;
PerfectForward(std::move(a)); // 右值
cout << endl;
const int b = 8;
PerfectForward(b); // const 左值
cout << endl;
PerfectForward(std::move(b)); // const 右值
cout << endl;
return 0;
}
std::forward完美转发在传参过程中保留对象原生类型属性
<code>template<typename T>
void PerfectForward(T&& t)
{
Fun(std::forward<T>(t));
}
📁 新的类功能
📂 强制生成默认函数的关键字default
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原 因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以 使用default关键字显示指定移动构造生成。
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p)
:_name(p._name)
, _age(p._age)
{}
Person(Person && p) = default;
private:
string _name;
int _age;
};
📂 禁止生成默认函数的关键字delete
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p) = delete;
private:
bit::string _name;
int _age;
};
📂 final和override关键字
final : 修饰虚函数,表示该虚函数不能被重写。
override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
这两个关键字在下面这篇文章详细介绍了。
【C++杂货铺】多态-CSDN博客
📁 可变模板参数
C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比 C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改 进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。
们掌握一些基础的可变参数模板特性就够我们用了。
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数 包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的, 只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特 点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变 参数,所以我们的用一些奇招来一一获取参数包的值。
1. 递归方式展开参数包
// 递归终止函数
template <class T>
void ShowList(const T& t)
{
cout << t << endl;
}
// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args)
{
cout << value <<" ";
ShowList(args...);
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
2. 逗号表达式展开参数包
我们知道逗号表达式会按顺序执行逗号前面的表达式。最终会创建一个元素值都为0的数组int arr[sizeof..(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args) 打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在 数组构造的过程展开参数包。
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
📂 STL容器emplace相关接口函数
template <class... Args>
void emplace_back (Args&&... args);
首先我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用。那么相对insert和 emplace系列接口的优势到底在哪里呢?
在使用方面,和push没有什么区别:
int main()
{
std::list< std::pair<int, char> > mylist;
// emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
// 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别
mylist.emplace_back(10, 'a');
mylist.emplace_back(20, 'b');
mylist.emplace_back(make_pair(30, 'c'));
mylist.push_back(make_pair(40, 'd'));
mylist.push_back({ 50, 'e' });
for (auto e : mylist)
cout << e.first << ":" << e.second << endl;
return 0;
}
但是,emplace系列接口,支持传递参数:
int main()
{
// 下面我们试一下带有拷贝构造和移动构造的bit::string,再试试呢
// 我们会发现其实差别也不到,emplace_back是直接构造了,push_back
// 是先构造,再移动构造,其实也还好。
std::list< std::pair<int, bit::string> > mylist;
mylist.emplace_back(10, "sort");
mylist.emplace_back(make_pair(20, "sort"));
mylist.push_back(make_pair(30, "sort"));
mylist.push_back({ 40, "sort"});
return 0;
}
📁 lambda表达式
我们有一个类,想要进行排序,需要为此写一个仿函数,但是如果这个类包含了很多属性,就需要书写很多仿函数,仿函数写起来也是比较麻烦的。因此C++11引入了lambda表达式。lambda表达式的实际就是一个匿名函数。
struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
3 }, { "菠萝", 1.5, 4 } };
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; });
}
📂 lambda表达式语法
[capture-list] (parameters) mutable -> return-type { statement }
[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来 判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda 函数使用。
(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以 连同()一起省略
mutable:默认情况下,lambda函数总是一个const函数(形参不能被修改),mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回 值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获 到的变量。
注意:参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
捕获列表说明
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
[var]:表示值传递方式捕捉变量var
[=]:表示值传递方式捕获所有父作用域中的变量(包括this) [&var]:表示引用传递捕捉变量var
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
[this]:表示值传递方式捕捉当前的this指针
注意:
1. 父作用域包含lambda函数的语句块
2. 语法上捕捉列表可以由多个捕捉项组成,并以逗号分割。
[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
3. 捕捉列表不允许变量重复捕捉
比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
4. 在块作用域以外的lambda函数捕捉列表必须为空。
5. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译错误。
6. lambda表达式之间不能相互赋值,即使看起来类型相同
void (*PF)();
int main()
{
auto f1 = []{cout << "hello world" << endl; };
auto f2 = []{cout << "hello world" << endl; };
// 此处先不解释原因,等lambda表达式底层实现原理看完后,大家就清楚了
//f1 = f2; // 编译失败--->提示找不到operator=()
// 允许使用一个lambda表达式拷贝构造一个新的副本
auto f3(f2);
f3();
// 可以将lambda表达式赋值给相同类型的函数指针
PF = f2;
PF();
return 0;
}
📂 函数对象和lambda表达式
函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了operator()运算符的类对象。从使用方式上来看,函数对象与lambda表达式完全一样。
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);
// lamber
auto r2 = [=](double monty, int year)->double {return monty * rate * year;
};
r2(10000, 2);
return 0;
}
函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到。
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。
📁 包装器
📂 function包装器
function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。那么我们来看看,我们为什么需要function呢?
<code>//上面func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能
//是lamber表达式对象?所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下!
//为什么呢?我们继续往下看
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 T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
Ret: 被调用函数的返回类型
Args…:被调用函数的形参
// 使用方法如下:
#include <functional>
int f(int a, int b)
{
return a + b;
}
struct Functor
{
public:
int operator() (int a, int b)
{
return a + b;
}
};
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
// 函数名(函数指针)
std::function<int(int, int)> func1 = f;
cout << func1(1, 2) << endl;
// 函数对象
std::function<int(int, int)> func2 = Functor();
cout << func2(1, 2) << endl;
// lamber表达式
std::function<int(int, int)> func3 = [](const int a, const int b)
{return a + b; };
cout << func3(1, 2) << endl;
// 类的成员函数
std::function<int(int, int)> func4 = &Plus::plusi;
cout << func4(1, 2) << endl;
std::function<double(Plus, double, double)> func5 = &Plus::plusd;
cout << func5(Plus(), 1.1, 2.2) << endl;
return 0;
}
function就类似于武林盟主,统一了可执行对象的类型,可以接受函数指针,仿函数,lambda表达式,只需要给出返回值,形参即可,统一标识为function包装器,方便统一进行操作。
📂 bind包装器
std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(通常M<N,M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作
// 原型如下:
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
可以将bind函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
int Sub(int a, int b)
{
return a - b;
}
int main()
{
auto f1 = Sub;
cout << f1(10, 5) << endl;
auto f2 = bind(Sub, placeholders::_2, placeholders::_1);
cout << f2(10, 5) << endl;
return 0;
}
bind包装器返回的是一个function适配器,如果想要将形参个数不一致的函数统一进行处理,利用bind将参数个数调整为一致的。
// 使用举例
#include <functional>
int Plus(int a, int b)
{
return a + b;
}
class Sub
{
public:
int sub(int a, int b)
{
return a - b;
}
};
int main()
{
//表示绑定函数plus 参数分别由调用 func1 的第一,二个参数指定
std::function<int(int, int)> func1 = std::bind(Plus, placeholders::_1,
placeholders::_2);
//auto func1 = std::bind(Plus, placeholders::_1, placeholders::_2);
//func2的类型为 function<void(int, int, int)> 与func1类型一样
//表示绑定函数 plus 的第一,二为: 1, 2
auto func2 = std::bind(Plus, 1, 2);
cout << func1(1, 2) << endl;
cout << func2() << endl;
Sub s;
// 绑定成员函数
std::function<int(int, int)> func3 = std::bind(&Sub::sub, s,
placeholders::_1, placeholders::_2);
// 参数调换顺序
std::function<int(int, int)> func4 = std::bind(&Sub::sub, s,
placeholders::_2, placeholders::_1);
cout << func3(1, 2) << endl;
cout << func4(1, 2) << endl;
return 0;
}
📁 线程库
📂 thread 类介绍
在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread >头文件。
http://www.cplusplus.com/reference/thread/thread/?kw=thread
<code>thread()
构造一个线程对象,没有关联任何线程函数,即没有启动任何线程
thread(fn,args1, args2,...)
构造一个线程对象,并关联线程函数fn,args1,args2,...为线程函数参数
get_id()
获取线程id
jionable()
线程是否还在执行,joinable代表的是一个正在执行中的线程。
jion()
该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行
detach()
在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离
的线程变为后台线程,创建的线程的"死活"就与主线程无关
void func()
{
cout << "hello world" << endl;
}
int main()
{
thread t1(func);
cout << t1.get_id() <<endl;
cout << this_thread::get_id() <<endl;// get_id 还存放在this_thread 类想要调用主线程的tid,就要使用this_thread里的
t1.join();//线程必须等待
return 0;
}
线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态。
当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程
get_id()的返回值类型为id类型,id类型实际为std::thread命名空间下封装的一个类,该类中包含了一个结构体:
// vs下查看
typedef struct
{ /* thread identifier for Win32 */
void *_Hnd; /* Win32 HANDLE */
unsigned int _Id;
} _Thrd_imp_t
当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。
线程函数一般情况下可按照以下三种方式提供:
1. 函数指针
2. lambda表达式
3. 函数对象(仿函数)
thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个
线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行。
可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效
采用无参构造函数构造的线程对象
线程对象的状态已经转移给其他线程对象
线程已经调用jion或者detach结束
📂 线程函数参数
线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。
#include <thread>
void ThreadFunc1(int& x)
{
x += 10;
}
void ThreadFunc2(int* x)
{
*x += 10;
}
int main()
{
int a = 10;
// 在线程函数中对a修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,但其实际
//引用的是线程栈中的拷贝
thread t1(ThreadFunc1, a);
t1.join();
cout << a << endl;
// 如果想要通过形参改变外部实参时,必须借助std::ref()函数
thread t2(ThreadFunc1, std::ref(a);
t2.join();
cout << a << endl;
// 地址的拷贝
thread t3(ThreadFunc2, &a);
t3.join();
cout << a << endl;
return 0;
}
注意:如果是类成员函数作为线程参数时,必须将this作为线程函数参数。
📂 原子性操作库
多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。比如:
#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);
t1.join();
t2.join();
cout << "After joining,sum = " << sum << std::endl;
return 0;
}
传统的解决方式:可以对共享修改的数据可以加锁保护。虽然加锁可以解决,但是加锁有一个缺陷就是:只要一个线程在对sum++时,其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁。
因此C++11中引入了原子操作。所谓原子操作:即不可被中断的一个或一系列操作,C++11引入的原子操作类型,使得线程间数据的同步变得非常高效。
在C++11中,程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的访问。更为普遍的,程序员可以使用atomic类模板,定义出需要的任意原子类型。
<code>atomic<T> t; // 声明一个类型为T的原子类型变量t
📂 互斥锁
在多线程环境下,如果想要保证某个变量的安全性,只要将其设置成对应的原子类型即可,即高效又不容易出现死锁问题。但是有些情况下,我们可能需要保证一段代码的安全性,那么就只能通过锁的方式来进行控制。
锁控制不好时,可能会造成死锁,最常见的比如在锁中间代码返回,或者在锁的范围内抛异常。因此:C++11采用RAII的方式对锁进行了封装,即lock_guard和unique_lock。
lock_guard和unique_lock 封装了mutex,实例化时加锁,出作用域销毁时,释放锁,即调用构造函数成功上锁,调用析构函数自动解锁,可以有效避免死锁问题。
unique_lock 和 lock_guard 的唯一区别是,unique_lock 可以手动操作。
锁mutex的类型:
1. std::mutex
C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动。mutex最常用的三个函数
lock() | 上锁:锁住互斥量 |
unlock() | 解锁:释放对互斥量的所有权 |
try_lock() | 尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞 |
2. std::recursive_mutex
其允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),除此之外, std::recursive_mutex 的特性和 std::mutex 大致相同。
3. std::timed_mutex
比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until()
try_lock_for():
接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
try_lock_until():
接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
4. std::recursive_timed_mutex
📂 条件变量condition_variable
condition_variable - C++ Reference (cplusplus.com)
<code>//wait:
void wait (unique_lock<mutex>& lck);
template <class Predicate>
void wait (unique_lock<mutex>& lck, Predicate pred);
// 第二个参数为 函数对象,返回false 阻塞 释放锁 ; true 不阻塞,往下执行
📁 总结
以上就是本期【C++杂货铺】的主要内容了,包含了C++11带来的最主要,常用的新特性,讲解了基本的使用方法,以及概念等内容。
如果感觉本期内容对你有帮助,欢迎点赞,收藏,关注Thanks♪(・ω・)ノ
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。