C++11详解

x33g5p2x  于2022-05-27 转载在 其他  
字(26.7k)|赞(0)|评价(0)|浏览(669)

C++11

C++11简介

在2003年C标准委员会曾经提交了一份技术勘误表(简称TC1),使得C03这个名字已经取代了C98称为C11之前的最新C标准名称。不过由于TC1主要是对C98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C98/03标准。从C0x到C11,C标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比C98/03,C11则带来了数量可观的变化,其中包含了约140个新特性,以及对C03标准中约600个缺陷的修正,这使得C11更像是从C98/03中孕育出的一种新语言。相比较而言,C11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。

列表初始化

C++98中{}的初始化问题

在C++98中,标准允许使用花括号{}对数组元素进行统一的列表初始值设定。比如:

  1. int a[] = {1,2,3,4};
  2. int b[5] = {0};

对于一些自定义的类型,却无法使用这样的初始化。比如:

  1. vector<int> v{1,2,3,4,5};

这样在C98中无法通过编译,导致每次定义vector时,都需要先把vector定义出来,然后使用循环对其赋初始值,非常不方便。C11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。

C++11内置类型的列表初始化

  1. int main()
  2. {
  3. // 内置类型变量
  4. int x1 = {10};
  5. int x2{10};
  6. int x3 = 1+2;
  7. int x4 = {1+2};
  8. int x5{1+2};
  9. // 数组
  10. int a[5] {1,2,3,4,5};
  11. int b[]{1,2,3,4,5};
  12. // 动态数组,在C++98中不支持
  13. int* p1 = new int[5]{1,2,3,4,5};
  14. // 标准容器
  15. vector<int> v{1,2,3,4,5};
  16. map<int, int> m{{1,1}, {2,2,},{3,3},{4,4}};
  17. return 0;
  18. }

总结:

C++11里面扩展了{}初始化使用,基本都可以使用他来初始化,建议还是按旧的用法使用,一般new[]建议这样来初始化

自定义类型的列表初始化

  1. 标准库支持单个对象的列表初始化
  1. class A
  2. {
  3. public:
  4. A(int x = 0, int y = 0): _x(x), _y(y)
  5. {}
  6. private:
  7. int _x;
  8. int _y;
  9. };
  10. int main()
  11. {
  12. A a{1,2};
  13. return 0;
  14. }

自定义类型对象可以使用{}初始化,必须要有对应参数类型和个数的构造函数,因为用{}初始化,会调用对应的构造函数。

STL当中的列表初始化

多个对象想要支持列表初始化,需给该类(模板类)添加一个带有initializer_list类型参数的构造函数即
可。注意:initializer_list是系统自定义的类模板,该类模板中主要有三个方法:begin()、end()迭代器
以及获取区间中元素个数的方法size()。

  1. #include<map>
  2. #include<vector>
  3. #include<list>
  4. int main()
  5. {
  6. vector<int> v1 = {1,2,3,4,5};
  7. vector<int> v1{1,2,3,4,5};//可以省略=号
  8. auto lt1 = {1,2,3,4};//lt1的类型为initializer_list<int>
  9. //所以使用{1,2,3,4}初始化本质上调用了initializer_list作为参数的构造函数
  10. initializer_list<int> lt1 = {1,2,3,4};
  11. map<string,int> dict = {pair<string,int>("sort",1),pair<string,int>("sort",1)};
  12. map<string,int> dict = {{"sort",1},{"sort",1}};
  13. return 0;
  14. }

C++11用{}初始化好像是万能的,一个自定义类型调用{}初始化,本质是调用对应的构造函数

模拟实现vector当中的initializer_list作为参数的构造函数:

  1. vector(initializer_list<T> lt)
  2. :_start(nullptr)
  3. ,_finish(nullptr)
  4. ,_endofstorage(nullptr)
  5. {
  6. typename initializer_list<T>::iterator it = lt.begin();
  7. auto it = it.begin();
  8. while(it != lt.end())
  9. {
  10. push_back(*it);
  11. ++it;
  12. }
  13. }

本质上下面这种初始化是调用了一个initializer_list作为参数的构造函数来初始化:

  1. vector<int> v1 = {1,2,3,4,5};

总结:

  • 自定义类型对象可以使用{}初始化,必须要有对应参数类型和个数的构造函数,因为用{}初始化,会调用对应的构造函数
  • STL容器支持{}初始化,容器里面有支持一个initializer_list作为参数的构造函数

变量类型推导

在定义变量时,必须先给出变量的实际类型,编译器才允许定义,但有些情况下可能不知道需要实际类型怎么给,或者类型写起来特别复杂

auto

  1. int main()
  2. {
  3. int i = 0;
  4. auto p = &i;//只能用来推导类型
  5. auto pf = strcpy;
  6. cout<<typeid(p).name()<<typeid(pf).name()<<endl;
  7. return 0;
  8. }

auto自动推导对象类型:

decltype

decltype是根据表达式的实际类型推演出定义变量时所用的类型

decltype和auto的区别:

  1. int main()
  2. {
  3. int i = 0;
  4. auto p = &i;//只能用来推导类型
  5. auto pf = strcpy;
  6. decltype(pf) pf1;//可以作为推导类型来创建对象或者变量
  7. cout<<typeid(pf).name()<<endl<<typeid(pf1).name()<<endl;
  8. return 0;
  9. }

可以作为推导类型来创建对象或者变量

STL当中的变化

array

固定大小的数组容器

下面这两个有什么区别呢?

  1. #include<array>
  2. int main()
  3. {
  4. array<int,10> a1;
  5. int a2[10];
  6. return 0;
  7. }

这两基本没有什么区别,唯一最大的区别是:

  1. a1[13] = 1;
  2. a2[13] = 1;

a1只要越界一定报错,而a2只要越界不一定报错,[]对越界检查使用了assert,更严格安全,还有就是a1是有迭代器的,C++11增加这个感觉没什么用

forward_list

单链表,这个容器里面没有尾插尾删,因为尾插尾删效率低,并且实现了insert_after,C++11增加这个属实没感觉到有什么用

unordered_map和unordered_set

对于unordered_map和unordered_set的添加还是让人高兴的,它们的底层使用哈希表实现的,它们的介绍请前往博主的C++专栏阅读

针对旧容器,基本都增加了移动构造,移动赋值,所有插入数据接口函数,都增加右值引用版本这些接口都是用来提高效率,下面我们来看一下什么是右值引用:

右值引用和移动语义

左值和右值

在说右值引用之前,我们得先谈谈什么是左值,什么是右值,可能大家理解的左值和右值的概念是赋值号左边的就是左值,赋值号右边的就是右值,其实不然,左值也可能出现在右边,比如:

  1. int main()
  2. {
  3. int a = 10;
  4. a = 20;
  5. int b = 10;
  6. b = a;
  7. }

这里的a是左值,b也是左值,b = a,发现左值也可以出现在赋值号右边,所以这些说法是不正确的,正确的说法应该是:左值可以获取地址,而右值不能获取地址。

左值都是可以获取地址,左值基本都可以出现在赋值符号的左边,可以修改,但是const修饰的左值,只能获取地址,不能赋值,右值不能出现赋值符号的左边,也就是不能修改右值,不能取地址

左值引用是给左值取别名,左值引用不能引用右值,const左值引用可以引用左值,也可以引用右值:

  1. int main()
  2. {
  3. int a = 10;
  4. int& rt = a;
  5. //int& rt = 10;//error,左值引用不能引用右值
  6. const int& rt = 10;//const左值引用可以引用右值
  7. return 0;
  8. }

当形参是const修饰的左值引用时,实参既可以是左值也可以是右值:

  1. void push_back(const T& x)
  2. {}

右值匹配了右值引用,左值匹配了左值引用,需要注意的是,我们说当形参是const修饰的左值引用时,实参既可以是左值也可以是右值,有人说那这里为什么f(1)不匹配第一个函数呢?是因为编译器会去找最匹配的。

右值引用只能引用右值不能引用左值:

  1. int a = 10;
  2. int&& r2 = a;//error

右值引用可以引用move以后的左值:

  1. int&& r3 = std::move(a);

右值引用作为形参时,实参只能传右值:

  1. void push_back(T&& x)
  2. {}

右值引用不可以连续引用:

  1. int&& r1 = 10;
  2. int&& r4 = r1;//error,右值引用只能引用右值,不能引用左值

右值引用可以引用move后的左值:

  1. int&& r4 = std::move(r1);

因此关于左值与右值的区分不是很好区分,一般认为:

  1. 普通类型的变量,因为有名字,可以取地址,都认为是左值。
  2. const修饰的常量,不可修改,只读类型的,理论应该按照右值对待,但因为其可以取地址(如果只是const类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间),C++11认为其是左值。
  3. 如果表达式的运行结果是一个临时变量或者对象,认为是右值。
  4. 如果表达式运行结果或单个变量是一个引用则认为是左值。
  5. 右值引用可以引用move后的左值

总结:

  • 不能简单地通过能否放在=左侧右侧或者取地址来判断左值或者右值,要根据表达式结果或变量的性质判断
  • 能得到引用的表达式一定能够作为引用,否则就用常引用。

C++11对右值进行了严格的区分:

  • C语言中的纯右值,比如:a+b, 100
  • 将亡值。比如:表达式的中间结果、函数按照值的方式进行返回。
左值引用和右值引用

左值引用的使用场景

在传参时:
左值引用的场景,引用传参可以减少拷贝

在引用返回时:

左值引用,引用返回可以减少拷贝,但是效果不明显,不使用引用返回,编译器会优化一次拷贝,使用引用返回,编译器不会优化

本质上引用都是用来减少拷贝,提高效率

1、左值引用解决大部分的场景(做参数,做返回值)

2、右值引用是左值引用一些盲区的补充

比如我们写一个to_string函数来讲解(这是一个整形转字符串的函数):

  1. string to_string(int val)
  2. {
  3. string str;
  4. while(val)
  5. {
  6. int i = val % 10;//取到最低的十进制位
  7. str += ('0' + i);
  8. val /= 10;
  9. }
  10. reverse(str.begin(),str.end());//逆置
  11. return str;
  12. }
  13. int main()
  14. {
  15. string s = to string(1234);
  16. cout<<s.c_str()<<endl;
  17. return 0;
  18. }

此时的to_string函数只能值返回,如果是引用返回就会出问题,因为临时对象出了作用域就销毁了,相当于s成了野指针,我们只能传值返回,传值返回会有一次拷贝构造,本来是str去拷贝构造临时对象,然后临时对象再去拷贝构造s,但是编译器做了优化,直接用str去拷贝构造s,这个临时对象小的话放在寄存器,大的话放在调用它的函数的栈帧当中。还记不记得前面说的将亡值:函数按照值的方式进行返回时产生的临时对象就是将亡值

当这个变量或者对象出了作用域在,这种场景就可以用左值引用,在这里我们可以硬用左值引用返回,将里面的string对象写成静态的就可以引用返回了,因为这个变量出了作用域还在:

  1. string& to_string(int val)
  2. {
  3. //线程安全问题
  4. static string str;
  5. str.clear();
  6. while(val)
  7. {
  8. int i = val % 10;//取到最低的十进制位
  9. str += ('0' + i);
  10. val /= 10;
  11. }
  12. reverse(str.begin(),str.end());//逆置
  13. return str;
  14. }

但是这样写会有多线程安全问题,而且每次进来还需要将str清理一次,所以左值引用无法解决局部对象返回,只能传值返回

那么右值引用返回呢?

  1. string&& to_string(int val)
  2. {
  3. //线程安全问题
  4. string str;
  5. while(val)
  6. {
  7. int i = val % 10;//取到最低的十进制位
  8. str += ('0' + i);
  9. val /= 10;
  10. }
  11. reverse(str.begin(),str.end());//逆置
  12. return (move)str;
  13. }

右值引用并不会改变局部变量的生命周期,返回的也是str的别名,出了作用域str也就销毁了,所以几乎没有使用右值引用返回的场景。

总结:str在按照值返回时,必须创建一个临时对象,临时对象创建好之后,str就被销毁了,最后使用返回的临时对象构造s,s构造好之后,临时对象就被销毁了。仔细观察会发现:str、临时对象、s每个对象创建后,都有自己独立的空间,而空间中存放内容也都相同,相当于创建了三个内容完全相同的对象,对于空间是一种浪费,程序的效率也会降低,而且临时对象确实作用不是很大,那能否对该种情况进行优化呢?

下面就用到了右值引用的场景:

右值引用的场景

移动语义

C11提出了移动语义概念,即:将一个对象中资源移动到另一个对象中的方式,可以有效缓解在值传递时空间的浪费问题,在C11中如果需要实现移动语义,必须使用右值引用。string类的移动构造函数

  1. //移动构造
  2. String(String&& s)
  3. :_str(nullptr)
  4. , _size(0)
  5. , _capacity(0)
  6. {
  7. this->swap(s);
  8. }

我们自己模拟实现string,下面的场景就用到了右值引用:

  1. #define _CRT_SECURE_NO_WARNINGS 1
  2. #include<iostream>
  3. #include<assert.h>
  4. #include<algorithm>
  5. using namespace std;
  6. class String
  7. {
  8. public:
  9. typedef char* iterator;
  10. iterator begin()
  11. {
  12. return _str;
  13. }
  14. iterator end()
  15. {
  16. return _str + _size;
  17. }
  18. String(const char* str = "")
  19. :_size(strlen(str))
  20. , _capacity(_size)
  21. {
  22. //cout << "string(char* str)" << endl;
  23. _str = new char[_capacity + 1];
  24. strcpy(_str, str);
  25. }
  26. //拷贝构造
  27. String(const String& s)
  28. :_str(nullptr)
  29. , _size(0)
  30. , _capacity(0)
  31. {
  32. cout << "拷贝构造:String(const String& s)"<< endl;
  33. String tmp(s._str);
  34. swap(tmp);
  35. }
  36. //移动构造
  37. String(String&& s)
  38. :_str(nullptr)
  39. , _size(0)
  40. , _capacity(0)
  41. {
  42. cout << "移动构造:String(String && s)" << endl;
  43. this->swap(s);
  44. }
  45. // 拷贝赋值
  46. String& operator=(const String& s)
  47. {
  48. cout << "String& operator=(const string& s) -- 深拷贝" << endl;
  49. String tmp(s);
  50. swap(tmp);
  51. return *this;
  52. }
  53. // s1.swap(s2)
  54. void swap(String& s)
  55. {
  56. ::swap(_str, s._str);
  57. ::swap(_size, s._size);
  58. ::swap(_capacity, s._capacity);
  59. }
  60. void reserve(size_t n)
  61. {
  62. if (n > _capacity)
  63. {
  64. char* tmp = new char[n + 1];
  65. strcpy(tmp, _str);
  66. delete[] _str;
  67. _str = tmp;
  68. _capacity = n;
  69. }
  70. }
  71. void push_back(char ch)
  72. {
  73. if (_size >= _capacity)
  74. {
  75. size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
  76. reserve(newcapacity);
  77. }
  78. _str[_size] = ch;
  79. ++_size;
  80. _str[_size] = '\0';
  81. }
  82. String& operator+=(char ch)
  83. {
  84. push_back(ch);
  85. return *this;
  86. }
  87. String to_string(int val)
  88. {
  89. String str;
  90. while (val)
  91. {
  92. int i = val % 10;//取到最低的十进制位
  93. str += ('0' + i);
  94. val /= 10;
  95. }
  96. reverse(str.begin(), str.end());//逆置
  97. return str;
  98. }
  99. const char* c_str() const
  100. {
  101. return _str;
  102. }
  103. char& operator[](size_t pos)
  104. {
  105. assert(pos < _size);
  106. return _str[pos];
  107. }
  108. void clear()
  109. {
  110. _str[0] = '\0';
  111. _size = 0;
  112. }
  113. ~String()
  114. {
  115. if (_str) delete[] _str;
  116. }
  117. private:
  118. char* _str;
  119. size_t _size;
  120. size_t _capacity;
  121. };
  122. int main()
  123. {
  124. String s1("hello");
  125. String s2("world");
  126. //s1.to_string(22222)没有接收对象
  127. String s3 = s1.to_string(22222);//有接收对象
  128. return 0;
  129. }

当我们去调用to_string函数时,发现外面没有接收对象和有接收对象它都调用了一次移动构造,为什么是这样呢?

当外面有接收对象时:
因为str对象的生命周期在创建好临时对象后就结束了,即将亡值,编译器做了优化,用str去拷贝构造返回对象时,把str识别为右值,C++11认为其为右值,在用str构造临时对象时,就会采用移动构造,即将str中资源转移到临时对象中。而临时对象也是右值,因此在用临时对象构造s3时,也采用移动构造,将临时对象中资源转移到s3中,整个过程,只需要创建一块堆内存即可,既省了空间,又大大提高程序运行的效率。但是编译器做了优化,直接用str移动构造s。

总结:外面没有接收对象时,str去构造临时对象,str被编译器优化成了右值(如果没有优化,这里是拷贝构造),这里调用了移动构造。外面有接收对象时,str去构造临时对象,这里一次移动构造,然后临时对象再去构造那个接收的对象,又一次移动构造,两次移动构造,编译器做了优化,直接用str移动构造s

但是也有人会这样使用:

  1. int main()
  2. {
  3. String s1;
  4. String s;
  5. s = s1.to_string(12345678);
  6. cout << s.c_str() << endl;
  7. return 0;
  8. }

可以看到调用to_string打印了一次移动构造,而且还有一次赋值构造,后面那个拷贝构造不用管,是因为在赋值构造里面写了现代写法调用的。

当没有移动构造时:一次拷贝构造和一次赋值构造

当有移动构造时:一次移动构造和一次赋值构造

这样的开销还是很大的,所以还要移动赋值:

移动赋值
  1. //移动赋值
  2. string& operator=( string&& s)
  3. {
  4. cout << "string& operator=(string&& s) --移动拷贝" << endl;
  5. swap(s);
  6. return *this;
  7. }

可以看到调用了移动构造和移动赋值涉及深拷贝的类,除了实现拷贝构造和拷贝赋值还要实现移动构造和移动赋值,面对这个类传值返回的函数场景就能进一步减少拷贝,提高效率

总结:

  1. 左值引用通常在传参和传返回值的过程中减少拷贝,一般是利用左值引用的语法特性,别名的特性,减少拷贝
  2. 右值引用,一般是利用深拷贝的类,需要实现移动构造和移动赋值,利用移动构造和移动赋值在传参和传返回值过程中间接转移资源,减少拷贝

下面我们来看移动构造在C++中的一些改变:

  1. int main()
  2. {
  3. string s1("hello world");
  4. string s2("1111");
  5. //在C++98中第二个比第一个效率高
  6. swap(s1,s2);
  7. s1.swap(s2);
  8. //在C++11中效率没有差别
  9. swap(s1,s2);
  10. s1.swap(s2);
  11. return 0;
  12. }

在C98中第二个比第一个效率高,因为第一个调用三次拷贝构造来进行交换,二是第二个仅仅交换成员即可,我们可以看到C11中swap函数是调用了移动构造的,string的成员函数swap是调用移动赋值

在C++11中容器中push_back接口增加了右值引用作为参数的接口:

容器里面插入时使用右值引用也会减少拷贝

  1. int main()
  2. {
  3. vector<string> v;
  4. string s("11111");
  5. v.push_back(s);//左值 一次拷贝构造
  6. v.push_back("222222");//右值 两次移动
  7. v.push_back(move(s));//对一个左值进行move,它的资源可能会被转走
  8. return 0;
  9. }

完美转发

完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。

所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相
应实参是右值,它就应该被转发为右值。这样做是为了保留在其他函数针对转发而来的参数的左右值属性进
行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)。

  1. void Fun(int &x){cout << "lvalue ref" << endl;}
  2. void Fun(int &&x){cout << "rvalue ref" << endl;}
  3. void Fun(const int &x){cout << "const lvalue ref" << endl;}
  4. void Fun(const int &&x){cout << "const rvalue ref" << endl;}
  5. template<typename T>
  6. void PerfectForward(T &&t)//模板里面的T&& 做参数,不再局限是右值引用,叫做万能引用
  7. //既可以引用左值,也可以引用右值
  8. {
  9. Fun(t);//t会退化成左值
  10. Fun(std::forward<T>(t));//不想t被退化就加forward
  11. }
  12. int main()
  13. {
  14. PerfectForward(10); // rvalue ref
  15. int a;
  16. PerfectForward(a); // lvalue ref
  17. PerfectForward(std::move(a)); // rvalue ref
  18. const int b = 8;
  19. PerfectForward(b); // const lvalue ref
  20. PerfectForward(std::move(b)); // const rvalue ref
  21. return 0;
  22. }

模板里面的T&& 做参数,不再局限是右值引用,叫做万能引用,既可以引用左值,也可以引用右值,在模板里面调用函数时,参数t会退化成左值。

当我们不完美转发时:

可以看到不管传左值还是右值,调用的都是左值引用。

当我们完美转发时:

  1. Fun(std::forward<T>(t))

此时就传的是左值就调用的是左值引用参数的函数,传的是右值就调用的是右值引用参数的函数

默认成员函数

原来C++类中,有6个默认成员函数:

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝赋值重载
  5. 取地址重载
  6. const取地址重载
    最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。
    C++11新增了两个:移动构造函数和移动赋值运算符重载。

针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:

  • 如果你没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载都没有实现。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
  • 如果你没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载都没有实现,那么编译器会自动生成一个默认移动赋值。默认生成的移动赋值函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
  • 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

我们有一种方法可以强制默认生成,设置成default,尽管条件不满足仍然生成:

  1. Person(const Person& p) = default;
  2. Person(Person&& p) = default;

可变参数模板

printf函数当中就有可变参数:

  1. int main()
  2. {
  3. int a;
  4. int b;
  5. double d;
  6. cin>>a>>b>>d;
  7. printf("%d,%f\n",a,d);
  8. printf("%d,%d,%f\n",a,b,d);
  9. return 0;
  10. }

Args是一个模板参数包,args是一个函数形参参数包,声明一个参数包Args…args,这个参数包中可以包含0到任意个模板参数。

  1. template<class ...Args>
  2. void ShowList(Args... args)
  3. {}
  4. int main()
  5. {
  6. return 0;
  7. }
  1. //可以算出有多少个参数,但是不能通过下标取出每一个参数
  2. void ShowList(Args... args)
  3. {
  4. for(size_t i = 0;i<sizeof...(args);++i)
  5. {
  6. //cout<<args[i]<<" ";
  7. cout<<i<<" ";
  8. }
  9. cout<<endl;
  10. }
  11. int main()
  12. {
  13. ShowList(1);
  14. ShowList(1,'A');
  15. ShowList();
  16. ShowList(1,3.33,std::string("hello"));
  17. return 0;
  18. }

我们可以算出有多少个参数,但是不能通过下标取出每一个参数

有一种方式取出每一个参数:

  1. void ShowList()//递归终止函数
  2. {
  3. cout<<endl;
  4. }
  5. //可以通过递归调用来获取参数
  6. template<class T,class ...Args>
  7. void ShowList(T x,Args... args)
  8. {
  9. cout<< x <<endl;
  10. ShowList(args...);
  11. }
  12. int main()
  13. {
  14. ShowList(1);
  15. ShowList(1,'A');
  16. ShowList();
  17. ShowList(1,3.33,std::string("hello"));
  18. return 0;
  19. }

emplace_back和push_back的区别

emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象,那么在这里我们可以看到除了用法上,和push_back没什么太大的区别

  1. int main()
  2. {
  3. std::list< std::pair<int, char>> mylist;
  4. // emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
  5. // 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别
  6. mylist.emplace_back(10, 'a');
  7. mylist.emplace_back(20, 'b');
  8. for (auto e : mylist)
  9. cout << e.first << ":" << e.second << endl;
  10. return 0;
  11. }

我们试一下带有拷贝构造和移动构造的String,我们为了方便验证,我们用一下我们上面自己实现的string类:

1、传左值对比:

  1. int main()
  2. {
  3. std::list<std::pair<int,String>> mylist;
  4. // 1、传左值对比 -- 没有区别
  5. std::pair<int, bit::string> kv(1, "11111");
  6. mylist.push_back(kv);
  7. mylist.emplace_back(kv);
  8. return 0;
  9. }

我们发现传左值没有区别,都调用了构造和拷贝构造。

2、传右值对比:

如果push_back/emplace_back的参数对象及其成员都实现了移动构造

  1. int main()
  2. {
  3. // 下面我们试一下带有拷贝构造和移动构造的bit::string,再试试呢
  4. // 我们会发现其实差别也不到,emplace_back是直接构造了,push_back
  5. // 是先构造,再移动构造,其实也还好。
  6. std::list<std::pair<int,String>> mylist;
  7. // 2、传右值对比 -- push_back是构造+移动构造
  8. // emplace_back是直接构造
  9. // 形态有区别,如果push_back/emplace_back的参数对象及其成员都实现了移动构造,本质区别不大
  10. // 因为构造出来+移动构造,和直接构造成本差不多
  11. // 但是如果push_back/emplace_back的参数对象及其成员没有实现移动构造
  12. // 那么emplace_back还是直接构造,push_back则是构造+拷贝构造,代价就大了
  13. // 结论:稳妥一点呢用emplace_back更好,因为他可以不依赖参数对象是否提供移动构造
  14. mylist.push_back(make_pair(2, "sort"));
  15. mylist.push_back({ 40, "sort" });
  16. cout << endl;
  17. mylist.emplace_back(make_pair(2, "sort"));
  18. mylist.emplace_back(10, "sort");
  19. return 0;
  20. }

可以看到push_back调用了构造和移动构造,而emplace_back调用了构造,形态有区别,如果push_back/emplace_back的参数对象及其成员都实现了移动构造,本质区别不大,因为构造出来+移动构造,和直接构造成本差不多

那么当我们不写移动构造时:

但是如果push_back/emplace_back的参数对象及其成员没有实现移动构造,那么emplace_back还是直接构造,push_back则是构造+拷贝构造,代价就大了

我们看pair当中的移动构造和拷贝构造是强制生成的,也就是如果不写强制生成默认的:

pair中first和second如果是自定义类型成员,当pair调用拷贝构造时, 自定义类型成员first和second就需要调用它们的拷贝构造,pair调用移动构造,自定义类型成员first和second就需要调用它们的移动构造,没有移动构造就调用拷贝构造

所以在前面当没有实现移动构造时,push_back在插入pair时调用了自定义类型成员的构造和拷贝构造

结论
稳妥一点用emplace_back更好,因为它可以不依赖参数对象是否提供移动构造

lambda表达式

可调用类型概念:
类型定义的对象可以像函数一样去调用

可调用类型主要有:

  • 函数指针
  • 仿函数,仿函数就是一个类+重载operator() 相比函数指针好用多了
  • lambda表达式
  • 包装器

我们可以看到sort函数就有接口里面有参数仿函数:

假如我们想要实现对一个货物进行排序,它可以分别按照名字排序,也可以按照价格排序,可以按照评价排序:

  1. struct Goods
  2. {
  3. string _name; // 名字
  4. double _price; // 价格
  5. int _evaluate; // 评价
  6. };
  7. struct ComparePriceLess
  8. {
  9. bool operator()(const Goods& gl, const Goods& gr)
  10. {
  11. return gl._price < gr._price;
  12. }
  13. };
  14. struct ComparePriceGreater
  15. {
  16. bool operator()(const Goods& gl, const Goods& gr)
  17. {
  18. return gl._price > gr._price;
  19. }
  20. };
  21. struct CompareEvaluateGreater;
  22. struct CompareEvaluateLess;
  23. int main()
  24. {
  25. Goods gds[] = { { "苹果", 2.1, 5}, { "香蕉", 3, 4}, { "橙子", 2.2,3}, { "菠萝", 1.5, 4} };
  26. sort(gds, gds + sizeof(gds) / sizeof(gds[0]), ComparePriceLess());
  27. sort(gds, gds + sizeof(gds) / sizeof(gds[0]), ComparePriceGreater());
  28. // 增加一个比较方式,就要提供一个对应的仿函数
  29. return 0;
  30. }

按照ComparePriceLess排序:

按照ComparePriceGreater排序:

但是这样我们增加一个比较方式,就要提供一个对应的仿函数

lambda表达式可以解决:

首先我们来看一下什么是lambda表达式:

lambda表达式语法

lambda表达式书写格式:

  1. [capture-list] (parameters) mutable -> return-type { statement }
  • lambda表达式各部分说明
    [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来
    的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。

(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起
省略

mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修
饰符时,参数列表不可省略(即使参数为空)。

->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分
可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。

{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

  • 注意:
    在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{};该lambda函数不能做任何事情。
  • 捕获列表说明:

捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
[var]:表示值传递方式捕捉变量var
[=]:表示值传递方式捕获所有父作用域中的变量(包括this)
[&var]:表示引用传递捕捉变量var
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
[this]:表示值传递方式捕捉当前的this指针

  • 注意:
    a. 父作用域指包含lambda函数的语句块
    b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
    比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量 [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量 c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
    d. 在块作用域以外的lambda函数捕捉列表必须为空。
    e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
    f. lambda表达式之间不能相互赋值,即使看起来类型相同

实现一个比较整数小于的lambda,如何实现?

lambda表达式写起来和函数类似,有参数,返回值,函数体。多的就是还有捕捉列表

  1. auto lessFunc = [](const int a,const int b)->bool{return a<b;};
  2. cout<<lessFunc(1,2)<<endl;
  1. int main()
  2. {
  3. //实现一个比较整数小于的lambda,如何实现
  4. //lessFunc lambda对象可以像函数一样调用
  5. //最简单的lambda
  6. auto f1 = []{cout<<"hello world"<<endl;};
  7. f1();
  8. auto lessFunc = [](const int a,const int b)->bool{return a<b;};
  9. cout<<lessFunc(1,2)<<endl;
  10. return 0;
  11. }

下面探索捕捉列表是干嘛的,实现交换x和y的lambda表达式:

  1. 普通实现
  1. int main()
  2. {
  3. //lambda表达式写起来和函数类似,有参数,返回值,函数体。多的就是还有捕捉列表
  4. //下面探索捕捉列表是干嘛的
  5. //实现交换x和y的lambda表达式
  6. //1.普通实现
  7. int x = 1;
  8. int y = 2;
  9. auto swap1 = [](int& a, int& b)
  10. {
  11. int tmp = a;
  12. a = b;
  13. b = tmp;
  14. };
  15. swap1(x, y);
  16. return 0;
  17. }

  1. 要求不传参数,交换x和y
  1. int main()
  2. {
  3. //lambda表达式写起来和函数类似,有参数,返回值,函数体。多的就是还有捕捉列表
  4. int x = 1;
  5. int y = 2;
  6. //2.要求不传参数,交换x和y
  7. auto swap2 = [x,y]()
  8. {
  9. //这里面不能用外面的x,y
  10. //lambda想用外面函数中的对象,需要用到捕捉列表
  11. int tmp = x;
  12. x = y;//传值过来的不能修改,加mutable就好了
  13. y = tmp;
  14. };
  15. return 0;
  16. }

这里x和y是以传值方式捕捉,本质就是拷贝过来的,并且不能修改,传值过来的不能修改,加mutable就好了:

  1. auto swap2 = [x,y]()mutable

但是mutable的作用就是让传值捕捉的对象可以修改,但是你修改的是传值拷贝的对象,不影响外面的对象,实际中mutable意义不大,除非你就是想传值捕捉过来,lambda中修改不影响外面的值

所以需要传引用

  1. auto swap2 = [&x,&y]()mutable
  1. int main()
  2. {
  3. int x = 1;
  4. int y = 2;
  5. //传引用捕捉
  6. auto swap2 = [&x,&y]()
  7. {
  8. //这里面不能用外面的x,y
  9. //lambda想用外面函数中的对象,需要捕捉
  10. int tmp = x;
  11. x = y;//传值过来的不能修改,加mutable就好了
  12. y = tmp;
  13. };
  14. return 0;
  15. }

还可以引用捕捉全部的变量:

  1. int main()
  2. {
  3. int x = 1;
  4. int y = 2;
  5. //引用捕捉全部变量
  6. auto swap2 = [&]()mutable
  7. {
  8. //这里面不能用外面的x,y
  9. //lambda想用外面函数中的对象,需要捕捉
  10. int tmp = x;
  11. x = y;//传值过来的不能修改,加mutable就好了
  12. y = tmp;
  13. };
  14. return 0;
  15. }

lambda表达式解决上面说的货物排序的问题:

  1. struct Goods
  2. {
  3. string _name; // 名字
  4. double _price; // 价格
  5. int _evaluate; // 评价
  6. };
  7. int main()
  8. {
  9. Goods gds[] = { { "苹果", 2.1, 5}, { "香蕉", 3, 4}, { "橙子", 2.2,3}, { "菠萝", 1.5, 4} };
  10. sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& gl, const Goods& g2)
  11. {
  12. return gl._price < g2._price;
  13. });
  14. sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& gl, const Goods& g2)
  15. {
  16. return gl._price > g2._price;
  17. });
  18. sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& gl, const Goods& g2)
  19. {
  20. return gl._evaluate < g2._evaluate;
  21. });
  22. sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& gl, const Goods& g2)
  23. {
  24. return gl._evaluate > g2._evaluate;
  25. });
  26. return 0;
  27. }

lambda表达式之间不能相互赋值,即使看起来类型相同,允许使用一个lambda表达式拷贝构造一个新的副本,可以将lambda表达式赋值给相同类型的函数指针

  1. void(*PF)();
  2. int main()
  3. {
  4. auto f1 = []{cout << "hello world" << endl; };
  5. auto f2 = []{cout << "hello world" << endl; };
  6. f1 = f2;//这里会编译失败,提示找不到operator=()
  7. auto f3(f2);
  8. return 0;
  9. PF = f2;
  10. }

我们来看一下lamada表达式的汇编语言,发现底层是调用了operator(),这说明了它底层是调用了一个仿函数:

我们定义一个lambda表达式,对我们而言是类型是匿名,实际编译器会把它转换成仿函数,这个仿函数的名称是lambda_uuid,uuid是随机生成的,为了保证不同的lambda表达式类型名称是不一样的。

接下来我们看下一种可调用类型:

可调用类型有:

  1. 函数指针(C语言)
  2. 仿函数
  3. lambda(匿名函数),我们看起来是匿名,但是编译器看起来不是匿名(掌握格式和原理)
  4. 包装器

包装器

function包装器,包装器是对可调用对象的包装,function包装器也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。

那么我们来看看,我们为什么需要function呢?首先来看下面的程序

  1. ret = func(x);

上面func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能是lamber表达式对象?所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下!
为什么呢?我们继续往下看:

  1. template<class F, class T>
  2. T useF(F f, T x)
  3. {
  4. static int count = 0;
  5. cout << "count:" << ++count << endl;
  6. cout << "count:" << &count << endl;
  7. return f(x);
  8. }
  9. double f(double i)
  10. {
  11. return i / 2;
  12. }
  13. struct Functor
  14. {
  15. double operator()(double d)
  16. {
  17. return d / 3;
  18. }
  19. };
  20. int main()
  21. {
  22. // 函数名
  23. cout << useF(f, 11.11) << endl;
  24. // 函数对象
  25. cout << useF(Functor(), 11.11) << endl;
  26. // lamber表达式
  27. cout << useF([](double d)->double{ return d/4; }, 11.11) << endl;
  28. return 0;
  29. }

通过上面的程序验证,我们会发现useF函数模板实例化了三份。
包装器可以很好的解决上面的问题:

该模板的第一个参数是返回值,后面的是参数列表的类型,所以我们这样用:

  1. int main()
  2. {
  3. //包装函数指针
  4. std::function<double(double)> f1(f);
  5. f1(10.1);
  6. //包装仿函数对象
  7. // 函数对象
  8. std::function<double(double)> f2 = Functor();
  9. cout << f2(10.1) << endl;
  10. //包装lambda
  11. std::function<double(double)> f3 = [](double d)->double {return d / 4; };
  12. f3(10.1);
  13. useF(f1, 10.1);
  14. useF(f2, 10.1);
  15. useF(f3, 10.1);
  16. }

我们发现此时就没有实例化出三份,只实例化出一份useF,因为传进来的F类型都是std::function<double(double)>。

包装器也可以对成员函数和静态成员函数进行包装:

  1. class Plus
  2. {
  3. public:
  4. static int plusi(int a, int b)
  5. {
  6. return a + b;
  7. }
  8. double plusd(double a, double b)
  9. {
  10. return a + b;
  11. }
  12. };
  13. int main()
  14. {
  15. // 类的静态成员函数
  16. std::function<int(int, int)> func4 = Plus::plusi;
  17. cout << func4(1, 2) << endl;
  18. //类的非静态成员函数
  19. std::function<double(Plus, double, double)> func5 = &Plus::plusd;//类的成员函数需要取地址
  20. cout << func5(Plus(), 1.1, 2.2) << endl;//需要传对象进去
  21. return 0;
  22. }

在包装类的非静态成员函数时需要传匿名对象进去,那么有没有办法不传呢?

bind

std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可
调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而
言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M
可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺
序调整等操作。

  1. int main()
  2. {
  3. // 使用bind进行优化
  4. // 需要绑定的参数,直接绑定值,不需要绑定的参数给 placeholders::_1 、 placeholders::_2.... 进行占位
  5. std::function<int(int, int)> func4 = std::bind(&Sub::sub, Sub(), placeholders::_1, placeholders::_2);
  6. cout << func4(1, 3) << endl;
  7. // 还可以调整参数顺序
  8. std::function<int(int, int)> func5 = std::bind(&Sub::sub, Sub(), placeholders::_2, placeholders::_1);
  9. cout << func5(1, 3) << endl;
  10. //
  11. return 0;
  12. }

thread

  1. #include<thread>
  2. void F1(int n)
  3. {
  4. for(int i = 0;i<n;i++)
  5. {
  6. cout<<i<<endl;
  7. }
  8. }
  9. int main()
  10. {
  11. thread t1(F1,10);
  12. thread t2(F1,10);
  13. t1.join();
  14. t2.join();
  15. return 0;
  16. }

创建一个空线程,不执行

创建一个执行fn可调用对象的线程:

  1. #include<thread>
  2. void F1(int n)
  3. {
  4. for(int i = 0;i<n;i++)
  5. {
  6. cout<<i<<endl;
  7. }
  8. }
  9. int main()
  10. {
  11. int n;
  12. cin>>n;
  13. thread t1([n,&i]
  14. {
  15. while(i<n)
  16. {
  17. cout<< i <<endl;
  18. ++i;
  19. }
  20. });
  21. thread t2([n,&i]
  22. {
  23. while(i<n)
  24. {
  25. cout<< i <<endl;
  26. ++i;
  27. }
  28. });
  29. t1.join();
  30. t2.join();
  31. return 0;
  32. }

移动赋值使用场景:

  1. #include<thread>
  2. #include<vector>
  3. #include<mutex>
  4. int main()
  5. {
  6. int n;
  7. cin>>n;
  8. vector<thread> works(n);
  9. std::mutex mtx;
  10. for(auto& thd : works)
  11. {
  12. //移动赋值使用场景
  13. thd = thread([&mtx](){
  14. for(int i = 0;i<10;++i)
  15. {
  16. mtx.lock();
  17. cout<<this_thread::get_id()<<":"<<i<<endl;
  18. mtx.unlock();
  19. }
  20. });
  21. }
  22. for(auto& thd : works)
  23. {
  24. thd.join();
  25. }
  26. return 0;
  27. }

注意:++操作不是原子的

  1. #include<thread>
  2. #include<vector>
  3. #include<mutex>
  4. int main()
  5. {
  6. int n;
  7. cin>>n;
  8. vector<thread> works(n);
  9. std::mutex mtx;
  10. size_t x = 0;
  11. for(auto& thd : works)
  12. {
  13. //移动赋值使用场景
  14. thd = thread([&mtx,&x](){
  15. for(int i = 0;i<100000;++i)
  16. {
  17. //mtx.lock();
  18. ++x;
  19. //mtx.unlock();
  20. }
  21. });
  22. }
  23. for(auto& thd : works)
  24. {
  25. thd.join();
  26. }
  27. cout<<x<<endl;
  28. return 0;
  29. }

所以我们需要加锁,锁加在外面比较好,++操作太快了,避免频繁的线程切换,保存上下文的消耗

锁加在外面和里面的效率对比:

  1. #include<thread>
  2. #include<vector>
  3. #include<mutex>
  4. int main()
  5. {
  6. int n;
  7. cin>>n;
  8. vector<thread> works(n);
  9. std::mutex mtx;
  10. size_t x = 0;
  11. size_t total_time = 0;
  12. for(auto& thd : works)
  13. {
  14. //移动赋值使用场景
  15. thd = thread([&mtx,&x,&total_time](){
  16. size_t begin = clock();
  17. for(int i = 0;i<100000;++i)
  18. {
  19. mtx.lock();
  20. ++x;
  21. mtx.unlock();
  22. }
  23. size_t end = clock();
  24. total_time += (end-begin);
  25. cout<<this_thread::get_id()<<":"<<end-begin<<endl;
  26. });
  27. }
  28. for(auto& thd : works)
  29. {
  30. thd.join();
  31. }
  32. cout<<x<<endl;
  33. cout<<total_time<<endl;
  34. return 0;
  35. }

明显加在外面效率高

有什么做法能让不加锁,就能保证线程安全呢?在C++库当中还有相关的原子操作

原子操作:

  1. #include<thread>
  2. #include<vector>
  3. #include<mutex>
  4. int main()
  5. {
  6. int n;
  7. cin>>n;
  8. vector<thread> works(n);
  9. atomic<size_t> x = 0;//保证x的一些操作是原子的
  10. atomic<size_t> total_time = 0;
  11. for(auto& thd : works)
  12. {
  13. //移动赋值使用场景
  14. thd = thread([&x,&total_time](){
  15. size_t begin = clock();
  16. for(int i = 0;i<100;++i)
  17. {
  18. ++x;
  19. }
  20. size_t end = clock();
  21. total_time += (end-begin);
  22. cout<<this_thread::get_id()<<":"<<end-begin<<endl;
  23. });
  24. }
  25. for(auto& thd : works)
  26. {
  27. thd.join();
  28. }
  29. cout<<x<<endl;
  30. cout<<total_time<<endl;
  31. return 0;
  32. }

线程安全笔试题

实现两个线程交替打印1-100,一个线程打印奇数,一个线程打印偶数

先看这个版本:

  1. #include<thread>
  2. #include<vector>
  3. #include<mutex>
  4. int main()
  5. {
  6. int i = 1;
  7. //大部分情况没问题,并不能完全保证一定是交替打印
  8. //打印奇数
  9. mutex mtx;
  10. thread t1([&i](){
  11. while(i<100)
  12. {
  13. mtx.lock();
  14. cout<<this_thread::get_id()<<":"<<i<<endl;
  15. i += 2;
  16. mtx.unlock();
  17. }
  18. });
  19. //极端场景下:假设主线程执行到这里时间片用完了,进入休眠排队
  20. //此时t2线程还没有创建
  21. int j = 2;
  22. //打印偶数
  23. thread t2([&j](){
  24. while(j<100)
  25. {
  26. mtx.lock();
  27. cout<<this_thread::get_id()<<":"<<i<<endl;
  28. j += 2;
  29. mtx.unlock();
  30. }
  31. });
  32. t1.join();
  33. t2.join();
  34. return 0;
  35. }

这个代码是有问题的:极端场景下:考虑到线程切换,假设主线程执行到21行代码这里时间片用完了,进入休眠排队,此时t2线程还没有创建,那么t1线程就会再次获取锁就会继续打印

假设某次unlock以后,t1时间片到了,进入休眠排队,此时也会导致t2连续获取到锁打印

在说下面正确代码时,我们先说两个锁:

lock_guard

lock_guard 类是一个mutex封装者,它为了拥有一个或多个mutex而提供了一种方便的 RAII style 机制。当一个lock_guard对象被创建后,它就会尝试去获得给到它的mutex的所有权。当控制权不在该lock_guard对象所被创建的那个范围后,该lock_guard就会被析构,从而mutex被释放。unique_lock也是类似。

lock_guardd的实现:

  1. template<class _Mutex>
  2. class lock_guard
  3. {
  4. public:
  5. // 在构造lock_gard时,_Mtx还没有被上锁
  6. explicit lock_guard(_Mutex& _Mtx)
  7. : _MyMutex(_Mtx)
  8. {
  9. _MyMutex.lock();
  10. }
  11. // 在构造lock_gard时,_Mtx已经被上锁,此处不需要再上锁
  12. lock_guard(_Mutex& _Mtx, adopt_lock_t)
  13. : _MyMutex(_Mtx)
  14. {}
  15. ~lock_guard() _NOEXCEPT
  16. {
  17. _MyMutex.unlock();
  18. }
  19. lock_guard(const lock_guard&) = delete;
  20. lock_guard& operator=(const lock_guard&) = delete;
  21. private:
  22. _Mutex& _MyMutex;
  23. };

通过上述代码可以看到,lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封
装,在需要加锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数
成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁
问题。
lock_guard的缺陷:太单一,用户没有办法对该锁进行控制,因此C++11又提供了unique_lock。

unique_lock

与lock_gard类似,unique_lock类模板也是采用RAII的方式对锁进行了封装,并且也是以独占所有权的方式管理mutex对象的上锁和解锁操作,即其对象之间不能发生拷贝。在构造(或移动(move)赋值)时,unique_lock 对象需要传递一个 Mutex 对象作为它的参数,新创建的unique_lock 对象负责传入的 Mutex 对象的上锁和解锁操作。使用以上类型互斥量实例化
unique_lock的对象时,自动调用构造函数上锁,unique_lock对象销毁时自动调用析构函数解锁,可以很方便的防止死锁问题。
与lock_guard不同的是,unique_lock更加的灵活,提供了更多的成员函数:

  • 上锁/解锁操作:lock、try_lock、try_lock_for、try_lock_until和unlock
  • 修改操作:移动赋值、交换(swap:与另一个unique_lock对象互换所管理的互斥量所有权)、释放(release:返回它所管理的互斥量对象的指针,并释放所有权)
  • 获取属性:owns_lock(返回当前对象是否上了锁)、operator bool()(与owns_lock()的功能相
    同)、mutex(返回当前unique_lock所管理的互斥量的指针)。
  1. #include<thread>
  2. #include<vector>
  3. #include<mutex>
  4. int main()
  5. {
  6. int i = 1;
  7. mutex mtx;
  8. //打印奇数
  9. thread t1([&i](){
  10. while(i<100)
  11. {
  12. //mtx.lock();
  13. {
  14. std::lock_guard<mutex> lock(mtx);//构造的时候加锁,lock析构的时候自动解锁
  15. //std::unique_lock<mutex> lock(mtx);//unique_lock和lock_guard差别不大,构造的时候加锁,lock析构的时候自动解锁
  16. cout<<this_thread::get_id()<<":"<<i<<endl;
  17. i += 2;
  18. }
  19. //mtx.unlock();
  20. }
  21. });
  22. //极端场景下:假设主线程执行到这里时间片用完了,进入休眠排队
  23. //此时t2线程还没有创建
  24. int j = 2;
  25. //打印偶数
  26. thread t2([&j](){
  27. while(j<100)
  28. {
  29. std::lock_guard<mutex> lock(mtx);//构造的时候加锁,lock析构的时候自动解锁
  30. cout<<this_thread::get_id()<<":"<<i<<endl;
  31. j += 2;
  32. }
  33. });
  34. t1.join();
  35. t2.join();
  36. return 0;
  37. }

在解决上面问题我们要引入条件变量:

第一个构造一定阻塞

第二个构造,pred返回false就调用wait阻塞

pred如果一直是false,那么被唤醒以后也会继续wait阻塞

pred返回true就不调用wait阻塞

本质是通过flag和条件变量配合控制互斥

  1. #include<iostream>
  2. using namespace std;
  3. #include<thread>
  4. #include<vector>
  5. #include<mutex>
  6. #include<condition_variable>
  7. int main()
  8. {
  9. int i = 1;
  10. //大部分情况没问题,并不能完全保证一定是交替打印
  11. //打印奇数
  12. mutex mtx;
  13. condition_variable cv;
  14. bool flag = true;
  15. thread t1([&i, &mtx, &cv, &flag]() {
  16. while (i < 100)
  17. {
  18. std::unique_lock<mutex> lock(mtx);
  19. cv.wait(lock, [&flag]() { return flag; });//flag最开始要是true,t1线程刚开始要先运行,因为是奇数
  20. cout << this_thread::get_id() << ":" << i << endl;
  21. i += 2;
  22. flag = false;
  23. cv.notify_one();
  24. }
  25. });
  26. int j = 2;
  27. //打印偶数
  28. thread t2([&j,& mtx, &cv, &flag]() {
  29. while (j < 100)
  30. {
  31. std::unique_lock<mutex> lock(mtx);
  32. cv.wait(lock, [&flag]() { return !flag; });
  33. cout << this_thread::get_id() << ":" << j << endl;
  34. j += 2;
  35. flag = true;
  36. cv.notify_one();
  37. }
  38. });
  39. t1.join();
  40. t2.join();
  41. return 0;
  42. }

t1打印运行时,t2肯定没有打印运行

t2可能有三种状态:

  1. 时间片用完了,休眠排队
  2. wait
  3. lock

先说第一种情况:当t2时间片用完了,休眠排队,t1打印完后,notify_one没有notify到t2,t1继续获取锁,但是此时flag是false,t1会wait,wait时会把锁解了,当t2被切回来时,然后t2继续获取到锁,此时!flag是true,不会wait,t2就进行打印运行了。

第二种情况:t2在wait,t1打印完后,notify_one到t2,t2被唤醒继续完成打印任务

第三种情况:t2在lock,t1打印完后出了作用域会将锁自动解了,然后t2就获取到了锁,即使t1竞争更大再次获取到锁,他也会在wait那里阻塞(阻塞时会解锁)。然后t2肯定能拿到锁

相关文章