C++11

C++11

  • C++11简介
  • 列表初始化
    • {}初始化
    • std::initializer_list
  • auto
  • decltype
  • nullptr
  • 范围for循环
  • 右值引用和移动语义
    • 左值、右值的概念
    • 右值引用与左值引用的区别
    • 右值引用使用场景和意义
    • 移动构造和移动赋值
    • 右值引用引用左值及其一些更深入的使用场景分析
    • 完美转发
  • 强制生成默认函数的关键字default
  • 禁止生成默认函数的关键字delete
  • 可变参数模板
  • 线程库
    • thread类的简单介绍
    • 线程函数的参数
    • 原子性操作库(atomic)
    • C++11互斥锁
    • 互斥锁的种类
    • lock_guard与unique_lock
    • 两个线程交替打印,一个打印奇数,一个打印偶数
  • 包装器

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标准中,标准允许利用花括号(“{ }”)对数组或struct 结构体进行统一的列表初始值设定进行初始化,比如:
C++11_第1张图片
C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型(注意要把成员变量的权限放出来),使用初始化列表时,可添加等号(=),也可不添加:
C++11_第2张图片
在实例化对象的时候,我们也可以利用列表初始化的方式来调用构造函数:
C++11_第3张图片
那么这与我们的直接使用列表初始化有什么区别呢?
当我们的结构体中实现了任何一个构造函数或者将成员变量的权限设为私有或保护的,那么编译器就不在允许我们直接使用列表初始化的方式来对结构体进行初始化了,而是会尝试用与{ }里面的参数比较匹配的构造函数来进行初始化,如果没有合适的构造函数,那么编译器就会报错;

std::initializer_list

initializer_list是C++11新增的一种类型,它可以用来表示一个初始化列表(“{ }”),其中包含多个同类型的值。初始化列表可以用于初始化STL容器对象、用户自定义的类等。在C++11中,初始化列表还被用来初始化变长参数函数中的参数。它常用于简化容器初始化并提高程序的性能。使用initializer_list可以帮助我们更方便、更高效地初始化对象。
eg:
C++11_第4张图片
我们可以看到{1,2,3,4}就是一个类型,因此如果我们以后自己设计的容器想像数组一样进行初始化的化,那么我们可以写一个参数为initializer_list< T>的构造函数;
在STL中的大多数容器中,都提供了这个构造函数,比如vector、list、map、set等:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
因此,对于vector、list、map、set等容器我们就可以像数组一样进行初始化了:
C++11_第5张图片

auto

在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局
部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将
其用于实现自动类型腿断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初
始化值的类型。
C++11_第6张图片
auto还可以用来当作函数的返回值类型,表示根据返回值来自动推导返回值的类型;
C++11_第7张图片
但是auto不能用来自动推导函数的参数类型:
C++11_第8张图片

decltype

可以用于获取一个变量的类型,并且可以使用该类型进行定义变量,与typeid(变量).name()的功能相似,typeid获取出来的类型是字符串,不能用来定义变量,但是用decltype获取出来的类型是可以定义变量的,比如:
C++11_第9张图片
对比结果:
C++11_第10张图片

nullptr

在C++中NULL表示空指针,但是NULL本质上是一个宏:
在这里插入图片描述
所以NULL本质上是一个int类型,而不是指针类型,对于理解这一点的程序员们来说,还比较友善,但是对于不知道这一点的程序员们来说,在某些情况下他们就会把NULL当作指针类型来使用,这回照成一些bug的!比如现在我不知道NULL本质上是一个宏,而是将其当作指针来使用:
C++11_第11张图片
我们可以很明显的发现,由于我们对于NULL的错误认识,导致Func(NULL)结果与我们预期不相符合,这是我们不希望看到的,C++11呢为了解决像这样由于误会引起的bug,就提出了nullptr关键字来替换NULL,nullptr才是真正的空指针,才具有指针属性:
C++11_第12张图片
这样就完美的避开了由于误会而引起的不必要的bug,在C++学习中呢,我们也更推荐使用nullptr来表示空指针;

范围for循环

就是一个简版的for循环,底层还是用的迭代器来实现的:
比如:
C++11_第13张图片
相比于传统的写法简洁了不少,要想使用范围for,有一个前提,那就是必须实现迭代器!

右值引用和移动语义

在传统C++语法中就有引用的的语法了,而C++11语法中又新增了右值引用语法特性,为此从现在开始,我们以前学的引用都叫做左值引用,但是记住,无论是左值引用还是右值引用,他们都是引用,只要是引用,那么就是给一块空间取别名,没有空间?编译器自动给你分一块空间,然后再将需要引用的数据放进去,然后在对这块空间进行取别名!
这就是引用的核心观点!
那么左值引用和右值引用之间有什么却别?
要想弄清楚这一点,那么我们就需要先弄清楚什么是左值什么是右值?

左值、右值的概念

左值: 可以取地址的就是左值!比如:变量、字符串等等;
C++11_第14张图片
什么是左值引用?
就是对于给左值取别名,格式是:类型+&+别名=被引用的左值
比如:C++11_第15张图片

右值: 不可以取地址的就是右值!比如:字面常量(‘a’、1、3.14)、表达式、宏、值返回的函数的返回值等;C++11_第16张图片
那么什么又是右值引用?
右值引用就是给右值取别名,格式:类型+&&+别名=右值
C++11_第17张图片

右值引用与左值引用的区别

对于左值引用来说:

  1. 一般情况下左值引用只能引用左值,不能引用右值;
  2. 如果非要用左值引用来引用右值,那么const左值引用可以用来引用右值;
    C++11_第18张图片

对于右值引用来说:

  1. 一般情况下右值引用只能引用右值,不能引用左值;
  2. 如果非要使用右值引用来引用左值,那么可以通过move(左值)函数来暂时修改左值的左值属性,让其暂时变为右值属性,然后再用右值引用来引用;
    C++11_第19张图片

总结:

  1. 无论是左值引用还是右值引用本质上都是引用,只要是引用,那么就是对"空间"取别名,如果没有空间,那么编译器会自动开辟一块空间,然后把数据放进去,然后再让你对这块空间进行引用:
    比如:
    C++11_第20张图片
  2. 其实上面的例子还验证了一个观点,那就是无论是左值引用还是右值引用,只要是被引用过后的空间其属性都会变为左值属性!比如上诉的:int&&b=10;这是右值引用吧,可是我们通过对别名b取地址,却取出来了地址,按照我们之前的理论,能取地址的就是左值,那么b所引用的那块空间其属性再被引用过后变为了左值!
  3. 左值引用来引用左值是天然的、合法的,但是左值引用来引用右值是不会改变右值的常性的,因此我们需要加const才能引用右值;对于右值引用来引用右值,也是天然的,但是再被右值引用过后右值的常性会被取消;同理右值引用再引用左值的时候,会由于左值的属性不对而无法引用,我们只需要用move函数来暂时改变左值的左值属性即可被右值引用所引用;
  4. 右值可以分为:纯右值(字面常量)、将亡值(临时对象、匿名对象),这些右值都具有常性;const左值引用和右值引用都能延长将亡值的生命周期!

右值引用使用场景和意义

前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引
用呢?是不是化蛇添足呢?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!

移动构造和移动赋值

确实,左值引用的提出确实提高了效率,也确实减少了拷贝,比如:我们在传参的时候可以用左值引用来作为参数,以此来减少拷贝次数,提高效率:
C++11_第21张图片
这样的话就大大提高了我们的效率!
可是,左值引用却无法解决函数返回值为值传递的情况,可能语言上并没有表述清除,我们用实际的代码来解释:
C++11_第22张图片
针对上述的Func函数来说,它的返回值是以值传递的形式来进行返回的,那么必然设计到拷贝;
我们先来讨论编译器不优化的过程:
在这里插入图片描述
上面的过程编译器看了都嫌麻烦,于是编译器对于上述动作做了优化,将两个拷贝构造直接优化成立一个拷贝构造,直接用str来拷贝构造s1,然后再销毁str,因此整个过程就只发生了一次拷贝:
C++11_第23张图片
这个我们可以通过运行结果来验证:
C++11_第24张图片
以上是编译器的常规作法;
可是我们有没有发现,尽管最后只需要一次拷贝了,可是这是深拷贝唉,还是很花费时间,况且str立马就要被销毁了,为何不把str所维护的资源直接交给s1呢?这样的话s1对象也就不用再进行深拷贝了,直接就获得了资源,何乐而不为?
为此,要实现上面的办法,我们可以通过移动构造函数来实现,这是C++11新提出来的一种构造函数,那么什么是移动构造?移动构造就是参数为本身类型的右值引用:
比如本立中string中的移动构造就是:
C++11_第25张图片
而右值引用引用的是那些对象呢?就是右值嘛,细分一下的话就是纯右值和将亡值,但是无论是哪一种,都可以说明右值引用所引用的对象再后续的操作中都不会被使用到或者再后续的操作中就消亡了,那么它们反正在后续的操作中都使用不到了,那么我们为何不把它们的资源给掠夺过来,提高资源使用效率呢?同时也可以减少一些拷贝这就是移动构造的本质,为此根据这个信息我们可以编写出本例中string的移动构造函数:
C++11_第26张图片
接着我们回到本例中,上面我们不是说编译器优化过后,还是要进行一次拷贝比较麻烦了,现在有了移动构造过后,就连最后一次拷贝也省略掉了;
C++11_第27张图片
你看这样是不是就大大的提高了效率,可是编译器是如何做到的呢?
首先我们先来看一看正常情况:
C++11_第28张图片
可是编译器一看,这怎么还有一份深拷贝,能不能省略掉?
为此编译器为了省略掉这一次深拷贝,就暂时性的将str的左值属性改为了右值属性,因此编译器就直接用了str来调用s1的移动构造来初始化s1;为此全过程中就彻底断绝了拷贝,大大的提高了效率!
C++11_第29张图片
当然既然有移动构造,那么移动赋值也是少不了的,移动赋值与移动构造的思想都是一样的,都是通过掠夺右值的资源来减少拷贝;
我们简单实现一下string的移动赋值:
C++11_第30张图片

实际上,编译器也会自动生成默认的移动构造和移动赋值,只不过需要一定的条件;
默认移动构造:当用户没有自己实现移动构造、拷贝构造、赋值运算符重载、析构函数时,则编译器会自己生成一个默认的移动构造;
移动赋值:当用户没有自己实现移动构造、拷贝构造、赋值运算符重载、析构函数时,则编译器会自己生成一个默认的移动赋值;

右值引用引用左值及其一些更深入的使用场景分析

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能
真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move
函数将左值转化为右值。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性,
它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义
C++11_第31张图片
C++11_第32张图片
可是以上的行为具有一定的危险性,上面我们说了移动构造的思想就是掠夺别人的资源,s2在利用s1进行移动构造过后,s1的资源就没有了,要是后续还需要使用s1,那么就有麻烦了,为此我们一般不建议给一个左值转为右值然后再调用移构造或移动赋值,而是直接用匿名对象直接进行移动构造,反正匿名对象再过了当前行就会被销毁,不用担心匿名对象对于后续操作的影响:
C++11_第33张图片

完美转发

模板中的&&表示万能引用:
C++11_第34张图片
模板中的&&并不是表示右值引用,而是万能引用;
&&表示T的类型是个引用,至于是左值引用还是右值引用,根据传入的参数决定,如果我们传入的参数是右值,那么T就是右值引用;如果我们传入的参数是左值,那么T就是左值引用;

因此根据以上信息,那么我们来看一段代码:

void fun(int& t)
{
	cout << "void fun(int& t)" << endl;
}
void fun(const int& t)
{
	cout << "void fun(const int& t)" << endl;
}void fun(int&& t)
{
	cout << "void fun(int&& t)" << endl;
}void fun(const int&& t)
{
	cout << "void fun(const int&& t)" << endl;
}
template<typename T>
void PerfectForward(T&& t)
{
	fun(t);
}
void test2()
{
	PerfectForward(10);
	int a = 10;
	PerfectForward(a);
	PerfectForward(move(a));
	const int b = 10;
	PerfectForward(b);
	PerfectForward(move(b));
}

我们可以来做一做这道题:
正常思路:
首先第一个PerfectForward(10),10是纯右值,因此T是右值引用,因此fun应该打印:void fun(int&& t);
其次第二个PerfectForward(a),a是左值,因此T是左值引用,因此fun应该打印:void fun(int& t);
其次第三个PerfectForward(move(a)),a的左值属性暂时性被改为了右值,因此T是右值引用,因此fun应该打印:void fun(int&& t);
其次第四个PerfectForward(b),b是const 左值,因此T是const 左值引用,因此fun应该打印:void fun(const int& t);
其次第五个PerfectForward(move(b)),b是const左值然后经过move过后暂时的变为了const右值,因此T是const右值引用,因此fun应该打印:void fun(constint&& t);
可是真的是这样吗?
C++11_第35张图片
我们可以发现,结果与我们的预期不太符合;
为什么?
主要是因为右值在经过右值引用引用过后,其右值属性会退化成左值,因此第一个PerfectForward(10),10是纯右值,因此T是右值引用,到这里我们分析的没问题,可是离开了参数哪一行,t的右值属性就开始退化成了左值属性,因此fun在调用的时候就会调用void fun(int& t);
同理第三个PerfectForward(move(a)),a的左值属性暂时性被改为了右值,因此T是右值引用至此也是正确的,可是一旦离开参数哪一行,t的右值属性就开始退化,退化成左值,因此fun调用的就是void fun(int& t);
同理第五个PerfectForward(move(b)),b是const左值然后经过move过后暂时的变为了const右值,因此T是const右值引用,可是在离开参数一行过后t就变为了const左值,因此fun调用的就是void fun(const int& t);

那么如何解决这样的问题?或者说如何才能让其正确的进行调用呢?
C++给我们提供了一个函数std::forward来解决这个问题,std::forward函数可以暂时的保持传入参数的原有属性;
如果原有参数是左值引用,那么转发过后也就是左值引用;
如果原有参数是const左值引用,那么转发过后也就是const左值引用;
如果原有参数是右值引用,那么转发过后也就是右值引用;
如果原有参数是const右值引用,那么转发过后也就是const右值引用;

因此我们对上面的代码做一下更改:
C++11_第36张图片
我们在看一下运行结果;
C++11_第37张图片
这一次就符合我们第一次推到时的思路了;
像上面这样,利用forward在传参过程中保持住了参数的原有属性的过程就叫做完美转发!

强制生成默认函数的关键字default

C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原
因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以
使用default关键字显示指定移动构造生成。

C++11_第38张图片
为此我们需要写一个默认构造函数,但是利用default关键字,我们不用自己写,可以让编译器强制生成默认构造函数:
C++11_第39张图片

这样我们就能正常编译了;
我们也可以强制生成移动构造;
C++11_第40张图片
我们可以发现并没有打印拷贝构造的语句,因此这说明default确实能强制生成默认成员函数;

禁止生成默认函数的关键字delete

既然有强制生成,就有强制删除,delete可以不让指定的默认函数生成;
如果想要限制某些默认成员函数的生成,C++98中是先自己写出该函数的框架,然后将该函数的属性设置为私有,这样别人只要一调用该函数就会报错;
在C++11中只需在该函数声明加上=delete即
可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
比如:

C++11_第41张图片

可变参数模板

C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比
C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改
进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现
阶段呢,我们掌握一些基础的可变参数模板特性就够我们用了,所以这里我们点到为止,以后大
家如果有需要,再可以深入学习。下面就是一个基本可变参数的函数模板
C++11_第42张图片
我们可以通过sizeof…(args)来求参数包中参数的个数:
C++11_第43张图片
可是我们光知道参数的个数没用啊,我们要取出参数来才行啊,为此我们可以采用递归的方法来取参数:

//递归出口
void PrintfValue(void)
{
	cout << endl;
}
template<class T,class ...Args>
void PrintfValue(const T&t,Args...args)
{
	cout << t << " ";
	PrintfValue(args...);
}
//这是一个基本可变参数的函数模板
template<class ...Args>
void ShowList(Args...args)
{
	PrintfValue(args...);
}

我们把每递归一次,就将参数包的第一个参数自动交给t,然后由编译器自动推导,这样的话每递归一次参数包个数就会少一个,直到最后为0;
C++11_第44张图片
当然,我们还可以通过另一种方式:
C++11_第45张图片
C++11_第46张图片

线程库

thread类的简单介绍

在C++11之前,涉及到的多线程问题都是和平台有关的,比如Windows和Linux下对于线程的相关操作都有自己的接口,彼此接口名字是不一样的,这就会导致我在Linux下写的多线程代码,在Windows下就跑不起来了,因为在Windows系统下当然找不到Linux系统下是线程接口啦!这样会使代码的可移植性变的极差!为了解决这个问题,C++11就引入了线程库,只要凡是设计到线程有关的操作,都用C++11提供的接口来完成,那么保证这样的多线程代码不仅能在Linux下跑还能在Windowns下跑,这样代码可以指性的问题就得到了解决;
那么C++11线程库是如何解决这个可移植性差的问题的呢?
实际上C++11提供的线程库是对于Linux和Windows下的线程接口都进行了封装,也就是有两份代码,当我们的环境是在Linux系统下时,那么百年一起就会进行条件编译,把Linux下的线程接口放出来,屏蔽掉Windows下的线程接口;当我们的环境是Windowns时,也是同样的道理!
以至于现在还被程序员们吐槽的C++标准网路库,C++也是还没有提供一个像线程库一样的标准网络库,这就导致了我们在进行网络编程时就不得不直接使用系统调用了,开发效率低不说,代码可移值性也是极差!当然,如果我们能力足够的话,也可以自己写一个简单的网络库来玩一玩
下面我们来具体介绍一下,C++线程库接口

函数名 功能
thread() 构造一个线程对象,由于无参构造没有关联任何一个线程函数,因此该线程对象没有启动任何线程
thread(fun,args1,args2,…) 构造一个线程对象,并从fun函数出开始启动该线程,args1,aegs2,…为其为fun的参数
get_id() 获取线程id
joinable() 判断一个线程对象是否还在执行
join() 用于主线程回收子线程,如果主线程在调用该函数时,子线程还在运行 ,那么主线程会阻塞在join函数里面,直到子线程运行结束
detach 如果嫌弃主线程每次都需要手动调用join来回收子线程的资源,那么我们在创建出子线程过后,可以调用detach函数将其属性改为"分离",那么在子线程运行结束过后,不需要主线程在手动的调用join来回收资源了,调了会报错!

注意:

  1. 线程与线程对象是两个不同的概念,线程是OS系统的概念,线程对象是用户层的概念;一个线程对象仅可以关联一个线程;如果线程对象在初始化时默认没有给其线程函数,那么说明该线程对象并没有关联一个线程,比如线程对象的默认构造;
  2. 在创建一个线程对象时,我们可以给其一个线程函数,让其关联一个线程,而给的线程函数,可以是以下类型:函数指针、函数对象、lambda对象、包装器对象;
  3. thread类是不允许拷贝构造和拷贝赋值的,但是允许移动构造、移动赋值,即将一个线程对象所控制的线程转移给另一个线程对象,在转移期间不影响线程函数的允许;
  4. joinable()函数可以判断线程对象所控制的线程是否正在运行;以下情况,线程对象joinable会失败:
    ①利用无参构造初始化的线程对象;
    ②线程函数运行结束;
    ③线程对象调用join()过后或者调用detach过后(尽管线程此时还在运行,照样会检查失败);
    ④线程对象所关联的线程被转移给了别的线程对象;
  5. 并发与并行的区别:并发实际上在任意时刻都只有一个线程在运行,只不过每个线程被切换的时间太快太快了,我们肉眼是无法感受出来的,因此在我们的感知上表现的就是多个线程同时运行的假象!而并行才是真正意义上的多个线程同时一起运行,也就是说在任意一个时刻,都是多个线程一起运行,而不是通过高速切换来实现的同时运行的假象,而是货真价实的多个线程同时运行!

线程函数的参数

线程函数的参数都是以值拷贝的方式拷贝到线程栈空间中,因此即使线程函数的参数是引用,在线程中修改过后也是不会影响外面的参数的变化,因为这个引用实际引用的就是拷贝过后的空间,不再是我们传递给线程函数的那块空间;
那么我们应该如何让线程函数内部的改变,影响到外面呢?
1、将参数的地址传过去,通过指针解引用来实现内部改变影响外面:
C++11_第47张图片
2、如果我们就是想用引用来作为线程函数的参数,那么我们可以借用借助std::ref函数:
C++11_第48张图片
3、当线程对象的线程函数是一个非静态成员函数的时候,我们在传参的时候,需要完成this指针的传递:
C++11_第49张图片

原子性操作库(atomic)

多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问
题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数
据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦,比如:
C++11_第50张图片
讲道理,我们的期望是线程1和线程2各自将g_num加加10000次,因此最后g_num的值应该是20000,可是最后的运行结果与我们预期相差比较远,这是为什么?
这是因为,主线从很快的的就创建完线程1和线程2了,线程1和线程2就开始并发运行,这就可能导致线程1和线程2同时访问g_num然后同时对其进行++,那么就会导致明明对g_num加了两次,但是得到的结果确实加一次的结果,这就是多线程访问共享资源时所带来的线程安全问题!
在C98中是通过对共享资源加锁的方式来保证线程安全的,比如:当多线程要访问同一个共享资源时,必须先申请锁,锁只有一个,只有申请到锁的线程才能对共享资源进行访问,没申请到锁的线程只有等待着其它线程把共享资源使用完,再把锁解掉之后在重新申请锁,在此期间由于没有申请到锁,那么只能阻塞:
C++11_第51张图片
虽然加锁可以解决,但是加锁有一个缺陷就是:只要一个线程在对sum++时,其他线程就会被阻
塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁。
因此C++11中引入了原子操作。所谓原子操作:即不可被中断的一个或一系列操作,在同一个时刻,同一个原子操作最多只能被一个线程执行,而其他线程必须等待该操作完成才能进行下一步操作。这就是原子操作所具有的同步和互斥性质,它可以避免多个线程同时对共享变量进行读写操作时所引发的竞态条件和数据竞争等问题。C++11引入的原子操作类型,使得线程间数据的同步变得非常高效。

C++11_第52张图片

在C++11中,程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的
访问。
更为普遍的,程序员可以使用atomic类模板,定义出需要的任意原子类型

在这里插入图片描述

注意:原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11
中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及
operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算
符重载默认删除掉了

C++11互斥锁

在多线程环境下,如果想要保证某个变量的安全性,只要将其设置为对应的原子类型即可,即高效也不容易出现死锁问题。但是在某些情况下我们可能需要保证一段代码的安全性,那么就只能通过锁的方式来进行控制,比如:
C++11_第53张图片
向上面的代码,我们据需要锁来将整个区域保护起来,但是如果锁控制不好的话,那么是会容易造成死锁问题的,最常见的是在锁控制的范围内进行return,或者在锁的范围里面抛出异常;因此为了解决这个问题,C++11设计了一种自动锁,这把自动锁的工作原理就是,我们需要传一把互斥锁给它,然后它会自动帮我们申请互斥锁,当我们离开自动锁的范围的时候,该自动锁将会自动帮助我们解锁,这种封装锁的方式叫做RAII,C++提供的有lock_guard、unique_lock;

互斥锁的种类

  1. mutex
    C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动。mutex最常用
    的三个函数:
    lock() :申请互斥锁,申请失败则阻塞在lock函数内部
    unlock() :解锁互斥锁
    try_lock() :如果说lock()是阻塞式申请锁,那么try_lock()就是非阻塞式申请锁,申请失败不在阻塞而是进行返回;
  1. recursive_mutex:
    当同一个线程在持有锁的情况下,在申请锁时,不会发生死锁问题,而是什么也不做,与mutex用法一致,唯一区别点就是当线程在持有锁的情况下,再申请同一把锁不会造成死锁;
  1. time_mutxe:
    比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until() 。
    try_lock_for()
    接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与
    std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回
    false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超
    时(即在指定时间内还是没有获得锁),则返回 false。
    try_lock_until()
    接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,
    如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指
    定时间内还是没有获得锁),则返回 false。
  1. recursive_timed_mutex:
    即拥有time_mutex的特点也拥有recursive_mutex的特点

lock_guard与unique_lock

前面我们说了lock_guard与unique_lock是两种RAII风格的上锁方式,当我们给其一把互斥锁的时候,都能够进行自动申请锁,当我们离开互斥锁控制范围的时候,可以自动解锁;
那么这两个RAII上锁方式有什么区别:
最主要的区别就是unique_lock上锁方式更加灵活:
当我们使用lock_guard的方式来上锁是,在锁控制的范围内我们是不能进行手动解锁的,只能等到出来锁的控制范围自动解锁,可是要是在锁的中间有一段比较耗时的操作同时也不需要使用共享资源时,我们此时还占有者锁属实有点浪费资源了,为何不在这段时间内将锁先放出去,让其他线程能够物有所用,提高工作效率?lock_guard无法做到这一点,于是C++11就提出了一种新的上锁方式unique_lock,这种上锁方式,能够解决我们上述的问题,这也是一种自动上锁自动解锁的方式,同时它还支持在锁控制的范围内随时随地的的解锁和申请锁,给了用户极大的操作空间!

两个线程交替打印,一个打印奇数,一个打印偶数

我们这里主要演示条件变量的作用;
我们让线程1打印奇数,线程2打印偶数
分析:
既然要交替打印那么必然涉及到共享资源的访问,为了保证多线程访问共享资源时的线程安全,我们需要一把互斥锁;
同时当计数器为奇数的时候,线程2不应该执行,应该进入条件变量挂起,不过在此之前应该先唤醒线程1,让线程1进来打印;当计数器为偶数的时候线程2进行打印操作;
同理当计数器为偶数的时候,线程1不应该执行,应该让线程1进入条件变量挂起,不过在此之前,应该先唤醒线程2,比较计数器是偶数是线程2的工作!如果计数器是奇数的时候,那么线程1就打印呗!
为此,代码方面我们可以按如下方面设计:

#include
#include
#include
#include
using namespace std;
#include
int main()
{
	int i = 1;
	int top = 10;
	mutex mux;
	condition_variable cond;
	thread t1([&]()->void {
		while (true)
		{
			//线程1
			//先判段奇偶
			unique_lock<mutex> loker(mux);
			if (i % 2==0)
			{
				//对于线程1来说,如果计数器是偶数那么我们需要唤醒线程2起来工作,同时把自己挂在条件变量
				cond.notify_one();
				//在线程进入条件变量等待时,需要把锁也传递给条件变量
				//它会帮我们自动解锁,避免我们带着锁进行阻塞,这样就会造成死锁
				//同时在我们醒来的时候自动帮我们申请锁
				cond.wait(loker);
			}
			//醒来过后检查一下工作是否合法
			if (i > top)
			{
				//唤醒一下线程2,避免线程1推出l没有人唤醒线程2的尴尬局面
				cond.notify_one();
				break;
			}
			//合法
			cout << "thread-1:" << i++ << endl;
		}
		});
	thread t2([&]()->void {
		while (true)
		{
			unique_lock<mutex> loker(mux);
			if (i % 2 )
			{
				cond.notify_one();
				cond.wait(loker);
			}
			//醒来过后检查一下工作是否合法
			if (i > top)
			{
				//唤醒一下线程1,避免线程2退出线程l没有人唤醒线程2的尴尬局面
				cond.notify_one();
				break;
			}
			//合法
			cout << "thread-2:" << i++ << endl;
		}
		});
	t1.join();
	t2.join();
	return 0;
}

C++11_第54张图片

包装器

function包装器也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。
那么我们来看看,我们为什么需要function呢?
简单点来说,原本的相同返回值类型、相同参数类型的参数的函数指针、函数对象、Lambda表达式可以用一个统一的类型来表示吗?
不可以吧!
在C++11中,为了解决这个问题就提出了包装器的概念,包装器底层还是对于函数指针、函数对象、Lambda表达式的一层封装,只不过在进行封装过后,站在包装器的层面,相同返回值类型、相同参数类型的参数的函数指针、函数对象、Lambda表达式就可以用一个统一的类型来表示了,同时利用包装器实例化出来的对象也可以像被封装的函数指针、函数对象、Lambda表达式那样进行函数调用!
包装器语法:
function< 返回值类型(arg1类型,arg2类型,…)>
C++11_第55张图片
这样的话,站在包装器层面Add函数指针、m函数对象、Lambda表达式就是同一个类型了,同理我们可以也可以通过包装器对象来调用其封装的函数指针、函数对象、Lambda表达式:
C++11_第56张图片
其底层原理原理就是包装其重载l自己的oprator()运算符,然后将其参数设置为包装其封装的函数指针、函数对象、Lambda表达式的参数,然后在包装器的operator中调用其封装的函数指针、函数对象、Lambda表达式;
我们在用成员函数封装非静态成员函数的时候需要注意一下this指针的传递:
这里与非静态成员函数充当线程函数非常类似,在其底层都要调用非静态成员函数,而非静态成员函数的调用需要通过对象,因此this指针的传递,我们用本类对象或本类指针来完成:
C++11_第57张图片

你可能感兴趣的:(C++,c++,开发语言,linux)