目录
前言
列表初始化
{ }初始化
initializer_list类
类型推导
auto
decltype
范围for
右值引用与移动语义
左值引用和右值引用
移动语义
1.移动构造
2.移动赋值
3.stl容器相关更新
右值引用和万能引用
完美转发
关键字
default
delete
final和override
可变参数模板
介绍
使用场景
lambda表达式
包装器
bind函数
线程库
后记
C++11 是 C++ 语言的一个重要更新,它加入了许多新的语言特性和标准库组件,旨在提高代码的可读性、可维护性、可移植性和安全性,同时也提高了语言的表达能力和性能。C++11 的引入,对于 C++ 程序员来说是一个里程碑式的事件,它使得 C++ 语言更加现代化和高效。因此我们要作为一个重点去学习。在把本篇文章中,主要介绍一些新增特性,比如花括号初始化、initializer_list类、auto、范围for等,其中较为重要的有右值引用、lambda表达式、线程库,内容较大的特性会专门出一片文章讲解,比如智能指针、异常相关新增特性等,下面来看看上述的详细介绍吧。
C++98允许使用花括号{ }对数组或者结构体元素进行统一的列表初始值设定,C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。列表初始化也可以适用于new表达式中,自定义类型不仅可以通过构造函数使用圆括号构造,也可以使用花括号构造,举例如下代码块。
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;
};
struct A
{
int _a;
int _aa;
};
int main()
{
int a{ 0 };
int arr1[] = { 1,2,3 };
int arr2[] { 1,2,3 };
int arr3[3] = { 0 };
int arr4[3] { 0 };
A a1 = { 1,2 };
A a2 { 1,2 };
int* ptr = new int[3]{ 1,1,1 };
Date d1(1, 2, 3);
Date d2{ 1,2,3 };
return 0;
}
initializer_list类似于数组和向量,可以存储一组数据,并且支持迭代器,可以用于函数参数、构造函数和赋值运算符的参数中。通过使用initializer_list,可以轻松地传递一组数据给一个函数或者对象,而不必显式地指定这组数据的长度或者元素类型。STL中的不少容器就增加 std::initializer_list作为参数的构造函数,比如
eg:
关键字auto在C++中是用于自动类型推导的关键字。当使用auto声明变量时,编译器会根据变量的初始化表达式自动推导出变量的类型。使用auto可以简化代码,特别是当变量类型较长或较复杂时。另外,auto还可以结合迭代器模板等使用,更加灵活和简洁。当auto与&结合说明这是个引用变量,当auto与*结合说明是个指针变量,举例如下:
eg:
decltype是一个关键字,用于获取表达式的类型,而不是用于实例化一个对象,可以用于函数返回值类型推断、模板参数类型推断等,举例:
eg:
C++中的范围for是一种遍历容器、数组、字符串等可迭代对象的简便方法,实际底层就是迭代器遍历。范围for循环通过在循环中声明一个变量,在每次迭代中自动将其设为下一个元素的值来遍历可迭代对象中的元素。
eg:
int arr[] = {1, 2, 3, 4, 5};
for (int x : arr) {
cout << x << " ";
}
// 输出: 1 2 3 4 5
首先,无论是左值引用还是右值引用,都是给对象取别名,要弄明白左值引用和右值引用,先了解一下左值与右值是什么意思。对于左值,可以获取它的地址+对它赋值,注意左值可以出现在赋值符号的左边,也可以出现在右边,左值引用就是对左值的引用,给左值取别名;在此之前所学的引用都是左值引用,左值引用使用一个&符号来声明,比如:
int a = 10; //左值
int* b = new int(1); //左值
const int c = 1; //左值
int& refa = a; //左值引用
int*& refb = b; //左值引用
const int& refc = c; //左值引用
对于右值,不能取地址+不能出现在赋值符号的左边,是一个表示数据的表达式,比如字面常量、表达式返回值等,右值引用就是对右值的引用,给右值取别名,比如:
int x = 0, y = 0;
1; //右值
x + y; //右值
x + 1; //右值
int&& rr1 = 1; //右值引用
int&& rr2 = x + y; //右值引用
int&& rr3 = x + 1; //右值引用
//int&& ref4 = x; //报错
左值引用与右值引用的比较:
①左值引用只能引用左值,不能引用右值,但是const左值引用既可引用左值,也可引用右值;
②右值引用只能右值,不能引用左值,但是右值引用可以move以后的左值;
其中move函数的作用就在于将左值强制转换为右值,比如:
int a = 10;
//int& d = 10; //左值引用引用不了右值
const int& d = 10; //const左值引用可以引用右值
//int&& e = a; //右值引用引用不了左值
int&& e = move(a); //右值引用可以引用move之后的左值
那左值引用用的好好的,为什么要提出右值引用呢?我们想一下左值引用的短板,有这样一个情况,当函数返回值是一个局部变量,出了作用域就会被销毁,就不能使用(左值)引用返回,只能使用传值返回,但是传值返回至少会有一次拷贝构造(即使在编译器优化以后),因此为了减少拷贝,下面考虑其他方法——引入移动构造、移动赋值。
在此之前,先介绍一下右值的分类,包括纯右值(内置类型右值)和将亡值(自定义类型右值),对于纯右值,就算是拷贝多次也无所谓,但是对于有申请资源的将亡值,拷贝一次都是极大地降低了效率,所以考虑将将亡值的资源转给需要的新对象,也就是用将亡值即将不要的资源去构造给需要的对象,这可以大大的减少拷贝,提高效率。
通过下面一个具体的例子描述一下这个过程,在模拟实现string类时,有这样一个int转string的函数,如下图,左边是string的拷贝构造函数,右边是To_string函数传值返回的过程,正常编译器优化情况下,会将str的资源拷贝一份给main函数中的str,但是我们发现这个To_string函数中的str就是一个将亡值,退出函数str就会被释放,而main函数中的str正好是一个需要此资源的新对象,正如上面所说,将To_string函数中的str对象资源移动给main函数中的str,这样就是一次移动构造,如第二图。
对于移动构造函数,我们可以看到,参数是一个右值引用,函数体就是进行资源的交换或者说移动,为什么To_string函数返回之后会调用移动构造函数去构造str呢?因为这里编译器会将To_string函数的局部变量返回值识别成一个右值(将亡值),就会自动去找最匹配的构造函数去构造对象。当没有移动构造函数时,这里就会去调用拷贝构造函数,因为const左值引用也可以接收一个右值,当两个都存在时,存在构造对象的地方会去调用最匹配的构造函数。
不仅有移动构造,还有移动赋值,原理一样,这里也简单说一下,结合在一起较为容易理解。如下图,对于移动赋值函数,参数依旧是右值引用,函数体内也是在不是两个相同的对象赋值的情况以外,交换或移动将亡值的资源给目标对象,实现资源的转移以提高效率。
移动构造和移动赋值不仅解决了传值返回的多次拷贝问题,而且这种资源移动的思想也应用到了stl的容器上,为相关接口增加了右值引用版本,以减少对象的拷贝,如:
eg:
同时,在原本6个默认成员函数的基础上又增加了两个默认成员函数——移动构造函数和移动赋值运算符重载。注意:
①如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任 意一个。那么编译器会自动生成一个默认移动构造;
②如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中 的任意一个,那么编译器会自动生成一个默认移动赋值,
实际上,实现一个类有申请资源时,则得实现拷贝构造、析构、拷贝赋值以进行深拷贝,同时想减少拷贝,就得实现移动构造和移动赋值,但由于有了上面那三个,编译器就不会自动生成,所以还是得自己实现这两个,因此存在属性申请资源时,自己实现拷贝构造、析构、拷贝赋值、移动构造、移动赋值。
万能引用主要有两种,一种是在函数模板中使用的一种引用类型,它的语法形式为“T&&”,其中T是一个模板参数。还有一种是“auto&&”,万能引用可以接受任意类型的实参,并且保留了实参的左右值属性。值得注意的是,必须存在类型推导才是万能引用,否则是右值引用。举例如下图,特别要注意最后一个例子,其中push_back函数得参数虽然是T&&,但是在模板实例化时T的理性就已确定,不存在类型推导,而且在前面也提到过,这个是容器新增得右值引用版本接口,不是万能引用。
eg:
完美转发提供了一种机制来保留函数参数的完整类型信息,并将其转发给另一个函数。传统上,在C++中,当一个函数接收一个参数并将其转发给另一个函数时,它会失去原始参数的类型信息(比如说右值引用版本的接口接收一个右值引用,但是在函数体内这个变量被当作左值去使用,那当我们需要去使用它的右值特性去调用其他相关函数时就没有办法了),此时C++完美转发保留了它的左值或右值的属性。语法如下:
template
void func(T&& arg)
{
other_func(std::forward(arg)); //完美转发
}
下面通过一个例子来展现一下完美转发的使用场景,如下代码是List类的模拟实现,仅包括尾插和插入函数,在mian函数中,尾插一个“1111”的常量字符串,毫无疑问,会匹配右值引用版本的push_back函数,其中需要复用insert函数,而且需要复用右值引用版本的insert函数,但是在push_back函数的函数体内,x已经被当作成了左值,已经失去了“1111”的右值特性,此时使用万能转发保持其属性,继续会匹配右值引用版本的insert函数,在这个函数体内,也需要去调用右值引用版本的Node节点的构造函数,也就是移动构造函数,也必须通过万能引用去操作,在Node的移动构造函数中,我们也需要将右值版本的字符串放进_data中,也是通过万能转发的方法。
从上面的例子当中可以看出,万能转发在实际开发中也是较为需要的,较为重要的。
代码:
template
struct ListNode
{
//构造函数用来创节点
ListNode(const T& x = T()) //左值版本
:_data(x)
, _prev(nullptr)
, _next(nullptr)
{
}
ListNode(T&& x) //右值版本
:_data(forward(x))
, _prev(nullptr)
, _next(nullptr)
{
}
T _data;
ListNode* _prev;
ListNode* _next;
};
template
class List
{
typedef ListNode Lnode;
public:
//...
iterator insert(iterator pos, const T& x) //左值版本
{
Lnode* newNode = new Lnode(x);
pos._node->_prev->_next = newNode;
newNode->_prev = pos._node->_prev;
newNode->_next = pos._node;
pos._node->_prev = newNode;
return iterator(newNode); //返回插入位置的迭代器
}
iterator insert(iterator pos, T&& x) //右值版本
{
Lnode* newNode = new Lnode(forward(x));
pos._node->_prev->_next = newNode;
newNode->_prev = pos._node->_prev;
newNode->_next = pos._node;
pos._node->_prev = newNode;
return iterator(newNode); //返回插入位置的迭代器
}
void push_back(const T& x) //左值版本
{
insert(end(), x);
}
void push_back(T&& x) //右值版本
{
insert(end(), forward(x));
}
private:
Lnode* _head;
};
int main()
{
List lt;
lt.push_back("1111");
return 0;
}
关键字default用于强制生成默认函数,可以更好的控制默认函数。比如,当只有拷贝构造函数时,运行会报错没有默认构造函数,此时使用default强制自动生成即可,如下图
eg:
关键字delete用于禁止生成默认函数,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
eg:
find和override关键字在之前的章节继承和多态中讲过,对于final,即可以修饰类不能被继承,也可以修饰虚函数不能被重写;对于override,放在子类中,检查子类虚函数是否重写了父类的虚函数,具体可见http://t.csdnimg.cn/5CvsAhttp://t.csdnimg.cn/5CvsA
可变参数模板可以让我们编写接受可变数量参数类型的函数和类模板。下面是一个基本可变参数的函数模板,args前面有省略号,称为参数包,其中包含若干个模板参数,我们无法直接获取其中的每个参数,只能展开参数包的方式获取,在C++中,有两种方式展开可变参数模板的参数包:递归函数方式展开和逗号表达式方式展开。
template
void printArgs(Args... args)
{}
递归函数方式展开:
递归展开是指在函数或类模板中递归调用自己,并将参数包展开为独立的参数列表。这可以通过使用递归模板函数或类模板来实现。如下代码,包括递归终止函数和普通展开函数,main函数中的ShowList调用过程为:
①1传进t,其余初步传进args参数包,继续递归调用展开函数;
②'a'传进t,其余传进args参数包,此时参数包只剩一个参数"111"了;
③调用最匹配的函数,即递归终止函数,传进t,之后递归结束,每个参数也最终获取到了。
template
void ShowList(const T& t) //递归终止函数
{
cout << t << endl;
}
template
void ShowList(const T& t, Args... args) //展开函数
{
cout << t << endl; //t就是参数包里的一个参数,这里进行使用即可
ShowList(args...);
}
int main()
{
ShowList(1, 'a', "111");
}
逗号表达式方式展开:
利用初始化列表来初始化一个变长数组,{(printarg(args), 0)...}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... ),最终会创建一个元素值都为0的数组arr,由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args) 获取到当前的参数,也就是说在构造int数组的过程中就将参数包展开了,因此获取到参数包中的所有参数,注意这个数组的目的纯粹是为了在数组构造的过程展开参数包,使用参数的地方是在PrintArg函数中。
template
void PrintArg(T t)
{
cout << t << " ";
}
//展开函数
template
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}
int main()
{
ShowList(1, 'A', "111");
return 0;
}
如果上面的参数包、展开方式你并没有看懂,那就作为了解即可,但是使用场景必须能看得懂,可变参数模板应用在stl容器的emplace相关接口上,比如
可以看到emplace接口参数,既支持模板的可变参数,又是万能引用,也就是同时可以接受左值,也可以接受右值,下面看看如何使用这个接口:其中对于一个元素是pair类的vector,可以直接将一个pair的元素使用emplace_back插入,但是push_back的话就必须去调用make_pair函数。
int main()
{
vector> v;
v.emplace_back("1", 1);
//v.push_back("1", 1); //报错
v.push_back(make_pair("1", 1));
v.push_back({ "1", 1 });
return 0;
}
lambda表达式是一种匿名函数,可以在需要函数对象的任何地方使用。lambda表达式的基本语法如下:
[capture-list] (parameters) mutable -> return-type { function-body }
其中,
捕获列表(capture-list):用于捕获外部变量,该列表总是出现在lambda函数的开始位置,编译器根据[ ]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda 函数使用,每个变量可以指定为按值捕获或按引用捕获,
- [var]:表示值传递方式捕捉变量var,正常情况下可读不可写,加上mutable变成了一份拷贝,就可读可写了
- [=]:表示值传递方式捕获所有所在栈帧的变量(包括this)
- [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有所在栈帧的变量(包括this)
- 由多个捕捉项组成,并以逗号分割
eg:
[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量;
[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量,
参数(parameters):用于传递参数,与普通函数的参数列表一致,如果不需要参数传递,则可以 连同()一起省略;
mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。注意使用该修饰符时,参数列表不可省略(即使参数为空);
返回类型(return-type):用于指定返回值类型,没有返回值时此部分可省略。返回值类型明确情况下也可省略,由编译器对返回类型进行推导;
函数体(function-body):用于实现函数的具体逻辑,可以使用捕获列表的变量也可以使用参数列表的变量,
如下图,fun2就是一个lambda表达式,值传递方式捕获了上文的所有变量,其中b是引用传递,传了一个参数c,返回值是int,这里不写也没事,因为编译器会自动推导,函数体内运算以后返回b,之后调用此lambda表达式,需要传一个参数,即可得到函数体内的计算结果。
注意:
①捕捉列表不允许变量重复传递,否则就会导致编译错误,比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复,就会报错;
②lambda表达式之间不能相互赋值,但可以拷贝构造一个lambda表达式,也可以赋值给相同类型的函数指针,比如
void (*PF)(); int main() { auto f1 = []{cout << "hello world" << endl; }; auto f2 = []{cout << "hello world" << endl; }; //f1 = f2; //报错 auto f3(f1); PF = f2; return 0; }
包装器,也叫适配器,是一种用于以统一的方式调用不同类型函数的抽象概念,本质是一个类模板。在引入lambda表达式之后,有没有这样一个问题,有的接口用函数实现,有的用函数对象实现,还有的用lambda表达式实现,万一有场景需要把这些不同实现方式的接口聚合在一起,该用什么来接收这些接口呢?对!就是使用包装器去接收,看看它的原型:
template
function; template class function ; 其中,Ret: 被调用函数的返回类型,Args…:被调用函数的形参,使用方式我举个例子,实现计算器的加减乘除功能,注意实现以及调用的细节。
int Add(int a, int b)
{
return a + b;
}
class Sub
{
public:
int operator()(int a, int b)
{
return a-b;
}
};
class func
{
public:
int Div(int a, int b)
{
return a / b;
}
};
int main()
{
function ADD = Add; //函数名
function SUB = Sub(); //函数对象
function DIV = &func::Div; //非静态成员函数
function MUL = [](int a, int b) {return a * b; }; //lambda表达式
cout << ADD(1, 2) << endl;
cout << SUB(1, 2) << endl;
cout << DIV(func(), 1, 2) << endl;
cout << MUL(1, 2) << endl;
return 0;
}
运行:
bind函数是一个非常强大的函数对象适配器,它可以把一个函数和一些参数绑定起来,形成一个新的函数对象,该函数对象可以像原函数一样调用,但是它已经部分确定了原函数的参数,同时还可以实现参数顺序调整,原型如下:
template
auto bind(F&& f, Args&&... args);
先看把普通函数和成员函数的一些参数绑定的例子:
int Add(int a, int b)
{
return a + b;
}
class Func
{
public:
int Mul(int a, int b)
{
return a * b;
}
};
int main()
{
//有了两数相加函数,实现任意数加7的功能
auto xPlus7 = bind(Add, placeholders::_1, 7);
cout << xPlus7(1) << endl;
//有了两数相乘的成员函数,实现任意数加倍的功能
auto increaseDouble = bind(&Func::Mul, Func(), placeholders::_1, 2);
cout << increaseDouble(8) << endl;
return 0;
}
运行:
再看调整参数的例子:
double Div(int a, int b)
{
return (double)a / b;
}
int main()
{
auto Divide1 = bind(Div, placeholders::_1, placeholders::_2);
auto Divide2 = bind(Div, placeholders::_2, placeholders::_1);
cout << Divide1(8, 2) << endl;
cout << Divide2(8, 2) << endl;
return 0;
}
运行:
线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态。thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不影响线程的执行。具体细节可参考文档cplusplus.com/reference/thread/thread/,下面介绍常用部分。
创建:
①构造一个没有任何关联对象的线程对象,也就是没有启动任何线程;
②常用创建线程方法,构造线程对象,并关联线程函数fn,后面填入线程函数的参数
eg:
接收:该函数调用后会阻塞主线程,等到该线程结束后,主进程才会继续执行。
eg:
获取线程id:
eg:
当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。 线程函数一般情况下,除了上面用到的函数指针方式,还可以使用lambda表达式、函数对象提供。
eg:
线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参,修改也是改的是线程中的数据,对主线程的数据毫无影响。如果想要通过线程修改主线程中的数据,就必须借助ref函数,或者将数据地址传进线程中修改。
eg:
mutex是C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动。mutex最常用的三个函数:
lock():加锁。如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前, 该线程一直拥有该锁;如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。
trylock:尝试加锁,与lock不同的是,如果当前互斥量被其他线程锁住时,当前线程不会阻塞而是返回false。
unlock():解锁。
eg:
除此之外,c++11还提供了另外3个互斥量的种类,与mutex不同的地方在于:
recursive_mutex:允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权, 释放互斥量时需要调用与该锁层次深度相同次数的 unlock();
timed_mutex:多了两个成员函数——try_lock_for(),try_lock_until()。其中前者是有锁直接锁,没锁会等一个时间段,还没锁才会返回false,后者也是有锁直接锁,但没锁会等到一个时间点,还没锁才会返回false。
recursive_timed_mutex:前两个种类的mutex的结合。
锁控制不好时,可能会造成死锁,最常见的比如在锁中间代码返回,或者在锁的范围内抛异常。因此C++11采用了RAII的方式对锁进行了封装,即lock_guard和unique_lock。
lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封装,在需要加锁的地方,调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题。 但是lock_guard的缺陷很明显:太单一,用户没有办法对该锁进行控制。
与lock_guard不同的是,unique_lock更加的灵活,提供了更多的成员函数:如
上锁/解锁操作:lock、try_lock、try_lock_for、try_lock_until和unlock;
修改操作:移动赋值、交换、释放;
获取属性:owns_lock(返回当前对象是否上了锁)、operator bool()、mutex(返回当前unique_lock所管理的互斥量的指针)等,
也就是在加锁以后,用户还是可以对锁进行控制,更加的灵活。
eg:
如上图,当n足够大时,得到的sum的值是不正确的,因为sum是共享数据,会带来线程安全问题,当多个线程修改共享数据时,就会产生预料之外的结果。在c++11以前,我们可以对共享修改的数据进行加锁保护,但是实践证明多次加锁解锁会极大的影响线程切换或者整机的效率。并且如果锁控制不好,还容易造成死锁。
因此,在c++11中引入了原子操作,这是一个不可被中断的一个过程。在添加头文件
的前提下,有两种方式,一是根据下图原子类型名称去定义变量,此变量在被修改时就不会被中断;二是使用类模板(atomic t)定义任意原子类型。 对于atomic类模板值得注意的是,在C++11中原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及 operator=等,并且标准库已经将这些函数默认删除掉了。
eg:
condition_variable是C++标准库中的一个同步原语,用于线程间的同步和通信。它通常与mutex一起使用,用于实现线程的等待和唤醒操作。condition_variable的主要作用是在某个条件满足时唤醒一个或多个等待线程。它提供了以下几个成员函数:
- wait(lock):让当前线程等待,直到另一个线程调用notify_one()或notify_all()唤醒它。调用wait()时需要传入一个lock参数,这个参数是一个unique_lock类型的互斥锁,用来保护共享变量的访问。在调用wait()之前,必须先获得这个互斥锁。
其中,lck是互斥锁,pred就是所谓的“条件”,具体用法看下面的例子。
- notify_one():唤醒一个等待线程,如果没有等待线程,则什么也不做。
- notify_all():唤醒所有等待线程。
通常的用法是,线程在条件不满足时调用wait()阻塞自己,直到另一个线程满足条件调用notify_one()或notify_all()唤醒它。在唤醒后,线程会再次尝试获取互斥锁,并继续执行。
condition_variable还提供了一些其他的成员函数和类型,如wait_for()和wait_until(),用于等待一段时间或直到某个时间点。
condition_variable是实现线程间同步和通信的重要工具,它可以解决一些典型的多线程问题,如生产者-消费者模型、读写锁等。但它并不能单独完成所有的任务,通常还需要与其他的同步原语如mutex、unique_lock一起使用,以实现更复杂的操作。更多接口介绍参考cplusplus.com/reference/condition_variable/condition_variable/
以下实现一个程序,支持两个线程交替打印1-100,一个打印奇数,一个打印偶数。可以看到,使用lambda表达式的方式创建了两个线程,假设我们就让t1打印奇数,t2打印偶数。先看主框架,两个线程都是循环从1打印到100,在修改共享资源时进行加锁保护,这些都没毛病,那如何实现交替打印的效果呢?
首先,从头开始看起,两个线程同时执行,无论t1还是t2先执行,t1要打印奇数,所以遇到偶数就阻塞挂起,t2要打印偶数,所以遇到奇数就阻塞挂起,这就是条件变量中的”条件“一词所体现的意义。它俩只有一方会阻塞挂起,因为有锁保证这一点,当一方在阻塞挂起时,另一方在访问总共资源,并且访问结束会执行notify_one函数唤醒正在阻塞的线程,之后两个线程会继续执行,循环往复。
代码:
int main()
{
int i = 1;
int n = 100;
mutex mtx;
condition_variable cv;
//打印奇数
thread t1([&](){
while (i < n)
{
unique_lock lck(mtx);
//while (i % 2 == 0) //方法一
//cv.wait(lck);
cv.wait(lck, [&]() {return i % 2 != 0; }); //方法二
//两者结果一致,wait函数第二个参数填while条件的逻辑非语句
cout < lck(mtx);
//while (i % 2 != 0)
//cv.wait(lck);
cv.wait(lck, [&]() {return i % 2 == 0; });
cout << this_thread::get_id() << ":" << i << endl;
i++;
cv.notify_one();
}
});
cout << "t1:" << t1.get_id() << " t2:" << t2.get_id() << endl;
t1.join();
t2.join();
return 0;
}
执行:
从以上可以看出,c++11新增的知识点还是特别多的,本文章只是讲述了较为重要的一部分,面试时被提问频率高的一部分,还有一部分没有提到,比如新增容器(如array),空指针nullptr,有一些大家可能已经熟练于心了,对于文中讲过的知识点,其中包括范围for、右值引用、lambda表达式都是重点中的重点,希望大家能够真正的看懂并理解,拜拜!