struct Point { int _x; int _y; Point(int x, int y) :_x(x) , _y(y) {} }; class Date { public: Date(int year, int month, int day) :_year(year) , _month(month) , _day(day) { cout << "Date(int year, int month, int day)" << endl; } private: int _year; int _month; int _day; };
可以这样初始化:
//int a[] = { 1, 2, 3, 4 }; int a[] { 1, 2, 3, 4 }; //Point p = { 1, 2 }; Point p{ 1, 2 }; int* p1 = new int(0); int* p2 = new int[5]{1,2,3,4,5}; Point* p3 = new Point[3]{{1, 1}, { 2, 2 }, { 3, 3 }}; //需要有初始化列表初始化,底层未封装这种初始化 Date d1(2022, 3, 13); Date d2 = { 2022, 3, 15 }; Date d3{ 2022, 3, 15 }; Date{2022,3,15}; int i = 1; int j = { 2 }; int k{ 3 };
C++11里面扩展了{}初始化使用,基本都可以使用它来初始化,但是建议还是按旧的用法来使用,一般new[]建议使用它来初始化
initializer list用来接收{},同时支持迭代器
例如:auto il = { 10, 20, 30 }; initializer_list<int> il={ 10, 20, 30 }; //支持迭代器: std::initializer_list<int>::iterator it; // same as: const int* it for ( it=il.begin(); it!=il.end(); ++it) { cout<< *it <<endl; }
以vector为例:
vector的constructor函数vector (initializer_list<value_type> il, const allocator_type& alloc = allocator_type());
vector中要实现新的成员函数vector(initializer_list l)
//C++11实现的initializer_list typedef T* iterator; vector(initializer_list<T> l) { _start = new T[l.size()]; _finish = _start + l.size(); _endofstorage = _start + l.size(); iterator vit = _start; typename initializer_list<T>::iterator lit = l.begin(); while (lit != l.end()) { *vit++ = *lit++; } }
auto最初是在C语言中的关键字,用于定义一个自动类型,在栈上,不用自动销毁,现已废弃
在C++中auto为自动推导类型
auto最常用在省略迭代器类型,比如:
initializer_list<T> l; auto lit = l.begin();
关键字decltype将变量的类型声明为表达式指定的类型
比如:
const int x = 1; double y = 2.2; decltype(x * y) ret; // ret的类型是double decltype(&x) p;// p类型是int*
decltype的一些使用使用场景
template<class T1, class T2> void F(T1 t1, T2 t2) { decltype(t1 * t2) ret; cout << typeid(ret).name() << endl; }
在传统C中 ‘
NULL
’ 是 头文件 stddef.h 中的一个宏,可能被定义为字面常量0
,或者被定义为无类型指针(void*)
的常量,C++98也用的NULL#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endif
所以我们在遇到这样的代码:
void f(int) { cout<<"f(int)"<<endl; } void f(int*) { cout<<"f(int*)"<<endl; } int main() { f(NULL); f((int*)NULL); return 0; }
第一次打印的是f(int)与我们想打印
空指针
相悖,只有第二次强制类型转换才可以达到预期效果
所以在C++中引入了专门用来表示空指针的nullptr
注意
:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr
底层其实就是迭代器,参考:C++初阶—string类的模拟实现的iterator迭代器部分
以 R"( 开头, )" 结束,是可以跨越多行的字符串字面值,转义字符如 \t\n 在raw string literal中是普通的文本,而不再是转义字符
- 因为raw string literal以 )“结束,不能在raw string literal 中再包含字符 )”。
- 如果你想在
raw string literal
中包含)"
,需要使用raw string literal扩展语法:const char* raw = R"d-char-sequences(raw-char-sequences)d-char-sequences"
raw-char-sequences
是原始的文本,首尾的d-char-sequences
是分隔符,首尾分隔符必须一样,最长为16个字符,而且)d-char-sequences 不能在 raw-char-sequence
s 中出现 之前的代码可以这样改正:const char* text = R"--(Embedded )" in string)--";
原文:C++ raw string literal
参考:C++进阶—智能指针
array
和a1; int a2[10];
底层一样,除了实现了迭代器,同时array加了越界assert强制检查,而对于数组,编译器是抽查forward_list
无实际的用处,仅仅比list少用了一个指针,且只支持push_front、pop_frontunordered_map/unordered_set
,map和set的hash封装,较多数据时效率更高- 插入函数的
右值引用
版本(见下)
复习左值引用:C++初阶—C++基础入门概览引用部分
左值/左值引用:
- 左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址
- 左值引用就是给左值的引用,给左值取别名
无论左值引用还是右值引用,都是给对象取别名
右值/右值引用:
- 右值也是一个表示数据的表达式,如:字面常量、表达式返回值,传值返回函数的返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址
- 右值引用就是对右值的引用,给右值取别名
例如:
double x=1.1,y=1.2;//x,y是左值 //以下都是右值 12; x+y; min(x,y); //以下是右值引用 int&& a=10; double&& b=x+y; double&& c=min(x,y);
注意:
赋值运算符的左操作数必须为左值
- 右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置(
这个别名就变成左值了
),且可以取到该位置的地址- 加了const 的右值引用不可被修改(同左值)
左值右值比较:
- 左值引用只能引用左值,不能引用右值,但是const左值引用既可引用左值,也可引用右值
- 右值引用只能右值,不能引用左值,但是右值引用可以move以后的左值,比如下面的代码:
int a=2; int&& s=2; //下面的语句会报错: message : 无法将左值绑定到右值引用 int&& s1=a; int&& s2=s; //使用move()将a转换为右值 int&& s3=std::move(a);
在没有右值引用的时候(C++98时不用左值引用),如何解决:
错误的思路
:使用一个static的变量让每次调用的都是一个变量,然后每次使用的时候清空一下
问题
:多线程下会有线程不安全的情况发生
错误使用右值引用:
string&& to_string(int val) { string str; //... return move(str); } cout << test2::to_string(123).c_str() << endl;
右值的应用场景:
(以以前我们模拟实现的string为例)
对于只有左值引用形参的函数时,f(1)和f(a)均会匹配void f(const int& a)
,而当有右值引用形参的函数时f(1)会优先匹配void f(int&& a)
void f(const int& a) { cout << "void f(const int& a)" << endl; } void f(int&& a) { cout << "void f(int&& a)" << endl; } int a = 10; f(1); f(a);
// 移动构造 string(string&& s) :_str(nullptr) , _size(0) , _capacity(0) { cout << "string(string&& s) -- 移动拷贝" << endl; //this->swap(s); swap(s); }
//移动构造 string(string&& s) :_str(nullptr) , _size(0) , _capacity(0) { cout << "string(string&& s) -- 移动拷贝" << endl; //this->swap(s); swap(s); }
直接接收打印:
//拷贝构造 string(const string& s) :_str(nullptr) { cout << "string(const string& s) -- 拷贝构造" << endl; string tmp(s._str); this->swap(tmp); } string to_string(int val) { string str; while (val) { int i = val % 10; str += ('0' + i); val /= 10; } reverse(str.begin(), str.end()); return str; } int main() { cout << test2::to_string(123).str.c_str() << endl; return 0; }
先接收再打印:
int main() { test2::string s = test2::to_string(123); cout << s.c_str() << endl; return 0; }
- 第一次拷贝构造,给str产生的临时变量
- 第二次拷贝构造给s
- 编译器优化为一次拷贝构造
- 第一次将to_string的返回值str是左值(编译器优化识别为将亡值)(拷贝构造)
- 第二次将赋给str返回时产生的临时变量识是将亡值 (移动构造)
- 优化为一次,跳过临时变量,直接将堆上的资源转移给s
string& operator=(string&& s) { cout << "string& operator=(string&& s) -- 移动赋值" << endl; swap(s); return *this; }
先定义一个string类型的s,再赋值给s
int main() { test2::string s; s = test2::to_string(123); cout << s.c_str() << endl; return 0; }
分析:
注意
:如果仅仅是定义右值引用,那么对象本身不会被移走,在作为参数时会发生对象被移走
使用场景1
:配合push_back使用降低开销
使用场景2
:初始化列表moveclass Person { public: Person(string name, string sex, int age) : _name(name) , _sex(sex) , _age(age) {} void swap(Person& s) { ::swap(_name, s._name); ::swap(_sex, s._sex); ::swap(_age, s._age); } Person(Person&& p) :_name(p._name) , _sex(p._sex) , _age(p._age) { //swap(p); } #if 0 Person(Person&& p) : _name("") , _sex("") , _age(p._age) { swap(p); } #else Person(Person&& p) : _name(move(p._name)) , _sex(move(p._sex)) , _age(p._age) {} #endif private: string _name; string _sex; int _age; }; Person GetTempPerson() { Person s("prety", "male", 18); return s; } int main() { Person s(GetTempPerson()); return 0; }
引用折叠规则:
- & + & -> &
- & + && -> &
- && + & -> &
- && + && -> &&
只要有&就是左值引用
详细请参考
:
C++11完美转发及实现方法详解
【C++11】引用折叠和完美转发
C++11中对于函数模板中使用右值引用语法定义的参数来说,它不再遵守右值引用形式的参数只能接收右值,不能接收左值的规则,它既可以接收右值,也可以接收左值
函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)void Fun(int& x) { cout << "lvalue ref" << endl; } void Fun(int&& x) { cout << "rvalue ref" << endl; } void Fun(const int& x) { cout << "const lvalue ref" << endl; } void Fun(const int&& x) { cout << "const rvalue ref" << endl; } template<typename T> void PerfectForward(T&& t) { //Func(t); //always lvalue ref Fun(std::forward<T>(t)); // can be lvalue or rvalue ref } int main() { PerfectForward(10); // rvalue ref int a; PerfectForward(a); // lvalue ref PerfectForward(std::move(a)); // rvalue ref const int b = 8; PerfectForward(b); // const lvalue ref PerfectForward(std::move(b)); // const rvalue ref return 0; }
总结:
C++11以后增加了两个默认成员函数:默认移动构造,默认移动赋值
注意:
- 如果你没有自己实现移动构造函数且没有实现析构函数.拷贝构造、拷贝赋值重载
都没有实现
。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数.对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造
- 如果你没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载都没有实现,那么编译器会自动生成一个默认移动赋值 默认生成的移动移动赋值)时于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值 (默认移动赋伯跟上面移动构造完全类似)
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值
注意1:
class test{ private: std::string _name = "jads";//提供缺省值,这里不是初始化 int _age; };
注意1:
static 成员不能在类里初始化
但是static const成员可以在类里初始化
class test{ private: static int _si; static const int _constsi=-1;//注意这里不是缺省值 }; int test::_s1=0;
defaulted 函数特性仅用于类的特殊成员函数,且该特殊成员函数没有缺省参数
前面说过,只要自己实现了析构函数.拷贝构造、拷贝赋值重载,都不会生成默认的移动构造或移动赋值,但我们可以强制default让系统自己生成默认移动构造
注意:
在一个类中如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值
class Person { public: Person(const char* name = "", int age = 0) :_name(name) , _age(age) {} //Person(const Person& p) = default; //Person(Person&& p) = default; ~Person(){} private: test2::string _name; int _age; }; int main() { Person s1; Person s2 = s1; Person s3 = std::move(s1); return 0; }
声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数
template <class ...Args> void ShowList(Args... args) {} ShowList(1); ShowList(1, 'A'); ShowList(1, 'A', std::string("sort"));
我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过
展开参数包
的方式来获取参数包中的每个参数
汇编代码:自动推导出来
由于参数类型不一样,不可以直接使用for循环拿到所有的参数
参考:第21课 可变参数模板(2)_展开参数包
参考:泛化之美–C++11可变模版参数的妙用
方法1:
使用value匹配第一个参数, Args… args参数包接受剩余的参数
每次递归调用ShowList(args…);每次读取一个参数// 递归终止函数1 template <class T> void ShowList(const T& t) { cout << t << endl; } // 递归终止函数2 void ShowList() { cout << endl; } // 展开函数 template <class T, class ...Args> void ShowList(T value, Args... args) { cout << value << " "; ShowList(args...); }
方法2
:逗号表达式展开参数包template <class T> void PrintArg(T t) { cout << t << " "; } //展开函数 template <class ...Args> void ShowList(Args... args) { int arr[] = { (PrintArg(args), 0)... }; cout << endl; }
{(printarg(args), 0)...}
将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), ... )
使用initializer_list+lambda表达式
[&]{std::initializer_list<int>{(cout << args << endl,0)...};}();
template <class... Args> void emplace_back (Args&&... args);
emplace系列的接口,支持模板的可变参数,并且万能引用
emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
例如:
std::list< std::pair<int, char> > mylist; mylist.emplace_back(make_pair(30, 'c')); mylist.push_back(make_pair(40, 'd')); mylist.emplace_back(10, 'a'); mylist.push_back({ 50, 'e' });
注意
:不支持mylist.emplace_back({ 50, 'e' });
写法
insert
、push_back
之所以慢的原因在与创建临时对象
时,需要申请内存空间, 申请内存空间一向是耗时很严重的操作;之后再通过拷贝构造函数把创建的临时对象复制到vector空间中,期间的复制操作也是需要CPU时间的;- emplace_back之所以快的其原因是直接在vector中已有的空间上, 调用了构造函数, 节省了临时对象的内存空间申请以及移动构造函数的复制操作
emplace_back和push_back效率问题:
仿函数使用场景:map set模拟实现取得key值,参考:C++进阶—Map和Set使用及模拟实现
格式
:[capture-list] (parameters) mutable -> return-type { statement}
lambda表达式各部分
[capture-list]
: 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用
- [ var]:表示值传递方式捕捉变量var
- [=]:表示值传递方式捕获所有父作用域中的变量(成员函数中包括this)
- [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有父作用域中的变量(成员函数中包括this)
(parameters)
:参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略mutable
:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)->returntype
:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导{statement}
:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量
注意:
- 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空
- 父作用域指包含lambda函数的语句块
- 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量 [&,a,this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
- 捕捉列表不允许变量重复传递,否则就会导致编译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
- 在块作用域以外的lambda函数捕捉列表必须为空
- 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错
lambda表达式之间不能相互赋值,即使看起来类型相同
- 允许使用一个lambda表达式拷贝构造一个新的副本,可以将lambda表达式赋值给相同类型的函数指针
lambda表达式不应出现在未经评估的上下文中(例如decltype和sizeof等)
因为每个lambda派生的闭包对象可以具有完全不同的类型,毕竟它们就像匿名函数一样
lambda对象没有默认构造函数,无法构造 lambda 实例
使用:
lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量//省略返回值类型 auto fun1 = [&](int c){b = a + c; }; fun1(10) cout<<a<<" "<<b<<endl; // 复制捕捉x int x = 10; auto add_x = [x](int a) mutable { x *= 2; return a + x; }; cout << add_x(10) << endl;
仿函数和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; }
查看反汇编:
仿函数:
lambda:
注意
:定义一个lambda表达式对我们而言类型是匿名的,实际对编译器不是匿名的,编译器会将他转换为一个仿函数
,这里的是 lambda_+通过底层算法生成的uuid
组成的类名字(uuid标识唯一)
这里也就解释了lambda表达式不能互相赋值的原因每个类名字不同
所以我们定义lambda表达式是这样的:
auto r2 = [=](double monty, int year)->double {return monty * rate * year;}
引入:
ret = func(x);
func可能是函数名,函数指针,函数对象(仿函数对象),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函数模板,用来封装普通函数,仿函数,lambda表达式,同时在其中定义一个static int变量count,每次打印count就输出一次他的地址
每个count的地址不同,导致模板的效率低下
function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器
template <class T> function; // undefined template <class Ret, class... Args> class function<Ret(Args...)>;
将上面的代码添加包装器封装:
std::function<double(double)> f1(f); //std::function
f1 = f; // 包装仿函数对象 std::function<double(double)> f2 = Functor(); // 包装lambda std::function<double(double)> f3 = [](double d)->double{ return d / 4; }; useF(f1, 10.11); useF(f2, 10.11); useF(f3, 10.11);class Plus { public: static int plusi(int a, int b) { return a + b; } double plusd(double a, double b) { return a + b; } }; // 包装类的静态成员函数 std::function<int(int, int)> f4 = Plus::plusi; cout << f4(1, 2) << endl; // 包装类的非静态成员函数 std::function<double(Plus, double, double)> f5 = &Plus::plusd; cout << f5(Plus(), 1.1, 2.2) << endl;
对于类的静态成员函数,对于类的非静态成员函数需要加上取地址符号,同时要传一个类进去,注意不能传地址,this指针是编译器自己进行传递的
应用场景
:leetcode题目:逆波兰表达式求值:
std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作
头文件
语法:
- 无返回值
template
/* unspecified */bind (Fn&& fn, Args&&... args);
- 有返回值
template
/* unspecified */bind (Fn&& fn, Args&&... args);
注意:
- Args&&… arg可以包含形如_n的占位符,比如_1表示fn的第一个参数,_2表示fn的第二个参数,以placeholder命名空间为例,_1,_2可以更换位置,但_1永远表示的第一个参数,_2表示的第二个参数,同时,占位符可以只有一个或多个,例子见下面代码
代码如下:
int Plus(int a, int b) { return a + b; } class Sub { public: int sub(int a, int b) { return a - b; } }; // 使用bind进行优化 // 需要绑定的参数,直接绑定值,不需要绑定的参数给 placeholders::_1 、 placeholders::_2.... 进行占位 std::function<int(int, int)> func4 = std::bind(&Sub::sub, Sub(), placeholders::_1, placeholders::_2); cout << func4(1, 3) << endl; // 调整参数顺序 std::function<int(int, int)> func5 = std::bind(&Sub::sub, Sub(), placeholders::_2, placeholders::_1); cout << func5(1, 3) << endl;
构造:
thread (const thread&) = delete;
thread& operator= (const thread&) = delete;
thread (thread&& x) noexcept;
thread& operator= (thread&& rhs) noexcept;
template
explicit thread (Fn&& fn, Args&&... args);
注意
:不允许拷贝构造和拷贝赋值,但是允许移动构造和移动赋值
当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。线程函数一般情况下可按照以下三种方式提供:
- 函数指针
- lambda表达式
- 函数对象
例如lambda表达式:
int n = 0; cin >> n; vector<thread> works(n); mutex mtx; int x = 0; for (auto& thd : works) { thd = thread([&mtx,&x]() { for (int i = 0; i < 100000; i++) { mtx.lock(); //cout << this_thread::get_id() << endl; x++; mtx.unlock(); } }); } for (auto& thd : works) { thd.join(); } cout << x;
注意
:++操作不是原子的
需要加锁,且锁加在循环外面更好,不会频繁的切换锁
clock函数验证:
size_t begin = clock(); mtx.lock(); for (int i = 0; i < 100000; i++) { //mtx.lock(); x++; //mtx.unlock(); } mtx.unlock(); size_t end = clock(); cout << end - begin << endl;
其他:
- thread::get_id()获取指定线程的id
- thread::join:相当于Linux pthread库中的waitpid
注意:线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参
thread头文件包含了一个this_thread的命名空间,直接调用get_id()等函数
- this_thread::get_id()获取当前线程的id
构造
:
mutex (const mutex&) = delete;
防拷贝
lock
- 获取锁,且失败会阻塞
unlock
- 释放锁
try_lock
- 尝试获取锁,且若未成功获取不阻塞
题目
:两个线程交替打印奇偶数
思路1:创建一把锁,两个线程竞争这把锁
int main() { mutex mtx; int i = 1; thread t1([&i, &mtx](){ while (i < 100) { mtx.lock(); { cout << this_thread::get_id() << ":" << i << endl; i += 2; } mtx.unlock(); } }); int j = 2; thread t2([&j, &mtx](){ while (j < 100) { mtx.lock(); cout << this_thread::get_id() << ":" << j << endl; j += 2; mtx.unlock(); } }); t1.join(); t2.join(); return 0; }
- 极端情况1:当t1在某次释放锁资源时t1的时间片到了,进入等待队列,t2连续竞争到锁
模拟
:// 假设某次unlock以后,t1时间片到了,进入休眠排队 // 会导致t2连续获取到锁打印 if (i == 29) std::this_thread::sleep_for(std::chrono::milliseconds(3));
- 极端思路2:主线程运行到一半时时间片到了,进入等待队列
模拟
// 极端场景下:假设主线程执行到这里时间片用完了,进入休眠排队 // sleep模拟一下这个场景 std::this_thread::sleep_for(std::chrono::milliseconds(1));
- 极端情况3:在t1线程执行中途时间片到了
所以使用锁不是线程安全的
推荐使用lock_guard和unique_lock
lock_guard:
- 在构造时,互斥对象被调用线程锁定,而在销毁时,互斥对象被解锁。 它是最简单的锁,并且作为具有自动持续时间的对象特别有用,该持续时间一直持续到其上下文结束。 通过这种方式,保证了互斥对象在抛出异常的情况下被正确解锁
lock_guard类的底层结构:
template<class _Mutex> class lock_guard { public: // 在构造lock_gard时,_Mtx还没有被上锁 explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) { _MyMutex.lock(); } // 在构造lock_gard时,_Mtx已经被上锁,此处不需要再上锁 lock_guard(_Mutex& _Mtx, adopt_lock_t) : _MyMutex(_Mtx) {} ~lock_guard() _NOEXCEPT//成员变量就是锁,直接销毁 { _MyMutex.unlock(); } lock_guard(const lock_guard&) = delete; lock_guard& operator=(const lock_guard&) = delete; private: _Mutex& _MyMutex;//私有成员变量为引用的经典场景 };
unique_lock:
- 和lock_guard的区别是可以手动控制临时解锁加锁
使用lock_guard和unique_lock更改代码:
定义在作用域内,出作用域自动调用析构函数释放锁{ std::lock_guard<mutex> lock(mtx); //std::unique_lock
lock(mtx); cout << this_thread::get_id() << ":" << j << endl; j += 2; }
递归互斥锁,递归程序中使用
try_lock_for:
- 尝试获取锁,最多阻塞指定时间
try_lock_until:
- 尝试获取锁,最多阻塞到指定时间
加锁有一个缺陷:只要一个线程在对sum++时,其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁
Windows和Linux都各自有相关的原子操作
Windows:
- LONG InterlockedIncrement( LONG volatile* Addend);原子加一
- LONG InterlockedDecrement( LONG volatile* Addend);原子减一
- BOOL CAS(LONG *target, LONG new, LONG old);原子比较交换
- LONG InterlockedExchange( LONG volatile* Target, LONG Value);原子写操作
Linux:
- atomic tv= ATOMIC_ INIT(0); 定义一个原子变量,并初始化
- atomic_dec(&v); 原子变量自减1
- atomic_inc(&v); 原子变量自加1
- atomic_read(&v); 读取原子变量的值
- atomic_ dec and_ test(&v); 原子变量自减1,并与0比较,如果为0则返回true,否则返回false
C++11将原子操作封装为两个类 atomic 和 flag_atomic (跨平台,底层条件编译)
atomic:
- 如bool对应atomic_bool,int对应atomic_int…
- atomic类operator T重载了类型转换符,即
可以使用atomic类模板,定义出需要的任意原子类型
示例:
flag_atomic:
注意:
- 原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了
condition_variable 对象不能被复制/移动(该类型的复制构造函数和赋值运算符都被删除)
wait
成员函数 功能 wait 等待直到通知 wait_for Wait for timeout or until notified wait_untill Wait until notified or time point
template
void wait (unique_lock
& lck, Predicate pred);
摘自:cplusplus
- 在阻塞线程的那一刻,函数自动调用 lck.unlock(),允许其他被锁定的线程继续
- 一旦被通知,该函数会解除阻塞并调用lck.lock(),使 lck 处于与调用函数时相同的状态,然后函数返回(注意最后一个互斥锁可能会在返回之前再次阻塞线程)
- 通常,函数通过调用另一个线程中的成员 notify_one 或成员 notify_all 来通知唤醒,但是可能会存在伪唤醒情况
- 因此,该功能的用户应确保满足其恢复条件, 如果指定了 pred,则该函数仅在 pred 返回 false 时阻塞,并且notify只能在线程变为 true 时解除阻塞
实现类似于: while (!pred()) wait(lck);
notify:
成员函数 功能 notify_one 通知一个 notify_all 通知所有
void notify_one() noexcept;
- 解除阻塞当前等待此条件的线程之一, 如果没有线程在等待,该函数什么也不做,如果在阻塞的线程数量大于1,则唤醒随机线程(线程间进行竞争)
重写题目
:两个线程交替打印奇偶数int main() { mutex mtx; condition_variable cv; bool flag = true; int i = 1; // 打印偶数 thread t2([&i, &mtx, &cv, &flag]() { while (i < 100) { std::unique_lock<mutex> lock(mtx); cv.wait(lock, [&flag]() {return !flag; }); cout << this_thread::get_id() << ":" << i << endl; i++; flag = true; cv.notify_one(); } }); // 打印奇数 thread t1([&i, &mtx, &cv, &flag]() { while (i < 100) { std::unique_lock<mutex> lock(mtx); cv.wait(lock, [&flag]() {return flag; }); cout << this_thread::get_id() << ":" << i << endl; i++; flag = false; cv.notify_one(); } }); t1.join(); t2.join(); return 0; }
分析(这里必须要有pred参数):
- 假如t1先竞争到锁,flag=true,return flag,pred为true,不wait,执行后续语句,假如t1先竞争到锁,flag=true,return !flag,pred为false, wait, 同时调用lck.unlock(),再次开始竞争,所以
一定是t1奇数先打印
- 为了继续打印偶数,而不是连续打印奇数,需要在t1中将flag置为false,同时通知处于cv这个条件变量下的其他处于阻塞的线程(这里只有t2),防止t1连续执行,同时!flag=true, t2得到锁后不wait,开始执行打印偶数部分,之后t2再将flag置为true,通知t1,如此往复
当出现极端情况的时候:某一个线程时间片用完,处于休眠状态,而不是阻塞状态
,notify_one()函数不起作用,但由于有flag,最近执行打印的线程由于pred==false一定不能连续执行,这就做到了交替打印