在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于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++98中,标准允许使用花括号{}对数组元素进行统一的列表初始值设定。比如:
对于一些自定义的类型,却无法使用这样的初始化。比如:
vector<int> v{1,2,3,4,5};
就无法通过编译,导致每次定义vector时,都需要先把vector定义出来,然后使用循环对其赋初始值,非常不方便。C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
注意:列表初始化可以在{}之前使用等号,其效果与不使用=没有什么区别。
template<class T>
class Vector
{
public:
Vector()
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{}
Vector(initializer_list<T> l)
{
_start = new T[l.size()];
_finish = _start + l.size();
_endofstorage = _start + l.size();
int i = 0;
for (auto e : l)
{
_start[i++] = e;
}
}
Vector<T>& operator=(initializer_list<T> l)
{
delete _start;
_start = new T[l.size()];
_finish = _start + l.size();
_endofstorage = _start + l.size();
int i = 0;
for (auto e: l)
{
_start[i++] = e;
}
return *this;
}
~Vector()
{
delete _start;
_start = _finish = _endofstorage = nullptr;
}
private:
T* _start;
T* _finish;
T* _endofstorage;
};
在定义变量时,必须先给出变量的实际类型,编译器才允许定义,但有些情况下可能不知道需要实际类型怎么给,或者类型写起来特别复杂,比如:
C++11中,可以使用auto来根据变量初始化表达式类型推导变量的实际类型,可以给程序的书写提供许多方便。将程序中c与it的类型换成auto,程序可以通过编译,而且更加简洁。关于auto的详细介绍可以参考C++初阶课件。
auto使用的前提是:必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型。但有时候可能需要根据表达式运行完成之后结果的类型进行推导,因为编译期间,代码不会运行,此时auto也就无能为力
template<class T1, class T2>
T1 Add(const T1& left, const T2& right)
{
return left + right;
}
如果能用加完之后结果的实际类型作为函数的返回值类型就不会出错,但这需要程序运行完才能知道结果的实际类型,即RTTI(Run-Time Type Identification 运行时类型识别)。
C++98中确实已经支持RTTI:
运行时类型识别的缺陷是降低程序运行的效率。
decltype是根据表达式的实际类型推演出定义变量时所用的类型,比如
在C++中对于空类编译器会生成一些默认的成员函数,比如:构造函数、拷贝构造函数、运算符重载、析构函数和&和const&的重载、移动构造、移动拷贝构造等函数。如果在类中显式定义了,编译器将不会重新生成默认版本。有时候这样的规则可能被忘记,最常见的是声明了带参数的构造函数,必要时则需要定义不带参数的版本以实例化无参的对象。而且**有时编译器会生成,有时又不生成,容易造成混乱,于是C++11让程序员可以控制是否需要编译器生成。
**
在C++11中,可以在默认函数定义或者声明时加上=default,从而显式的指示编译器生成该函数的默认版本,用=default修饰的函数称为显式缺省函数。
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且不给定义,这样只要其他人想要调用就会报错。*在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数 *
C++98中提出了引用的概念,引用即别名,引用变量与其引用实体公共同一块内存空间,而引用的底层是通过指针来实现的,因此使用引用,可以提高程序的可读性。
为了提高程序运行效率,C++11中引入了右值引用,右值引用也是别名,但其只能对右值引用
为了与C++98中的引用进行区分,C++11将该种方式称之为右值引用。
左值与右值是C语言中的概念,但C标准并没有给出严格的区分方式,一般认为:可以放在=左边的,或者能够取地址的称为左值,只能放在=右边的,或者不能取地址的称为右值,但是也不一定完全正确。
#include
using namespace std;
int Add(int a, int b)
{
return a + b;
}
int main()
{
//左值
// a和b都是左值,b既可以在=的左侧,也可在右侧,
// 说明:左值既可放在=的左侧,也可放在=的右侧
int a = 10;
int b = 20;
a = b;
b = a;
const int c = 30;
// 编译失败,c为const常量,只读不允许被修改
//c = a;
// 因为可以对c取地址,所以c是左值
const int* p = &c;
//右值
//
//常量
10;
//返回的临时变量
a + b;
Add(10, 20);
return 0;
}
因此关于左值与右值的区分不是很好区分,一般认为:
总结:
C++11对右值进行了严格的区分:
在C++98中的普通引用与const引用在引用实体上的区别:
#include
using namespace std;
int main()
{
// 普通类型引用只能引用左值,不能引用右值
int a = 10;
int& ra1 = a; // ra为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
注意: 普通引用只能引用左值,不能引用右值,const引用既可引用左值,也可引用右值。
C++11中右值引用:只能引用右值,一般情况不能直接引用左值。
#include
using namespace std;
int main()
{
// 10纯右值,本来只是一个符号,没有具体的空间,
// 右值引用变量r1在定义过程中,编译器产生了一个临时变量,r1实际引用的是临时变量
int&& r1 = 10;
r1 = 100;
int a = 10;
//int&& r2 = a; // 编译失败:右值引用不能引用左值
int&& r3 = move(a);//通过move后右值引用可以引用左值
return 0;
}
问题:既然C++98中的const类型引用左值和右值都可以引用,那为什么C++11还要复杂的提出右值引用呢?
如果一个类中涉及到资源管理,用户必须显式提供拷贝构造、赋值运算符重载以及析构函数,否则编译器将会自动生成一个默认的,如果遇到拷贝对象或者对象之间相互赋值,就会出错,比如:
#include
#include
using namespace std;
namespace ts
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
//cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 资源转移" << endl;
this->swap(s);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
this->swap(tmp);
return *this;
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string s) -- 资源转移" << endl;
swap(s);
return *this;
}
~string()
{
//cout << "~string()" << endl;
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
//string operator+=(char ch)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string operator+(char ch)
{
string tmp(*this);
push_back(ch);
return tmp;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
}
// 场景1
// 左值引用做参数,基本完美的解决所有问题
void func1(ts::string s)
{}
void func2(const ts::string& s)
{}
// 场景2
// 左值引用做返回值,只能解决部分问题
// string& operator+=(char ch) 解决了
// string operator+(char ch) 没有解决
// 右值引用,如何解决operator+传值返回存在拷贝的问题呢?
// C++11 将右值分为:纯右值,将亡值
//返回的时候调用移动构造,减少拷贝
ts::string func3()
{
ts::string ret("hello");
return ret;
}
C++11提出了移动语义概念,即:将一个对象中资源移动到另一个对象中的方式,可以有效缓解该问题。
没有移动构造
有移动构造
没有移动赋值
有移动赋值
注意:
没有实现拷贝构造,赋值和析构
自己实现了拷贝构造赋值和析构当中的任意一种
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。
注意:以上代码是move函数的经典的误用,因为move将s1转化为右值后,在实现s2的拷贝时就会使用移动构造,此时s1的资源就被转移到s2中,s1就成为了无效的字符串
完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。
#include
using namespace std;
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);//t的属性变为了左值
}
int main()
{
PerfectForward(10);//右值
int a;
PerfectForward(a);//左值
PerfectForward(move(a));//右值
const int b = 8;
PerfectForward(b);//const左值
PerfectForward(move(b));//const 右值
return 0;
}
PerfectForward为转发的模板函数,Func为实际目标函数,但是上述转发还不算完美,完美转发是目标函数总希望将参数按照传递给转发函数的实际类型转给目标函数,而不产生额外的开销,就好像转发者不存在一样。
所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)。
在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法。
如果待排序元素为自定义类型,需要用户定义排序时的比较规则:
随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法, 都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C11语法中出现了Lambda表达式
#include
#include
#include
#include
using namespace std;
struct Goods
{
string _name;
double _price;
};
struct Compare
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price <= gr._price;
}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1 }, { "相交", 3 }, { "橙子", 2.2 }, {"菠萝", 1.5} };
//sort(v.begin(), v.end(), Compare());//仿函数
//lambda表达式
//auto price = [](const Goods& g1, const Goods& g2) {return g1._price < g2._price; };
//sort(v.begin(), v.end(), 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._price > g2._price; });
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;
}
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }
注意: 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
通过上述例子可以看出,lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。
注意:
a. 父作用域指包含lambda函数的语句块
b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量 [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
d. 在块作用域以外的lambda函数捕捉列表必须为空。
e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错
f. 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);
// lamber
auto r2 = [=](double monty, int year)->double{return monty*rate*year; };
r2(10000, 2);
return 0;
}
从使用方式上来看,函数对象与lambda表达式完全一样
函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到。
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。
C++11的新特性–可变模版参数(variadic templates)是C++11新增的最强大的特性之一,它对参数进行了高度泛化,它能表示0到任意个数、任意类型的参数。相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。
可变参数模板和普通模板的语义是一样的,只是写法上稍有区别,声明可变参数模板时需要在typename或class后面带上三个点“…”。比如我们常常这样声明一个可变模版参数:template
template <class... T>
void f(T... args);
递归函数展开参数包是一种标准做法,也比较好理解,但也有一个缺点,就是必须要一个重载的递归终止函数,即必须要有一个同名的终止函数来终止递归,这样可能会感觉稍有不便。有没有一种更简单的方式呢?其实还有一种方法可以不通过递归方式来展开参数包,这种方式需要借助逗号表达式和初始化列表。可以改成这样:
vector中的push_back和emplace_back有什么不同呢?
c++提供了多个包装器(也叫适配器)。这些对象用于给其他编程接口提供更一致或更合适的接口。
模板function是在头文件functional头文件中声明的,他从参数和返回值的角度定义了一个对象,可用于包装调用参数和返回值相同的函数指针、伪函数或lambda表达式
由于函数调用可以使用函数名、函数指针、函数对象或有名称的lambda表达式,可调用类型太丰富导致模板的效率极低。
#include
#include
using namespace std;
int f(int a, int b)
{
return a + b;
}
struct Functor
{
public:
int operator()(int a, int b)
{
return a + b;
}
};
struct Plus
{
public:
static int plus(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
function<int(int, int)> f1 = f;
cout << f1(1, 2) << endl;
function<int(int, int)> f2 = Functor();
cout << f2(3, 4) << endl;
function<int(int, int)> f3 = &Plus::plus;
cout << f3(5, 6) << endl;
//成员函数需要多传一个参数,加上&运算符
function<double(Plus, double, double) > f4 = &Plus::plusd;
cout << f4(Plus(),1.1, 2.2) << endl;
return 0;
}
class Solution {
public:
int evalRPN(vector<string>& tokens) {
map<string, function<int(int,int)>> opFuncMap =
{
{"+", [](int a, int b) -> int { return a + b; } },
{"-", [](int a, int b) -> int { return a - b; } },
{"*", [](int a, int b) -> int { return a * b; } },
{"/", [](int a, int b) -> int { return a / b; } },
};
stack<int> s;
for(int i = 0; i < tokens.size(); ++i)
{
string str = tokens[i];
//操作数入栈
if(opFuncMap.find(str) == opFuncMap.end())
{
s.push(stoi(str));
}
else
{
//操作符
int right = s.top();
s.pop();
int left = s.top();
s.pop();
s.push(opFuncMap[str](left, right));
}
}
return s.top();
}
};
td::bind用来将可调用对象与其参数一起进行绑定。绑定后的结果可以使用std::function进行保存,并延迟调用到任何我们需要的时候。通俗来讲,它主要有两大作用:
#include
#include
using namespace std;
int sub(int a, int b)
{
return a - b;
}
class Func
{
public:
int add(int a, int b)
{
return a + b;
}
};
int main()
{
//调整参数顺序
function<int(int, int)> f1 = sub;
cout << f1(1, 2) << endl;
function<int(int, int)> f2 = bind(sub, placeholders::_2, placeholders::_1);
cout << f2(1, 2) << endl;
//改变参数个数
function<int(Func, int, int)> f3 = &Func::add;
cout << f3(Func(), 1, 2) << endl;
function<int(int, int)> f4 = bind(&Func::add, Func(), placeholders::_1, placeholders::_2);
cout << f4(1, 2) << endl;
return 0;
}
placeholders::_1 是一个占位符,代表这个位置将在函数调用时被传入的第一个参数所替代。同样还有其他的占位符 placeholders::_2、placeholders::_3、placeholders::_4、placeholders::_5 等……
在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread >头文件。
Linux线程库:
pthread线程库:(POSIX)
1.创建线程:
2.回收线程(pthread_join):
pthread_join(pthread_t thread, void **retval)
多线程会引发线程安全的问题。看下面例子:
可以通过加锁解决:
也可以使用原子操作:
get_id()的返回值类型为id类型,id类型实际为std::thread命名空间下封装的一个类,该类中包含了一个结构体
// vs下查看
typedef struct
{ /* thread identifier for Win32 */
void* _Hnd; /* Win32 HANDLE */
unsigned int _Id;
} _Thrd_imp_t;
thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行。
可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效:
采用无参构造函数构造的线程对象
线程对象的状态已经转移给其他线程对象
线程已经调用jion或者detach结束
启动了一个线程后,当这个线程结束的时候,如何去回收线程所使用的资源呢?thread库给我们两种选择
多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。
因此C++11中引入了原子操作。所谓原子操作:即不可被中断的一个或一系列操作,C++11引入的原子操作类型,使得线程间数据的同步变得非常高效
注意:原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了。
在C++11中,Mutex总共包了四个互斥量的种类:
线程函数调用try_lock()时,可能会发生以下三种情况
try_lock_for()
try_lock_until()
在多线程环境下,如果想要保证某个变量的安全性,只要将其设置成对应的原子类型即可,即高效又不容易出现死锁问题。但是有些情况下,我们可能需要保证一段代码的安全性,那么就只能通过锁的方式来进行控制。
#include
#include
#include
#include
using namespace std;
void func(vector<int>& vthd, int n, int base, mutex& mtx)
{
try
{
for (int i = 0; i < n; ++i)
{
mtx.lock();
//cout << this_thread::get_id() << endl;
vthd.push_back(base + i);
if (base == 100 && i == 140)
throw bad_alloc();
mtx.unlock();
}
}
catch (const exception& e)
{
cout << e.what() << endl;
mtx.unlock();//捕捉之后要释放锁
}
}
int main()
{
vector<int> vthd;
mutex mtx;
thread t1(func, ref(vthd), 1000, 100, ref(mtx));
thread t2(func, ref(vthd), 3000, 200, ref(mtx));
t1.join();
t2.join();
for (auto e : vthd)
{
cout << e << " ";
}
cout << endl;
cout << vthd.size() << endl;
return 0;
}
述代码的缺陷:锁控制不好时,可能会造成死锁,最常见的比如在锁中间代码返回,或者在锁的范围内抛异常。因此:C++11采用RAII的方式对锁进行了封装,即lock_guard和unique_lock
#include
#include
#include
#include
using namespace std;
void func(vector<int>& vthd, int n, int base, mutex& mtx)
{
try
{
for (int i = 0; i < n; ++i)
{
lock_guard<mutex> lck(mtx);//只提供构造函数和析构函数,出了作用域就会自动释放锁
unique_lock<mutex> ulck(mtx);//除了提供构造和析构函数,还提供了其它函数
//cout << this_thread::get_id() << endl;
vthd.push_back(base + i);
if (base == 100 && i == 140)
throw bad_alloc();
mtx.unlock();
}
}
catch (const exception& e)
{
cout << e.what() << endl;
}
}
int main()
{
vector<int> vthd;
mutex mtx;
thread t1(func, ref(vthd), 1000, 100, ref(mtx));
thread t2(func, ref(vthd), 3000, 200, ref(mtx));
t1.join();
t2.join();
for (auto e : vthd)
{
cout << e << " ";
}
cout << endl;
cout << vthd.size() << endl;
return 0;
}
#include
#include
#include
#include
using namespace std;
int main()
{
int n = 100;
mutex mtx;
int i = 0;
condition_variable cv;
bool flag = false;
thread t1([n, &i, &mtx, &cv, &flag] {
while (i < n)
{
unique_lock<mutex> ulck(mtx);
cv.wait(ulck, [&flag]() {return !flag; });
cout << this_thread::get_id() << "->" << i << endl;
++i;
flag = true;
cv.notify_one();
}
});
thread t2([n, &i, &mtx, &flag, &cv] {
while (i < n)
{
unique_lock<mutex> ulck(mtx);
cv.wait(ulck, [&flag]() {return flag; });
cout << this_thread::get_id() << "->" << i << endl;
++i;
flag = false;
cv.notify_one();
}
});
t1.join();
t2.join();
return 0;
}