C++学习-------C++11标准新出的操作

C++11简介

在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能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。

列表初始化

1.C++98中{}的初始化问题:
在C++98中,已经有了对数组元素用{}进行初始化:例如:

int arr[] = {1,2,3,4,5,6,7};
int arr[5] = {0};

但是对于一些自定义类却无法使用,例如vector:

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

无法这样去进行,所以导致我们初始化vector的时候会出现麻烦。
2.而C++11扩大了{}的使用范围,使它不仅仅局限于对数组的操作,也限于对自定义类的操作和内置类型,并且还加入了(=)的情况。(当然,这个=可加可不加,没有任何影响)
其中内置类型如:

int a{5};//给变量a赋值为5

这种就为内置类型。
3.自定义类型列表的初始化:
①:标准库支持单个列表对象的列表初始化:

class Point
{
public:
	Point(int x = 0, int y = 0) : _x(x), _y(y)
	{}
private:
	int _x;
	int _y;
};
int main()
{
	Point p{ 1, 2 };
	return 0;
}

直接将Point类中的成员变量分别初始化为1和2。
②:多个对象的列表初始化
多个对象想要支持列表初始化,需给该类(模板类)添加一个带有initializer_list参数类型的构造函数即可。注意:initializer_list是系统自定义的类模板,该类模板中主要有三个方法:begin()、end()迭代器
以及获取区间中元素个数的方法size()。
具体代码如下:

#include <initializer_list>
template<class T>
class Vector 
{
public:
    Vector(initializer_list<T> l): _capacity(l.size()), _size(0)
    {
        _array = new T[_capacity];
        for(auto e : l)
        _array[_size++] = e;
    }
    Vector<T>& operator=(initializer_list<T> l) 
    {
       delete[] _array;
       size_t i = 0;
       for (auto e : l)
       _array[i++] = e;
       return *this;
     }
// ...
private:
    T* _array;
    size_t _capacity;
    size_t _size;
};

变量类型推导

1.进行类型推导的原因:
①:在定义一个变量的时候,必须得卸除变量的数据类型,而有时候我们不知道变量的数据类型,或者写数据类型的时候非常的麻烦,所以我们就需要一个类型推导,让机器帮我们进行这一项工作。
②:而C++11中就帮我们提供了一个可以使用auto关键字帮我们进行数据类型的推导:例如:

std::vector<int> v;
std::vector<int>::iterator it = v.begin();//使用vector的迭代器。
auto it = v.begin();//我们可以完全使用auto来省去这一步复杂的内容。

2.decltype类型推导:
①:由于auto的前提是,已经对auto声明的类型进行了初始化,否则,编译器还是无法推导出数据的类型。但是有可能也要根据表达式运行完成的结果进行推导,因为在编译期间,代码无法推导,此时auto也就没话说了。
例如:

template<class T1, class T2>
T1 Add(const T1& left, const T2& right)
{
return left + right;
}

对于以上函数,如果对于函数执行完加法后,用结果的实际类型作为函数的返回类型,那么这个代码就不会出错,但是只有程序运行完才知道结果的类型,所以这时候我们就要提到RTTI(运行时类型识别)。
而在C++98中存在RTII,分别如下:

  • typeid:只能查看数据的类型,不能使用。
  • dynamic_cast:只能应用于含有虚函数的继承体系中。

缺点:运行时,识别类型会降低程序运行的效率。
2.decltype
①:decltye是根据表达式的实际类型推演出定义变量时的数据类型。
②:推演表达式类型作为变量的定义的类型:

int main()
{
int a = 10;
int b = 20;
decltype(a+b) c;// 用decltype推演a+b的实际类型,作为定义c的类型
cout<<typeid(c).name()<<endl;
return 0;
}

③:推演函数返回值的类型:

void* GetMemory(size_t size)
{
return malloc(size);
}
int main()
{
// 如果没有带参数,推导函数的类型
cout << typeid(decltype(GetMemory)).name() << endl;
// 如果带参数列表,推导的是函数返回值的类型,注意:此处只是推演,不会执行函数
cout << typeid(decltype(GetMemory(0))).name() <<endl;
return 0;
}

右值引用

1.右值引用的概念:
在C++98中提出了引用的概念,是对同一个物理内存使用不同的变量名进行操作,而引用的底层是通过指针完成的,这样大大提高了程序的可读性。但是为了增加程序的运行效率,在C++11中我们提供了右值引用。(右值引用也是对一个物理内存起一个别名,然后进行操作)
2.而此时,左值是什么?右值又是什么?
①:C语言中没有给出严格的标准:但是一般情况下,只能放在=右边的,或者不能取地址的为右值;只能放在=左边的,或者可以获取地址的成为左值。(但是这样的情况不一定正确)
例如:

int g_a = 10;
// 函数的返回值结果为引用
int& GetG_A()
{
    return g_a;
}
int main()
{
    int a = 10;
    int b = 20;
    // a和b都是左值,b既可以在=的左侧,也可在右侧,
    // 说明:左值既可放在=的左侧,也可放在=的右侧
    a = b;
    b = a;
    const int c = 30;
    c = a;// 编译失败,c为const常量,只读不允许被修改
    cout << &c << endl;// 因为可以对c取地址,因此c严格来说不算是左值
    // 编译失败:因为b+1的结果是一个临时变量,没有具体名称,也不能取地址,因此为右值
    //b + 1 = 20;
    GetG_A() = 100;
    return 0;
}

因此对于左值和右值我们有以下区分:

  • 普通类型修饰的变量,既可以被修改,也可也取地址,所以为左值。
  • const类型修饰的常量,不可修改,但是可以取地址(如果不对其进行操作,那么编译器会认为它是常量,不会分配空间,但是如果对其进行任何操作:例如取地址,那么编译器就会给其分配空间),所以它也是左值。
  • 如果表达式运行结果是一个临时变量或者对象,是右值。
  • 如果表达式运行结果或者单个变量是一个引用则认为是左值。

②:而在C++中给出了严格的左值右值标准

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

3.引用与右值引用的比较
①:C++98中普通引用与const引用的区别:

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中的右值引用:

int main()
{

int&& r1 = 10;// 10纯右值,本来只是一个符号,没有具体的空间,
r1 = 100;// 右值引用变量r1在定义过程中,编译器产生了一个临时变量,r1实际引用的是临时变量
int a = 10;
int&& r2 = a; // 编译失败:右值引用不能引用左值
return 0;
}

可以看到:右值引用只能引用右值,无法引用左值。
4.移动语句:
①:首先,我们先看如下代码:
这个代码为一个string类的简单模拟实现

class String
{
public:
   String(char* str = "")
   {
      if (nullptr == str)
      str = "";
      _str = new char[strlen(str) + 1];
      strcpy(_str, str);
   }
   String(const String& s)
   : _str(new char[strlen(s._str) + 1])
   {
     strcpy(_str, s._str);
   }
   String& operator=(const String& s)
   {
     if (this != &s)
     {
         char* pTemp = new char[strlen(s._str) +1];
         strcpy(pTemp, s._str);
         delete[] _str;
         _str = pTemp;
     }
     return *this;
   }
     String operator+(const String& s)
   {
     char* pTemp = new char[strlen(_str) + strlen(s._str) + 1];
     strcpy(pTemp, _str);
     strcpy(pTemp + strlen(_str), s._str);
     String strRet(pTemp);
     return strRet;
   }
   ~String()
   {
    if (_str) 
       delete[] _str;
   }
private:
     char* _str;
};

如上面的代码:我们可以看到,这是一个模拟string类的实现:但是我们发现,这个代码中会存在一个不尽人意的地方,就是在进行两个相加的时候,我们会产生三次内存的创建和销毁:
如下图:
在这里插入图片描述
我们看出来,可谓是一波三折,不仅折损了内存的大小,还浪费了时间。对于这种情况的解决,我们就引出了移动构造,这样就可以简单的解决了这个问题。
②:移动构造:
由于上述代码:我们发现,都是对一个相同的数据进行反复的构造与析构,这样我们就想到,如果我们将这个相同的数据只进行构造,然后让其所指的指针进行删除即可,那么就得出,只让这个相同的数据进行不断的移动,所以就减少了两次的构造,只让其移动,最终让最后的需要的类指向其空间即可。
此时我们需要在类中添加以下代码:

String(String&& s)
: _str(s._str)
{
   s._str = nullptr;
}

可以看出来,这个构造函数的参数为右值引用,这是因为,三次构造中,有两次都是将亡值,他们在完成自己的任务后就进行的释放,所以对于将亡值,在C++11中代表为右值,而C++中类的匹配机制为用最合适的函数进行匹配,所以就会选择以上函数进行拷贝,这样便解决了上面的问题。
5.右值引用左值:
①:在C++11中标准定义:右值是无法引用左值的。
但是在C++标准库中给了一个move函数,这个函数可以将左值转化为右值,从而进行右值引用,这个函数的使用方法如下:

int a =10;
int&& b = move(a);

上面这段函数的运行不会报错,并且a会被转化为右值被b进行右值引用。
注意:

  • 被转化的左值,它的声明周期并没跟着左值的转化而改变,
  • STL中也是有一个move函数,他是将一个范围内的数据移动到另一个范围内。

②:move函数的误用:
且看如下代码:

int main()
{
String s1("hello world");
String s2(move(s1));
String s3(s2);
return 0;
}

上面的函数是有问题的,因为s1在被move函数使用后变成右值然后对s2进行
拷贝构造,那么必然会触发string类中的移动语句,这样就会使s1中的数据为空,那么对s1来说就早曾了数据丢失,从而达不到预期的效果。
6.完美转发:
①:概念:完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。

void Func(int x)
{
// ......
}
template<typename T>
void PerfectForward(T t)
{
Fun(t);
}

PerfectForward函数为模板函数,Fun为实际目标函数,但是上述代码并不完美,完美转发是目标函数总希望将参数按照传递给转发函数的实际类型转给目标函数,而不产生额外的开销。
②:所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)。
所以:

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){Fun(std::forward<T>(t));}
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;
}

上述代码就实现了完美转发,在转发的过程中,没有任何其他内存的消耗。
(上述对模板会有一个小的知识,就是PerfectForward这个函数的参数,它可以引用为转化为右值的左值a,是因为模板的强大,即便是&&的右值情况,但是传进去的是左值,他也还是会以&的方式接收,所以不会报错)。
7.右值引用的作用

  • 实现移动语义
  • 给中间临时变量取别名
  • 实现完美转发

lambda表达式

1.为什么会有lambda表达式:
①:当我们对一个数据集合进行排序的时候,可以使用sort函数进行排序,例如:

#include <algorithm>
#include <functional>
using namespace std;
int main()
{
int array[] = {4,1,8,5,3,7,0,9,2,6};
sort(array, array+sizeof(array)/sizeof(array[0]));// 默认按照小于比较,排出来结果是升序
sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());// 如果需要降序,需要改变元素的比较规则
return 0;
}

当然此时的greater是一个仿函数,头文件为functional,如果我们要进行降序的话需要这样。(因为对于sort函数,它的默认排序是升序的)。
②:但是如果待排的数据为自定义类型的数据时,我们就必须实现一个algorithm算法,如下:

struct Goods
{
    string _name;
    double _price;
};
struct Compare
{
    bool operator()(const Goods& gl, const Goods& gr)
    {
        return gl._price <= gr._price;
    }
};
int main()
{
    Goods gds[] = { { "苹果", 2.1 }, { "相交", 3 }, { "橙子", 2.2 }, {"菠萝", 1.5} };
    sort(gds, gds+sizeof(gds) / sizeof(gds[0]), Compare());
    return 0;
}

如同这样的一个类,我们进行排序的时候就需要重写一个仿函数,如果我们需要其他的比较类型,这样会感觉比较麻烦,特别是相同类的命名,到时候会出现困难,因此C++11中提供了一个lambda表达式。
2.lambda表达式的语法:
①:书写格式:

[capture-list] (parameters) mutable -> return-type { statement }

其中每个部分的解释如下:

  • capture-list:捕捉列表,这个列表总是在lambda表达式的最开始位置,这个位置可以让程序判断接下来的代码是不是为lambda表达式,并且这个位置是可以捕捉上下文列表的变量供lambda表达式使用。
  • parameters:参数列表。与普通函数的参数相同,就是传一个参数,如果不需要可以连同()一块省略掉。
  • mutable:由于lambda函数默认为const类型,而mutable可以消除其const类型。但是使用mutable时候,参数列表不可以为空,即使没有参数也要将()写出来。
  • return-type:返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可以省略。返回值类型确定的情况下也可也省略该部分,可以通过编译器自动推导。
  • statement:函数体,在函数体内可以使用捕捉过来的变量。

注意:在书写lambda函数的时候,参数列表和返回值类型是可选部分,而捕捉列表和函数体可以为空,因此最简单的lambda表达式为:[]{},该表达式什么工作都做不了。
②:对lambda函数进行使用,如下:

int main()
{
   []{};// 最简单的lambda表达式, 该lambda表达式没有任何意义
   int a = 3, b = 4;// 省略参数列表和返回值类型,返回值类型由编译器推导为int
[=]{return a + 3; };
   auto fun1 = [&](int c){b = a + c; };// 省略了返回值类型,无返回值类型
   fun1(10)
   cout<<a<<" "<<b<<endl;
   auto fun2 = [=, &b](int c)->int{return b += a+ c; };// 各部分都很完善的lambda函数
   cout<<fun2(10)<<endl;
   int x = 10;
   auto add_x = [x](int a) mutable { x *= 2; return a + x; };// 复制捕捉x
   cout << add_x(10) << endl;
   return 0;
}

由上面的代码可以看出,lambda函数为无名函数,我们可以通过auto给这个无名函数起一个名字,进而对其进行操作即可。
③:捕捉列表的说明:
捕捉列表代表的是从上下程序中捕捉可以被lambda函数使用的变量,以及捕捉的变量是传值还是传引用操作。

  • [var]:表示值传递捕捉变量var。
  • [=]:表示值传递捕捉父作用域中的所有变量(包括this)。
  • [&var]:表示引用传递捕捉var。
  • [&]:表示引用传递捕捉父作用域中的所有变量(包括this)。
  • [this]:表示以传值的方式捕捉this。

注意:

  1. 父作用域代表的是lambda函数所在的作用域中。
  2. 上述捕捉变量可以捕捉多个,并且可以以逗号隔开。(但是需要注意的是,变量不可以重复捕捉,不然会造成编译错误,例如:[=,a]:=已经捕捉了所有变量,又捕捉一个a,导致重复捕捉,所以出错)
  3. 在作用域外的lambda表达式的捕捉列表必须为空。
  4. 在块作用域中的lambda表达式只能捕捉在该父作用域中的变量,如果捕捉不在这个作用域中的函数,那么就会报错。
  5. lambda表达式之间不可以相互赋值。(但是可以用一个lambda表达式去拷贝构造另一个lambda表达式)

④:函数对象与lambda表达式
其实,对于lambda表达式的底层是通过在类中重载了一个operator()来实现的。如果实现了一个lamdab表达式,那么相应的在底层就会生成一个相应的operator()

线程库

1.thread类的介绍:
①:由于在C++11之前就已经又很多多线程的问题,都是和平台相关的,比如在windows和linux下都有自己的线程,这样使代码的可移植性很差,C++11中最重要的特性就是对多线程问题进行了支持,C++的并行编程的使用中不需要去依赖第三方库。而且在原子操作中引入了原子类。首先要使用标准库的线程必须先引入头文件#include<thread>
②:thread类中的接口函数:

函数名功能
thread()构造一个线程对象,该线程对象没有链接任何线程函数,所以没有启动任何线程。
thread(fn,args1…)构造一个线程对象,并且关联一个fn为线程函数,args1为线程函数参数。
join()该函数运行后会堵塞主线程,等待子线程运行完后主线程再继续往下运行。
joinable()线程是否还正在运行,joinable代表一个正在运行的线程。
get_id()获取线程id号。
detach()在创建线程后马上使用这个函数,可以是产生的子线程与父线程脱离关系,这个时候不需要join函数进行等待了,子线程不管如何都和主线程没有任何关系了。

注意:

  1. 线程是操作系统中的一个概念,线程函数可以关联一个线程,可以用来控制一个线程以及获取线程的状态。
  2. 当创建一个线程对象时,如果没有关联线程函数,那么这个线程对象就没有对应任何线程。
  3. 当创建一个线程后,子线程就会和父线程一样去运行,就如同从此分开,各自运行各自的,但是注意的是,只要有子线程就必须需要join函数进行接收子线程的资源,否则就会出现资源泄漏的危险。(并且线程函数可以通过:①:函数,②:lambda表达式,③:类对象去提供)
  4. thread是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行。(就和智能指针的意思相似)
  5. 可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效:①:采用无参构造函数构造的线程对象;②:线程对象的状态已经转移给其他线程对象;③:线程已经调用jion或者detach结束。

2.线程函数的参数
①:线程函数的参数实际上是以拷贝构造的方式传递到线程栈里面的,因此如果参数是通过引用传递的,那么在子线程中去改变这个参数,实际上原有的参数没有被改变。(但是用指针可以改变,由于指针传递的是地址)
例如下列代码:

#include <thread>
void ThreadFunc1(int& x)
{
    x += 10;
}
void ThreadFunc2(int* x)
{
    *x += 10;
}
int main()
{
   int a =10;
   thread th1(ThreadFunc1,a);
   th1.join();
   cout<<a<<endl;
   thread th2(ThreadFunc2,&a);
   th2.join();
   cout<<a<<endl;
}

通过上述代码,可以发现,只有在线程th2执行完毕后,a才发生了改变。
3.join()和detach()
当启动一个进程的时候,系统给了我们两个函数,当进程执行完之后去释放他们的资源,防止资源的泄漏。
①:join()方式:
join():主线程被阻塞,当新线程终止时,join()会清理相关的线程资源,然后返回,主线程再继续向下执行,然后销毁线程对象。由于join()清理了线程的相关资源,thread对象与已销毁的线程就没有关系了,因此一个线程对象只能使用一次join(),否则程序会崩溃。
但是join的使用方式与delete的使用方式类似,会出现以下几种错误:

  1. 由于异常情况,使得主进程在创建子进程后就出现了异常,所以程序没有运行下去,没有运行到join的位置,那么子进程的资源就无法释放,造成资源泄漏。
  2. 对一个进程使用了多个join操作,就是对已经释放资源的进程又进行了资源释放,这样也会导致错误。

这两种误用如下:

//误用1:
void ThreadFunc() { cout<<"ThreadFunc()"<<endl; }
bool DoSomething() { return false; }
int main()
{
   std::thread t(ThreadFunc);
   if(!DoSomething())
   return -1;//在此刻退出了,子进程还没有被释放
   t.join();
   return 0;
}
//误用2:
void ThreadFunc() { cout<<"ThreadFunc()"<<endl; }
void Test1() { throw 1; }
void Test2()
{
    int* p = new int[10];
    std::thread t(ThreadFunc);
    try
    {
        Test1();//抛出了异常
    }
    catch(...)
    {
        delete[] p;//而在这里只对p进行了释放,子线程没有进行释放,就退出了
        throw;
    }
    t.jion();
}

因此,由于以上的错误,我们对进程的创建与释放也采取了RAII思想,对其进行类包装。所以其简单实现方式如下:

#include <thread>
class mythread
{
public:
    mythread(std::thread &t) :m_t(t){}
    ~mythread()
    {
       if (m_t.joinable())
        m_t.join();
    }
public:
    mythread(mythread const&)=delete;
    mythread& operator=(const mythread &)=delete;
private:
    std::thread &m_t;
};

②:detach()方式:
概念:detach():该函数被调用后,新线程与线程对象分离,不再被线程对象所表达,就不能通过线程对象控制线程了,新线程会在后台运行,其所有权和控制权将会交给c++运行库。同时,C++运行库保证,当线程退出时,其相关资源的能够正确的回收。(只要子进程和父进程分离了,那么就不需要父进程对其进行回收,它会被C++运行库进行回收)
用法:在进程刚被创建好就之间用,因为如果不是jion()等待方式结束,那么线程对象可能会在新线程结束之前被销毁掉而导致程序崩溃。
4.原子性操作库
在运行线程的时候,由于线程的运行速度我们是不了解的,就相当于一次生成了两个东西,这两个东西会互相争夺资源,所以我们并不知道资源就竞争是如何的结果,所以导致我们进行多线程运行的时候,会出现资源争夺的情况。
例如如下代码:

#include<thread>
void func1()
{
  cout<<"This is func1()"<<endl;
}
void func2()
{
   cout<<"This is func2()"<<endl;
}
int main()
{
   thread th1(func1);
   thread th2(func2);
   th1.join();
   th2.join();
   return 0;
}

运行上面的程序,我们会发现每次运行的结果都会不同。
而运行下面这段代码:

#include<thread>
int g_val = 0;
void func()
{
 for(int i = 0;i<10000;++i)
 {
    g_val++;
 }
}
int main()
{
   thread th1(func);
   thread th2(func);
   th1.join();
   th2.join();
   cout<<g_val<<endl;
   return 0;
}

这段函数运行下来最好的结果就是g_val的结果是20000,但是,由于存在竞争关系,当相同大小的g_val同时被两个线程进行相加的时候,那么这个自加就只能加一次,那么造成的结果是,最终g_val的结果并不是20000,而是比20000小。
1:对于C++98来说,以上的问题一般是通过加锁的方式进行解决的,例如:

#include<mutex>
std::mutex m;
void func()
{
 m.lock();//加锁
 for(int i = 0;i<10000;++i)
 {
    g_val++;
 }
 m.unlock();//解锁
}

使用上面添加的锁就可以使最终的结果是我们心里想的结果了。
而枷锁的原理是:当一个线程对这个函数进行操作的时候,另一个线程只能在外面等着,只有当这个锁里面没有线程的时候,才能进入。
但是:这样还是有缺点的:
容易造成线程堵塞,程序的运行效率会降低。
对于这样的情况,所以在C++11中引入了原子操作。
2.原子操作:
①:概念:不可中断的一个或者一系列操作,C++11的原子操作引入,让多个线程的同步数据变得高效起来。
②:原子操作变量如下:
在这里插入图片描述
使用这些原子变量需要添加头文件:#include<atomic>
例如下面代码:

#include <iostream>
using namespace std;
#include <thread>
#include <atomic>
atomic_long sum{ 0 };
void fun(size_t num)
{
   for (size_t i = 0; i < num; ++i)
       sum ++; // 原子操作
}
int main()
{
   cout << "Before joining, sum = " << sum << std::endl;
   thread t1(fun, 1000000);
   thread t2(fun, 1000000);
   t1.join();
   t2.join();
   cout << "After joining, sum = " << sum << std::endl;
   return 0;
}

由于线程的运行对原子变量具有互斥的原因,所以程序运行下来的结果和加锁运行下来的结果相同,并且相比之下,效率更高。
并且,我们还可以使用原子类自定义出自己想要的原子变量
如:atomic<T> t//声明一个原子类变量t。(其中T为类型模板)
注意:原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了。
3.lock_guard与unique_lock
在多线程下,为了保证一个变量的安全性,我们可以将其声明为原子变量然后进行多线程下的操作,这样即高效有省时,但是有时候,需要保证一段代码的安全性,我们又不得不进行加锁的操作。
比如如下代码:

#include <thread>
#include <mutex>
int number = 0;
mutex g_lock;
int ThreadProc1()
{
   for (int i = 0; i < 100; i++)
   {
      g_lock.lock();
      ++number;
      cout << "thread 1 :" << number << endl;
      g_lock.unlock();
   }
   return 0;
}
int ThreadProc2()
{
   for (int i = 0; i < 100; i++)
   {
      g_lock.lock();
      --number;
      cout << "thread 2 :" << number << endl;
      g_lock.unlock();
   }
   return 0;
}
int main()
{
   thread t1(ThreadProc1);
   thread t2(ThreadProc2);
   t1.join();
   t2.join();
   cout << "number:" << number << endl;
   system("pause");
   return 0;
}

如这些代码:如果锁没有控制好,就有可能造成死锁,意思为如果锁中间的数据返回了,或者锁范围内的函数出现异常了。因此在C++11中对锁也使用了RAII思想。
①:Mutex的种类:
在C++11中一共提供了四种类型分别如下:
1.std::Mutex。
C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动。mutex最常用的三个函数:

函数名函数功能
lock()上锁
unlock()解锁
trylock()尝试上锁,如果互斥量被其他线程占有,那么就退出,当前线程也不会被堵塞

注意:使用lock()函数的时候,可能会出现以下情况:

  • 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。
  • 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。
  • 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

线程函数调用try_lock()时,可能会发生以下三种情况:

  • 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。
  • 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。
  • 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。


2. std::recursive_mutex:
其允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,释放互斥量时需要调用与该锁层次深度相同次数的unlock(),除此之外,std::recursive_mutex 的特性和std::mutex 大致相同。

3.std::timed_mutex:
比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until() 。
①:try_lock_for()
接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与 std::mutex的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
(就是加了个时间概念,去检测在相同的时间内,这个需要被上锁的位置是否上了锁,如果上了锁那还好,如果没有,那就报错)
②:try_lock_until()
接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
3. std::recursive_timed_mutex

①:lock_guard
其在std中的定义如下:

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;
};

通过上述代码可以看到,lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封装,在需要加锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数成功上锁,出作用域前lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题。(采用了RAII思想,需要加锁的地方进行加锁,如果出了作用域,那么之间进行自动解锁,防止了死锁问题)
但是还是有缺点:太单一,用户没有办法对该锁进行控制,因此C++11又提供了unique_lock。
②:unique_lock
概念:与lock_gard类似,unique_lock类模板也是采用RAII的方式对锁进行了封装,并且也是以独占所有权的方式管理mutex对象的上锁和解锁操作,即其对象之间不能发生拷贝。在构造(或移动(move)赋值)时,unique_lock 对象需要传递一个 Mutex 对象作为它的参数,新创建的 unique_lock 对象负责传入的 Mutex对象的上锁和解锁操作。使用以上类型互斥量实例化unique_lock的对象时,自动调用构造函数上锁,unique_lock对象销毁时自动调用析构函数解锁,可以很方便的防止死锁问题。
两者不同的点为:
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
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
【为什么还需要学习C++?】 你是否接触很多语言,但从来没有了解过编程语言的本质?你是否想成为一名资深开发人员,想开发别人做不了的高性能程序?你是否经常想要窥探大型企业级开发工程的思路,但苦于没有基础只能望洋兴叹? 那么C++就是你个人能力提升,职业之路进阶的不二之选。【课程特色】 1.课程共19大章节,239课时内容,涵盖数据结构、函数、类、指针、标准库全部知识体系。2.带你从知识与思想的层面从0构建C++知识框架,分析大型项目实践思路,为你打下坚实的基础。3.李宁老师结合4大国外顶级C++著作的精华为大家推的《征服C++11》课程。【学完后我将达到什么水平?】 1.对C++的各个知识能够熟练配置、开发、部署;2.吊打一切关于C++的笔试面试题;3.面向物联网的“嵌入式”和面向大型化的“分布式”开发,掌握职业钥匙,把握行业先机。【面向人群】 1.希望一站式快速入门的C++初学者; 2.希望快速学习 C++、掌握编程要义、修炼内功的开发者; 3.有志于挑战更高级的开发项目,成为资深开发的工程师。 【课程设计】 本课程包含3大模块基础篇本篇主要讲解c++的基础概念,包含数据类型、运算符等基本语法,数组、指针、字符串等基本词法,循环、函数、类等基本句法等。进阶篇本篇主要讲解编程中常用的一些技能,包含类的高级技术、类的继承、编译链接和命名空间等。提升篇:本篇可以帮助学员更加高效的进行c++开发,其中包含类型转换、文件操作、异常处理、代码重用等内容。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值