C++11

文章目录

    • 1.c++11简介
    • 2.统一的列表初始化
      • 2.1{}初始化
      • 2.2std::initializer_list
        • 2.2.1std::initializer_list的使用
        • 2.2.2底层原理
    • 3.声明
      • 3.1auto
      • 3.2decltype
      • 3.3nullptr
    • 4.范围for循环
    • 5.智能指针
    • 6.STL的一些变化
    • **7.右值引用和移动语义**
      • 7.1左值引用和右值引用
        • 7.1.1左值的概念和左值引用的概念
        • 7.1.2右值的概念和右值引用的概念
        • 7.1.2右值引用的注意点(完美转发场景的解释)
      • 7.2左值引用和右值引用的区别
        • 7.2.1左值引用
        • 7.2.2右值引用
      • 7.3右值引用的使用场景和意义
        • 7.3.1传值返回与移动构造
          • 7.3.1.1解决不可避免的传值返回
          • 7.3.1.2移动构造的原理与过程
        • 7.3.2移动赋值
        • 7.3.3小结
        • 7.3.4右值引用引用左值及其一些更深入的使用场景的分析
          • 7.3.4.1std::move
          • 7.3.4.2STL中增加的右值引用版本传参
          • 7.3.4.2emplace_xxx接口
          • 7.3.4.3小结
      • 7.4完美转发
        • 7.4.1模板参数`&&`的万能引用
        • 7.4.2完美转发
        • 7.4.3完美转发场景
    • 8.新的类功能
      • 8.1默认移动构造
      • 8.2默认移动赋值
      • 8.3小结
      • 8.4类内对象声明给缺省
      • 8.5default强制生成默认函数
      • 8.6delete强制禁止默认函数
      • 8.7final和override
    • 9.可变模板参数
      • 9.1递归函数方式展开参数包
      • 9.2逗号表达式展开参数包
    • **10.lambda表达式**
      • 10.1lambda出现场景
      • 10.2lambda表达式语法
      • 10.3lambda的底层原理
    • 11.包装器
      • 11.1std::function包装器的使用和意义
      • 11.2std::function包装器的使用场景
      • 11.3std::function小结
      • 11.4std::bind
    • 12.线程库
      • 12.1thread类接口
      • 12.2get_id()和this_thread::get_id()
      • 12.3关联线程函数
      • 12.4线程函数参数
      • 12.5原子性操作库
      • 12.6锁
        • 12.6.1mutex种类
        • 12.6.1lock和unlock
        • 12.6.2lock_guard
          • 12.6.2.1 lock和unlock缺陷场景
          • 12.6.2.2 lock_guard解决方法
        • 12.6.3unique_lock
        • 12.6.4相关题
        • 12.6.5补充

对于8.节请使用vs2019验证,vs2013并不是很好的支持。

1.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增加的语法特性非常篇幅非常多,本次主要学习实际中比较实用的语法。

因此以下demo中打!!!注释的作为吸收部分。

2.统一的列表初始化

{}初始化的使用场景,进行了扩大,各个地方都能用。

2.1{}初始化

在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如:

struct Point
{
    int _x;
    int _y;
};
int main()
{
    int array1[] = { 1, 2, 3, 4, 5 };
    int array2[5] = { 0 };
    Point p = { 1, 2 };
    return 0;
}

C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。

如下面的p2,x2虽然可以去掉=,但是还是建议保留之前的用法,加=。

#include
using namespace std;

struct Point
{
	int _x;
	int _y;
};
int main()
{
	Point p1 = { 1,2 };
	Point p2{ 1,2 };
	int x1 = 1;
	int x2{ 2 };
	//!!!
	int* p3 = new int[4]{ 0 };
	int* p4 = new int[4]{ 1,2,3,4 };
}

创建对象时也可以使用列表初始化方式调用构造函数初始化。

这里虽然可以这么用,但是还是不建议用,这种用法是为其他地方做准备的。

class Date
{
    public:
    Date(int year =0 , int month = 1, int day = 1)
        :_year(year)
            ,_month(month)
            ,_day(day)
        {
            cout << "Date(int year, int month, int day)" << endl;
        }
    private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d1;
    Date d2(2022, 1, 1); // old style
    // C++11支持的列表初始化,这里会调用构造函数初始化
    Date d3{ 2022, 1, 2 };
    Date d4 = { 2022, 1, 3 };
    return 0;
}

2.2std::initializer_list

2.2.1std::initializer_list的使用

std::initializer_list的介绍文档

C++11_第1张图片

#include
#include
using namespace std;
int main()
{
	auto il1 = { 10,20,30 };
	std::initializer_list<int> il2 = { 1,2,3,4 };
	cout << typeid(il1).name() << endl;
}

C++11_第2张图片

实际应用场景:

以前STL初始化的时候要一个个push_back/insert

C++11_第3张图片

class Date
{
public:
    Date(int year = 0, int month = 1, int day = 1)
        :_year(year)
        , _month(month)
        , _day(day)
    {
        cout << "Date(int year, int month, int day)" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
	auto il1 = { 10,20,30 };
	std::initializer_list<int> il2 = { 1,2,3,4 };
	cout << typeid(il1).name() << endl;

	vector<int> v = { 1,2,3,4,5 };
	list<int> lt{ 10,20,30 };
    vector<Date> vd1 = { Date(2022,1,17), Date{2022,1,17}, {2020,1,17} };
    map<string, int> dict= { make_pair("left",1),{"insert",2} };
	v = {10,20,30};//修改
	//C++98
	vector<Date> vd2;
	vd2.push_back(Date());
}

2.2.2底层原理

initializer_list是一个类,在stl容器的实现中作为构造参数传入。

namespace Y
{
    template<class T>
    class vector {
    public:
         typedef T* iterator;
         vector(initializer_list<T> l)
         {
             _start = new T[l.size()];
             _finish = _start + l.size();
             _endofstorage = _start + l.size();
             iterator vit = _start;
             /*取类模板里的iterator,取的时候类模板可能没有被实例化(如果iterator是个类,比如一颗红黑树封装map和set就是那种情况)。这里由于直接将原生指针定义,假如iterator也是原生指针就不用typename。为了统一情况,因此编译器规定不会去类模板里去取它再定义的类型,除非实例化了。因此加typename是编译器给的机制。告诉编译器等initializer实例化再去取*/
             typename initializer_list<T>::iterator lit = l.begin();
             while (lit != l.end())
             {
                 *vit++ = *lit++;
             }
             //for (auto e : l)
             //   *vit++ = e;
         }
         vector<T>& operator=(initializer_list<T> l) {
             vector<T> tmp(l);
             std::swap(_start, tmp._start);
             std::swap(_finish, tmp._finish);
             std::swap(_endofstorage, tmp._endofstorage);
             return *this;
         }
    private:
         iterator _start;
         iterator _finish;
         iterator _endofstorage;
     };
}

3.声明

C++11提供了多种简化声明的方式,尤其是使用模板时。

3.1auto

具体在之前的学习中已经讲解过。

auto在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型腿断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。

int main()
{
    int i = 10;
    auto p = &i;
    auto pf = strcpy;
    cout << typeid(p).name() << endl;
    cout << typeid(pf).name() << endl;
    map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
    //map::iterator it = dict.begin();
    auto it = dict.begin();
    return 0;
}

3.2decltype

关键字decltype将变量的类型声明为表达式指定的类型。

int main()
{
    int i = 10;
    auto p = &i;
    auto pf = strcpy;
    cout << typeid(p).name() << endl;
    cout << typeid(pf).name() << endl;

    typeid(pf).name() px;//C++98中不支持
    return 0;
}
int main()
{
    int i = 10;
    auto p = &i;
    auto pf = strcpy;
    cout << typeid(p).name() << endl;
    cout << typeid(pf).name() << endl;

    decltype(pf) px;//c++11支持
    return 0;
}
int main()
{
    const int x = 1;
    double y = 2.2;
    decltype(x * y) ret; // ret的类型是double
    decltype(&x) p;      // p的类型是int const*
    cout << typeid(ret).name() << endl;
    cout << typeid(p).name() << endl;
    return 0;
}

3.3nullptr

由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。

void f(int){
    cout<<"void int()"<<endl;
}
void f(int* p){
    cout<<"void int*"<<endl;
}
int main()
{
    f(NULL);
    f(nullptr);
}
#ifndef NULL
#ifdef __cplusplus
#define NULL   0
#else
#define NULL   ((void *)0)
#endif
#endif

4.范围for循环

底层的支持是迭代器,之前模拟实现讲过了。

5.智能指针

之后具体提及。

6.STL的一些变化

  • 新容器

arrayforward_list,unordered_map,unordered_set

array相比数组,多出来的就是越界报异常。

forward_list是单链表。

  • 容器中的一些新方法

如果我们再细细去看会发现基本每个容器中都增加了一些C++11的方法,但是其实很多都是用得比较少的。比如提供了cbegin和cend方法返回const迭代器等等,但是实际意义不大,因为begin和end也是可以返回const迭代器的,这些都是属于锦上添花的操作。

实际上C++11更新后,容器中增加的新方法最后用的插入接口函数的右值引用版本,emplace还涉及模板的可变参数。这些是宝贵的。

比如void push_back(value_type && val),void emplace_back(Arges&&... val)

http://www.cplusplus.com/reference/vector/vector/emplace_back/

http://www.cplusplus.com/reference/vector/vector/push_back/

http://www.cplusplus.com/reference/map/map/insert/

http://www.cplusplus.com/reference/map/map/emplace/

7.右值引用和移动语义

右值引用实际上引自boost库。

7.1左值引用和右值引用

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以之前学习的引用就叫做左值引用。

**无论左值引用还是右值引用,都是给对象取别名。**区别是左值引用主要是给左值取引用,右值引用主要给右值做引用。

7.1.1左值的概念和左值引用的概念

左值是一个表示数据的表达式(如变量名或解引用的指针),一般情况下,我们可以获取它的地址+可以对它赋值,左值一般在左边,可以出现赋值符号的右边,右值不能出现在赋值符号左边。

特殊情况:定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。

总结一下,

左值:

  1. 可以取地址。
  2. 一般情况下可以修改。(const修饰不能修改)

左值引用就是给左值的引用,给左值取别名。

int main()
{
    // 以下的p、b、c、*p都是左值
    int* p = new int(0);
    int b = 1;
    const int c = 2;
    // 以下几个是对上面左值的左值引用,左值出现在右边
    int*& rp = p;
    int& rb = b;
    const int& rc = c;
    int& pvalue = *p;
    return 0;
}

7.1.2右值的概念和右值引用的概念

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,传值返回函数的返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。

总结一下:

右值:

  1. 不能取地址(说明没有存起来)
  2. 不能出现在赋值=的左边,不能修改。

也可以将右值划分为:

  1. 纯右值,如10x+y
  2. 将亡值,如函数返回的临时对象,匿名对象。

右值引用就是对右值的引用,给右值取别名。

int main()
{
    double x = 1.1, y = 2.2;
    // 以下几个都是常见的右值
    10;
    x + y;
    fmin(x, y);
    // 以下几个都是对右值的右值引用
    int&& rr1 = 10;
    double&& rr2 = x + y;
    double&& rr3 = fmin(x, y);
    // 这里编译会报错:error C2106: “=”: 左操作数必须为左值。因为这是右值,,右值不能出现在赋值符号的左边。
    10 = 1;
    x + y = 1;
    fmin(x, y) = 1;
    return 0;
}

7.1.2右值引用的注意点(完美转发场景的解释)

需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址。

例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。

如果不想rr1被修改,可以用const int&& rr1去引用。

这个了解一下即可,实际中右值引用的使用场景并不在于此,这个特性也不重要。

int main()
{
    double x = 1.1, y = 2.2;
    // 以下几个都是常见的右值
    10;
    x + y;
    fmin(x, y);
    // 以下几个都是对右值的右值引用
    int&& rr1 = 10;
    double&& rr2 = x + y;
    double&& rr3 = fmin(x, y);

    //右值引用可以修改,原因是右值一般都不会存下来,而右值引用引用了之后开了一块空间把右值存下来了 
    rr1 = 20;
    cout << rr1 << endl;
    cout << &rr1 <<endl;
    return 0;
}

image-20220130165128633

int main()
{
    double x = 1.1, y = 2.2;
    int&& rr1 = 10;
    const double&& rr2 = x + y;
    rr1 = 20;
    rr2 = 5.5;  // 报错
    return 0;
}

7.2左值引用和右值引用的区别

7.2.1左值引用

  • 左值引用总结
  1. 左值引用只能引用左值,不能引用右值。(涉及权限的放大)
  2. 但是const左值引用既可引用左值,也可引用右值。
template<class T>
void f(const T& x)/*1.const 保护x不改 ; 2.const& 既可以接收左值又可以接收右值*/
{}
int main()
{
    // 左值引用只能引用左值,不能引用右值。
    int a = 10;
    int& ra1 = a;   // ra为a的别名
    //int& ra2 = 10;   // 编译失败,因为10是右值
    // const左值引用既可引用左值,也可引用右值。
    const int& ra3 = 10 + 20;
    const int& ra4 = a;
    return 0;
}

7.2.2右值引用

  • 右值引用总结
  1. 右值引用只能右值,不能引用左值。
  2. 但是右值引用可以move以后的左值。
    1. move并不会改变a的属性,可以理解为move(a)的返回值是一个右值
int main()
{
     // 右值引用只能右值,不能引用左值。
     int&& r1 = 10;

     // error C2440: “初始化”: 无法从“int”转换为“int &&”
     // message : 无法将左值绑定到右值引用
     int a = 10;
     int&& r2 = a;
     // 右值引用可以引用move以后的左值
     int&& r3 = std::move(a);
     a =10 ;//move并不会改变a的属性,可以理解为move(a)的返回值是一个右值
     return 0;
}

7.3右值引用的使用场景和意义

前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引用呢?是不是多此一举呢?

下面看看左值引用的短板,右值引用是如何补齐这个短板的。

在这之前需要分析左值引用的使用场景和短板。

我们取之前的string类来研究。

左值引用的使用场景:做参数,做返回值。

7.3.1传值返回与移动构造

7.3.1.1解决不可避免的传值返回

其中在做参数方面已经完善了。而做返回值并不完善。对于能传引用返回的部分使用传引用减少拷贝构造。比如operator+=能传引用返回,但是to_string不能传引用返回。所以说在这里就是左值引用短板。

如果函数返回对象出了函数作用域就不在了,就不能使用做引用返回,就会存在拷贝。

namespace Y
{
    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)
            {
                cout << "string(const string& s) -- 深拷贝" << endl;
                string tmp(s._str);
                swap(tmp);
            }
        // 赋值重载
        string& operator=(const string& s)
        {
            
            cout << "string& operator=(string s) -- 深拷贝" << endl;
            string tmp(s);
            swap(tmp);
            return *this;
        }
        ~string()
        {
            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;
        }
        const char* c_str() const
        {
            return _str;
        }
        private:
        char* _str;
        size_t _size;
        size_t _capacity; // 不包含最后做标识的\0
    };
    Y::string to_string(int value)
    {
        bool flag = true;
        if (value < 0)
        {
            flag = false;
            value = 0 - value;
        }
        Y::string str;
        while (value > 0)
        {
            int x = value % 10;
            value /= 10;
            str += ('0' + x);
        }
        if (flag == false)
        {
            str += '-';
        }
        std::reverse(str.begin(), str.end());
        return str;
     }
}
void func1(Y::string s)
{

}
void func2(const Y::string& s)
{

}
int main()
{
    Y::string s1("hello world");
    func1(s1);
    func2(s1);
    //operator+=可以使用传引用返回
    s1+='A';
    //to_string不能用左值引用返回
    Y::string ret = Y::to_string(1234);
}

上面的问题如何解决?右值引用和移动构造。

移动构造:增加一个参数是右值引用的版本。此时传对象的时候如果是左值则走拷贝构造,传的是右值就走移动构造。

对于临时对象进行交换。

 // s1.swap(s2)
void swap(string& s)
{
    ::swap(_str, s._str);
    ::swap(_size, s._size);
    ::swap(_capacity, s._capacity);
}
//移动构造
string(string&& s)
    :_str(nullptr)
    ,_size(0)
    ,_capacity(0)
{
    cout<<"string(string&& s) -- 移动构造"<<endl;
    this->swap(s);
}
7.3.1.2移动构造的原理与过程

移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。

而且特别注意,在返回的时候,编译器做了优化处理,把str处理成了右值。

C++11_第4张图片

C++11_第5张图片

C++11_第6张图片

假如有ret返回值接收,有移动构造调用移动构造,没有移动构造就调用拷贝构造。

假如没有ret返回值接收,这个时候就不会被优化了。如果有移动构造调用移动构造,没有就调用拷贝构造。

C++11_第7张图片

7.3.2移动赋值

如果已经对于一个已经存在的对象,进行赋值则调用一次构造一次赋值,编译器没法优化。

C++11_第8张图片

如果此时有移动构造,则变成一次移动构造+赋值。(编译器将马上返回的左值识别成右值)

image-20220130193839382

此时有一个函数叫移动赋值。如果当同时存在移动构造和移动赋值的时候,这个场景就变了。

// 移动赋值
string& operator=(string&& s)
    {
        cout << "string(string&&s) -- 移动赋值" << endl;
        this->swap(s);
        return *this;
    }

C++11_第9张图片

image-20220130195241755

右值引用在移动构造和移动赋值类似的地方最大价值是和左值引用进行区分,当是左值就匹配左值引用,是右值则匹配右值引用。

7.3.3小结

右值引用出来以后,并不是直接使用右值引用去减少拷贝,提高效率,而是支持深拷贝的类,提供移动构造和移动赋值,这时这些类的对象进行传值返回或者是参数为右值引用时,则可以用移动构造和移动赋值,转移资源,避免深拷贝,提高效率。

而且移动构造和移动赋值是如果有更好,没有的话就调用拷贝构造或者赋值。即对于传值返回值来说优先移动构造或移动赋值。

没有使用右值引用返回的场景。

右值引用使用场景1,主要是对深拷贝的类,如:string,vector,list等等增加移动构造和移动赋值。

C++11后STL中的容器都增加了移动构造和移动赋值。

正常情况下左值做深拷贝,右值去做移动拷贝和赋值。

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

7.3.4.1std::move

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?

因为有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。

template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{
// forward _Arg as movable
 return ((typename remove_reference<_Ty>::type&&)_Arg);
}
int main()
{
    Y::string s1("hello");//构造函数

    Y::string s2 = s1;//左值,调用拷贝构造
    Y::string s3 = Y::to_string(1234);//右值,一次移动构造

    Y::string s4 = std::move(s1);//move处理以后, 左值s1会被当成右值,调用移动构造
    // 但是这里要注意,一般是不要这样用的,因为我们会发现s1的资源被转移给了s4,s1被置空了。
}

C++11_第10张图片

7.3.4.2STL中增加的右值引用版本传参

C++11_第11张图片

C++11_第12张图片

int main()
{
     list<Y::string> lt;
     Y::string s1("1111");
    // 这里调用的是拷贝构造
     lt.push_back(s1);//左值只能匹配左值匹配的版本
    // 下面调用都是移动构造
     lt.push_back("2222");
     lt.push_back(Y::string("2222"));
     lt.push_back(std::move(s1));
     return 0;
}

C++11_第13张图片

左值引用后调用拷贝构造,根据原本的string对象new一份新的。而右值引用调用移动构造,直接将这一份替换给newnode。

C++11_第14张图片

这里要注意一下moves1就为空了。

7.3.4.2emplace_xxx接口

这部分要先看完美转发和可变参数模板后更好理解。多出的构造是因为自己的string类中的拷贝构造是复用构造的。

这里的&&在模板中是万能引用,实参是左值,参数包的这个形参就是左值引用;实参是左值,参数包的这个形参就是右值引用。

template <class... Args>
  void emplace_back (Args&&... args);
//展开函数
template<class ...Args>
void ShowList(Args&&... args)
{
    //列表初始化
    //{(PrintArg(args),0)...}将会展开成
    //((Printarg(arg1),0),(Printarg(arg2),0),(Printarg(arg3),0),...);
    //(Printarg(arg1),0)可以通过逗号表达式最后取0
    int arr[] = { (PrintArg(args),0)... };
    cout << endl;
}
int main()
{
    ShowList();
    string str = "B";
    ShowList(1 ,2 ,3 ,4 ,5,str);//左值也可以调用,&&是万能引用
    ShowList(1, 'A');
    ShowList(1, 'A', std::string("abc"));
}

image-20220131233135367

首先我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用。那么相对insert和emplace系列接口的优势到底在哪里呢?

C++11_第15张图片 C++11_第16张图片
class Date
{
    public:
 		Date(int year =1 ,int month = 1 ,int day =1)
            :_year(year),
    		_month(month),
    		_day(day)
            {z}
    private:
    	int _year;
    	int _month;
    	int _day;
}
int main()
{
    std::list< std::pair<int, char> > mylist;
    // emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
    // 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别
    mylist.emplace_back(10, 'a');
    mylist.emplace_back(20, 'b');
    mylist.emplace_back(make_pair(30, 'c'));
    mylist.push_back(make_pair(40, 'd'));
    mylist.push_back({ 50, 'e' });
    for (auto e : mylist)
        cout << e.first << ":" << e.second << endl;
    
    std::list<Date> lt;
    Date d1;
    lt.push_back(d1);//push_back接收左值
    lt.push_back(Date(2022,1,20));//push_back()接收右值
    lt.push_back({2021,1,21});//push_back()接收右值
    
    lt.emplace_back(d1);
    lt.emplace_back(Date(2022,1,20));
    //lt.emplace_back({2022,1,21});//error:{2022,1,21}识别成initializer_list
    
    lt.emplace_back(2022,1,21);//区别,前面是构造再拷贝构造,或者构造再移动;这里是直接用参数构造。
    return 0;
}

stl中new是申请空间适配器上的空间,并不是调用全局new,因此调用定位new对已经申请出的空间(该地址)调用构造函数。

emplace_back和push_back不同的地方是前者想达到的目的是在插入对象的时候可以直接传对象的参数,直接就去调用构造,少一次最开始的拷贝。而直接传左值需要先构造再拷贝构造,而传右值先拷贝然后调用移动构造。最后一个就是直接用参数构造即可。

C++11_第17张图片

因此不是说使用emplace_back就高效,对于第一种没有什么区别,对于第二种可能存在编译器优化,第三种是高效。

int main()
{
    std::list< std::pair<int, Y::string> > mylist;
    std::pair<int, Y::string> kv(10, "left");
    mylist.emplace_back(kv);
    mylist.emplace_back(std::pair<int, Y::string>(10, "left"));
    mylist.emplace_back(make_pair(10, "left"));//编译器优化
    mylist.emplace_back(10, "left");
    return 0;
}

其中对于make_pair的自动推演存在编译器优化。

C++11_第18张图片

int main()
{
    std::list< std::pair<int, Y::string> > mylist;
    std::pair<int, Y::string> kv(10, "left");
    mylist.emplace_back(kv);
    mylist.emplace_back(std::pair<int, Y::string>(10, "left"));
    mylist.emplace_back(make_pair(10, "left"));//编译器优化
    mylist.emplace_back(10, "left");

    cout << endl;

    mylist.push_back(kv);//左值
    mylist.push_back(std::pair<int, Y::string>(10, "left"));//右值,push_back也重载右值引用进行移动赋值和构造
    return 0;
}

C++11_第19张图片

  • 总结emplace_xxx的优势是第三种,直接传构造对象参数包。对于第一种和第二种push_back也能做到。
7.3.4.3小结

右值引用使用场景二,还可以使用在容器插入接口函数中,如果实参是右值,则可以转移它的资源,减少拷贝。

7.4完美转发

右值引用的对象,再作为实参传递时,属性会退化为左值,只能匹配左值引用。使用完美转发可以保证原有的属性。std::forward(t)保持t的属性,实参是左值,保持t的左值属性;实参是右值,保持t的右值属性。

7.4.1模板参数&&的万能引用

  • 模板中的&&万能引用

平时我们写具体类型的&&是右值引用,模板中的&&不代表右值引用,而是万能引用。既然接收左值又能接收右值。而普通函数中的&&只能接收右值。

模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要下面的完美转发。

#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);
}
int main()
{
     PerfectForward(10);           // 右值
     int a;
     PerfectForward(a);            // 左值
     PerfectForward(std::move(a)); // 右值
     const int b = 8;
     PerfectForward(b);      // const 左值
     PerfectForward(std::move(b)); // const 右值
     return 0;
}

C++11_第20张图片

7.4.2完美转发

std::forward完美转发在传参的过程中保留对象原生类型属性。

#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(std::forward<T>(t));
}
int main()
{
     PerfectForward(10);           // 右值
     int a;
     PerfectForward(a);            // 左值
     PerfectForward(std::move(a)); // 右值
     const int b = 8;
     PerfectForward(b);      // const 左值
     PerfectForward(std::move(b)); // const 右值
     return 0;
}

C++11_第21张图片

7.4.3完美转发场景

以下代码的情况不能实现移动构造,右值引用的对象,再作为实参传递时,属性会退化为左值,只能匹配左值引用。因此都是调用深拷贝的insert

原因在于,右值引用注意点中提及,右值引用之后是有空间的,可以取地址。因此26行可以打印出地址。其他对象再来引用x,x会识别为左值。

需要使用完美转发让x保持它的右值属性。

同时注意之后的每一层传参都要完美转发才能保持右值属性。

namespace Y{
template<class T>
struct ListNode
{
     ListNode* _next = nullptr;
     ListNode* _prev = nullptr;
     T _data;
};
template<class T>
class List
{
 typedef ListNode<T> Node;
public:
    List()
    {
        _head = new Node;
        _head->_next = _head;
        _head->_prev = _head;
    }
    void PushBack(T& x)
    {
        Insert(_head, x);
    }
    void PushBack(T&& x)
    {
        //cout<<&x<
        Insert(_head, x);//这种情况右值引用的属性就会退化。
        //Insert(_head, std::forward(x));//需要完美转发的场景
    }
    void Insert(Node* pos, T&& x)
    {
        Node* prev = pos->_prev;
        Node* newnode = new Node;
        newnode->_data = std::forward<T>(x); // 关键位置,这里也要用完美转发。之后的每一层传参都要完美转发。
        // prev newnode pos
        prev->_next = newnode;
        newnode->_prev = prev;
        newnode->_next = pos;
        pos->_prev = newnode;
    }
    void Insert(Node* pos, const T& x)
    {
        Node* prev = pos->_prev;
        Node* newnode = new Node;
        newnode->_data = x; // 关键位置
        // prev newnode pos
        prev->_next = newnode;
        newnode->_prev = prev;
        newnode->_next = pos;
        pos->_prev = newnode;
    }
    private:
    Node* _head;
};
}
int main()
{
     List<Y::string> lt;
     Y::string s1("1111");
     lt.PushBack(s1);
     
     lt.PushBack("1111");
     lt.PushFront("2222");
     return 0;
}

C++11_第22张图片

同时注意stl中的new是从空间适配器获得的,所以我们这里要仿照进行定位new从而获得类似的效果。

namespace Y {
    template<class T>
    struct ListNode
    {
        ListNode* _next = nullptr;
        ListNode* _prev = nullptr;
        T _data;
    };
    template<class T>
    class List
    {
        typedef ListNode<T> Node;
    public:
        List()
        {
            _head = new Node;
            _head->_next = _head;
            _head->_prev = _head;
        }
        void PushBack(const T& x)
        {
            Insert(_head, x);
        }
        void PushBack(T&& x)/*万能引用*/
        {
            //Insert(_head, x);
            Insert(_head, std::forward<T>(x));
        }
        void PushFront(T& x)
        {
            Insert(_head->_next, x);
        }
        void PushFront(T&& x)
        {
            Insert(_head->_next, std::forward<T>(x));
        }
        void Insert(Node* pos, T&& x)
        {
            Node* prev = pos->_prev;
            //Node* newnode = new Node;
            //newnode->_data = std::forward(x); // 关键位置
            // prev newnode pos
            Node* newnode = (Node*)malloc(sizeof(Node));
            new(newnode)T(std::forward<T>(x));

            prev->_next = newnode;
            newnode->_prev = prev;
            newnode->_next = pos;
            pos->_prev = newnode;
        }
        void Insert(Node* pos, const T& x)
        {
            Node* prev = pos->_prev;
            //Node* newnode = new Node;
            //newnode->_data = x; // 关键位置
            // prev newnode pos
            Node* newnode = (Node*)malloc(sizeof(Node));
            new(newnode)T(x);
            prev->_next = newnode;
            newnode->_prev = prev;
            newnode->_next = pos;
            pos->_prev = newnode;
        }
    private:
        Node* _head;
    };
}
int main()
{
    Y::List<Y::string> lt;
    Y::string s1("1111");
    lt.PushBack(s1);

    lt.PushBack("1111");
    lt.PushFront("2222");
    return 0;
}

image-20220131142318536

8.新的类功能

原来C++类中,有6个默认成员函数:构造函数,析构函数,拷贝构造函数,拷贝赋值重载,取地址重载,const取地址重载。重要的是前4个,后两个用处不大。

默认成员函数就是我们不写编译器会生成一个默认的。C++11新增了两个:移动构造函数和移动赋值运算符重载。因此现在有8个默认成员函数。

8.1默认移动构造

默认生成移动构造的条件:

  • 如果自己没有实现移动构造函数,**且都没有实现析构函数、拷贝构造、拷贝赋值重载。**那么编译器会自动生成一个默认移动构造。
    • 默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝。对于自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用该成员的移动构造,没有实现就调用拷贝构造。

对于此时没有实现拷贝,析构和赋值重载的Person类,编译器自动生成一个默认移动构造,对于其自定义类型string,如果实现了移动构造此时则会调用移动构造。

class Person
{
public:
    Person(const char* name = "", int age = 0)
        :_name(name)
        , _age(age)
    {}
    /*Person(const Person& p)
    :_name(p._name)
    ,_age(p._age)
    {}*/
    /*Person& operator=(const Person& p)
    {
    if(this != &p)
    {
    _name = p._name;
    _age = p._age;
    }
    return *this;
    }*/
    /*~Person()
    {}*/
private:
    Y::string _name;
    int _age;
};
int main()
{
    Person s1;
    //Person s2 = s1;
    Person s3 = std::move(s1);//右值,能调用Person3的默认移动构造。
    //Person s4;
    //s4 = std::move(s2);
    return 0;
}

image-20220131144859020

对于此时实现拷贝,析构和赋值重载中一个的Person类,此时不会生成移动构造。对Person类调用拷贝构造。因此对于其自定义类型string只会调用拷贝构造。

class Person
{
public:
    Person(const char* name = "", int age = 0)
        :_name(name)
        , _age(age)
    {}
    /*Person(const Person& p)
    :_name(p._name)
    ,_age(p._age)
    {}*/
    /*Person& operator=(const Person& p)
    {
    if(this != &p)
    {
    _name = p._name;
    _age = p._age;
    }
    return *this;
    }*/
    ~Person()
    {}
private:
    Y::string _name;
    int _age;
};
int main()
{
    Person s1;
    //Person s2 = s1;
    Person s3 = std::move(s1);//右值,调用Person3的默认移动拷贝
    //Person s4;
    //s4 = std::move(s2);
    return 0;
}

image-20220131145255684

8.2默认移动赋值

如果你没有自己实现移动赋值重载函数,且都没有实现析构函数、拷贝构造、拷贝赋值重载函数,那么编译器会自动生成一个默认移动赋值。

默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,对于自定义类型成员,则需要看这个成员是否实现移动赋值。如果实现了就调用该成员的移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)

class Person
{
public:
    Person(const char* name = "", int age = 0)
        :_name(name)
        , _age(age)
    {}
    /*Person(const Person& p)
    :_name(p._name)
    ,_age(p._age)
    {}*/
    /*Person& operator=(const Person& p)
    {
    if(this != &p)
    {
    _name = p._name;
    _age = p._age;
    }
    return *this;
    }*/
   /* ~Person()
    {}*/
private:
    Y::string _name;
    int _age;
};
int main()
{
    Person s1;
    Person s2;
    Person s3 = std::move(s1);
    Person s4;
    s4 = std::move(s2);
    return 0;
}

image-20220131145818121

8.3小结

C++对自定义类型成员变量非常地友好,默认成员函数都会恰当地处理自定义类型成员。

如下不写构造拷贝析构会自己调用,同时此时会生成移动构造和移动赋值,对于右值可以使用。

class MyQueue
{
    private:
    	private:
    std::stack<int> _pushST;
    std::stack<int> _popST;
}

8.4类内对象声明给缺省

C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化,这个之前在类和对象中提及,这里就不再细讲了。

8.5default强制生成默认函数

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

对于只写了拷贝构造的情况就不会生成,想要默认构造函数就可以用default强制生成。

对下下述情况就不会生成默认构造函数和默认移动构造从而编译不过。

class Person
{
    public:
      Person(const Person& p)
          :_name(p._name)
          ,_age(p._age)
      {	}
     private:
    	string _name;
    	int _age;
};
int main()
{
    Person p1;
    Person p2(std::move(p1));
}

default可以对不生成的强制生成,从而可以进行移动构造。

class Person
{
    public:
      Person() = default;
      Person(Person&& p) = default;
      Person(const Person& p)
          :_name(p._name)
          ,_age(p._age)
      {	}
     private:
    	string _name;
    	int _age;
};
int main()
{
    Person p1;
    Person p2(std::move(p1));
}

image-20220131154026670

8.6delete强制禁止默认函数

如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

具体使用场景在特殊类的设计中提及了。

8.7final和override

final修饰类,这个类不能被继承。

final修饰虚函数,这个虚函数不能被重写。

override修饰子类重写的虚函数,检查是否完成重写,如果没有就报错。

一般纯虚函数,才是要求子类强制重写,如果子类不重写,就不能完成实例化。

在继承和多态部分学习了,不清楚了就复习。

9.可变模板参数

在C语言中其实是有函数可变参数相关内容的。

#include
#include

int average(int n,...){
    va_list arg;//char* 

    int res=n;
    int sum=0;

    va_start(arg,n);//arg=(char*)&n;arg+=sizeof(n);

    while(res--){
      sum+=va_arg(arg,int);//*((int*)arg); arg+=sizeof(int)
    }
    va_end(arg);//arg=NULL
    return sum/n;

}

int main(){
    int a=5;int b=10;int c=30;
    int arg1=average(2,a,c); 
    int arg2=average(3,a,b,c);

    printf("avgof(a,c)=%d\n",arg1);
    printf("\n");
    printf("avgof(a,b,c)=%d\n",arg2);
}

C++11的新特性可变参数模板能够让我们创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现阶段学习基础的可变参数模板特性就够。以后如果有需要,再可以深入学习。

下面就是一个基本可变参数的函数模板。

// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}

上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以要学习一些奇招来一一获取参数包的值。

首先要知道是可以通过sizeof...(args)获得一共有多少参数的。

语法不支持使用args[i]这样方式获取可变参数的原因在于是模板,模板不是一个可调用的东西,单是ShowList(Args... args)只能推演出是一个参数包。而具体的每个参数需要进一步要在编译过程一个个推演,这里并没有进一步地一个个推演,因此没法直接用args[i]获得。

9.1递归函数方式展开参数包

既然可以获得参数整体个数,我们可以通过递归慢慢拆分,从而取出一个个参数个数。但是这里递归的结束条件不能和常规的一致。

我们的if是一个运行时逻辑,展开参数包是编译时逻辑。模板要实例化,第一个参数有具体类型了才推演出具体的函数。也就是说需要一个个去推演才能生成一个个ShowList函数,推演是编译过程,因此推演的过程到底就存在问题了。而if判断起效果编译完成了后实际运行才起,并没有起到作用。

template<class T,class ...Args>
void ShowList(T value,Args... args)
{
    cout<<value<<endl;
    /*这种方法是行不通的。*/
    if(sizeof...(args) == 0){
        return ;
    }
    ShowList(args...);
}
int main()
{
    ShowList(1,'A',std::string("abc"));
}
void ShowList()
{
    cout<<"结束了"<<endl;
}
template<class T,class ...Args>
void ShowList(T value,Args... args)
{
    cout<<value<<endl;
    ShowList(args...);//推到空参数包的时候就去调用那个空参数的函数就可以结束了。
}
int main()
{
    ShowList();
    ShowList(1);
    ShowList(1,'A');
    ShowList(1,'A',std::string("abc"));
}

9.2逗号表达式展开参数包

这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的。而用于构造的数组的在过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。

数组的列表初始化需要知道元素的个数,因此可以把args...进行数组的列表初始化从而获得相关元素。但是C++中只能存类型相同的,因此下面代码的情况只能处理全部类型相同的情况。

//展开函数
template<class ...Args>
void ShowList(Args... args)
{
    //列表初始化
    int arr[] = { args... };
    cout << endl;
}
int main()
{
    ShowList(1 ,2 ,3 ,4 ,5);
   /* ShowList(1, 'A');
    ShowList(1, 'A', std::string("abc"));*/
}

利用逗号表达式来处理。

template<class T>
void PrintArg(T x)
{
    cout << x << " ";
}
void ShowList()
{

}
//展开函数
template<class ...Args>
void ShowList(Args... args)
{
    //列表初始化
    //{(PrintArg(args),0)...}将会展开成
    //((Printarg(arg1),0),(Printarg(arg2),0),(Printarg(arg3),0),...);
    //(Printarg(arg1),0)可以通过逗号表达式最后取0
    int arr[] = { (PrintArg(args),0)... };
    cout << endl;
}
int main()
{
    ShowList();
    ShowList(1 ,2 ,3 ,4 ,5);
    ShowList(1, 'A');
    ShowList(1, 'A', std::string("abc"));
}

或者利用返回值来达到数组的初始化。

template<class T>
int PrintArg(T x)
{
    cout << x << " ";
	return 0;
}
void ShowList()
{

}
//展开函数
template<class ...Args>
void ShowList(Args... args)
{
    //列表初始化
    int arr[] = { PrintArg(args)... };
    cout << endl;
}
int main()
{
    ShowList();
    ShowList(1 ,2 ,3 ,4 ,5);
    ShowList(1, 'A');
    ShowList(1, 'A', std::string("abc"));
}

10.lambda表达式

10.1lambda出现场景

在之前学习中,加上这次的学习,可调用对象类型有

  • 仿函数
  • 函数指针
  • lambda表达式

在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法。

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

如果待排序元素为自定义类型,需要用户定义排序时的比较规则:

要求分别按名字、价格、数量进行排序,升序或降序。

虽然下面的代码能够实现不同的排序,但是对于不同的比较要求需要各自写一个仿函数。如果仿函数名是按照比较规则命名的,还方便理解,如果命名不规范,比如Compare1,Compare2...那么就难受了。

struct Goods
{
     string _name;
     double _price;
     int _num;
};
struct ComparePriceLess
{
     bool operator()(const Goods& gl, const Goods& gr)
     {
     	return gl._price <= gr._price;
     }
};
struct CompareNumLess
{
     bool operator()(const Goods& gl, const Goods& gr)
     {
     	return gl._num <= gr._num;
     }
};
int main()
{
     vector<Goods> v= { { "苹果", 2.1,300 }, { "相交", 3,200 }, { "橙子", 2.2,100 }, {"菠萝", 1.5,100} };
     sort(v.begin(),v.end(),ComparePriceLess());
     sort(v.begin(),v.end(),CompareNumLess());
     return 0;
}

那么有没有办法解决这么多次定义仿函数呢?仿函数定义在全局,而lambda定义在局部。

struct Goods
{
     string _name;
     double _price;
     int _num;
};
struct ComparePriceLess
{
     bool operator()(const Goods& gl, const Goods& gr)
     {
     	return gl._price <= gr._price;
     }
};
struct CompareNumLess
{
     bool operator()(const Goods& gl, const Goods& gr)
     {
     	return gl._num <= gr._num;
     }
};
int main()
{
     //完整实现
     //auto f1= [](const Goods& g1,const Goods& g2)->bool {return g1._price <= g2._price;};
     
     vector<Goods> v= { { "苹果", 2.1,300 }, { "相交", 3,200 }, { "橙子", 2.2,100 }, {"菠萝", 1.5,100} };
     sort(v.begin(),v.end(),f1);
     sort(v.begin(),v.end(),[](const Goods& g1,const Goods& g2)
          {
              return g1._num <= g2._num;
          };
         );
     sort(v.begin(),v.end(),[](const Goods& g1,const Goods& g2){return g1._price > g2._price;};);
     return 0;
}

10.2lambda表达式语法

lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }

[capture-list] (parameters) mutable ->return-type { statement }
捕捉列表 参数 返回值类型 函数实现
不能省略 可以省略 不可省略 可以省略 不能省略
该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用 与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略 默认情况下,**lambda函数总是一个const函数,**mutable可以取消其常量性。**使用该修饰符时,参数列表不可省略(即使参数为空)。**也就是说默认值传递方式捕捉过来的都是带const属性。 返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。 在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
  • 捕获列表说明

捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。

  • [var]:表示值传递方式捕捉变量var
  • [=]:表示值传递方式捕获所有父作用域中的变量(成员函数中包括this)
  • [&var]:表示引用传递捕捉变量var;
  • [&]:表示引用传递捕捉所有父作用域中的变量(成员函数中包括this);
  • [this]:表示值传递方式捕捉当前的this指针

注意:

  1. 父作用域指包含lambda函数的语句块{}部分)
  2. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
    1. 比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
    2. [&, a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
    3. c捕捉列表不允许变量重复传递,否则就会导致编译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a,重复
    4. 在块作用域以外的lambda函数捕捉列表必须为空。(如果labmbda函数写到全局,捕捉列表得为空)
    5. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
    6. lambda表达式之间不能相互赋值,即使看起来类型相同。因为每个表达式是不同的类型。(在底层原理中介绍)

最简单的lambda表达式

int main()
{
    []{}
}

实现a+b的lambda

一般借助auto调用

int main()
{
    int a = 1; int b =2;
    //实现add的lambda
    auto add = [](int x,int y)->int {return x+y ;};
    cout<<add(a,b)<<endl;
    //在捕捉列表,捕捉a,b,没有参数可以省略参数列表,返回值可以通过推演,也可以省略
    //auto add1 = [a,b]()->int{return a+b+10 ;};
    auto add1 = [a,b]{return a+b+10;}
	cout<< add1()<<endl;
}

实现a和b交换的lambda的表示式

int main()
{
	int x = 1; int y = 2;
	//标准写法1
	auto myswap=[](int& a, int& b)->void {int tmp = a; a = b; b = tmp; };
	cout << x << " " << y << endl;
	myswap(x, y);
	cout << x << " " << y << endl;
	//尝试利用捕捉列表,捕捉当前局部域的变量,这样就不用传参或减少传参,省略参数和返回值。
	//这里传值方式捕捉,拷贝外面的x和y给lamdba里面的x,y
	//lambda里面的a、b的改变不会影响外面
	auto myswap2 = [x, y]()mutable {int temp = x; x = y; y = temp; };
	myswap2();
	cout << x << " " << y << endl;

	/*以引用方式捕捉*/
	auto myswap3 = [&x, &y] {int temp = x; x = y; y = temp; };
	myswap3();
	cout << x << " " << y << endl;
    
    /*以引用方式全捕捉*/
	auto myswap4 = [&] {int temp = x; x = y; y = temp; };
	myswap4();
	cout << x << " " << y << endl;
	return 0;
}

image-20220201113113708

原理:捕捉的本质还是传参。就像范围for,看起来自动,但是底层是迭代器。

10.3lambda的底层原理

#pragma warning(disable : 4996)
#include
#include
#include
using namespace std;

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

C++11_第23张图片

实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如 果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。

11.包装器

11.1std::function包装器的使用和意义

目前为止,可调用对象类型:函数指针,仿函数(函数对象),lambda。

下面的func可能是什么?那么func可能是函数名?函数指针?函数对象(仿函数)?也有可能是lambda表达式。

ret = func(x);

以下程序会发现useF函数模板实例化了三份。

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 func(double i)
{
    return i / 2;
}
struct Functor
{
     double operator()(double d)
     {
     return d / 3;
     }
};
int main()
{
     // 函数名
     cout << useF(func, 11.11) << endl;
     // 函数对象
     cout << useF(Functor(), 11.11) << endl;
     // lamber表达式
     cout << useF([](double d)->double{ return d/4; }, 11.11) << endl;
     return 0;
}

C++11_第24张图片

包装器可以对上面的可调用对象进行封装,模板参数相同,以统一的方式进行实例化调用。并且看起来是一个统一的返回值和参数。

std::function在头文件<functional>
// 类模板原型如下
template <class T> function;     // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
Ret: 被调用函数的返回类型
Args…:被调用函数的形参包
#pragma warning(disable : 4996)
#include
#include
#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;
    }
};
class Plus
{
public:
    static int plusi(int a, int b)
    {
        return a + b;
    }
    double plusd(double a, double b)
    {
        return a + b;
    }
};
int main()
{
    // 包装函数指针
    std::function<int(int, int)> ff1 = f;
    cout << ff1(1, 2) << endl;

    // 包装仿函数
    std::function<int(int, int)> ff2 = Functor();/*仿函数对象*/
    cout << ff2(1, 2) << endl;
    
    // 包装静态成员函数
    std::function<int(int, int) > ff3 = Plus::plusi;
    cout << ff3(1, 2) << endl;

    // 包装成员函数
    std::function<double(Plus, double, double)> ff4 = &Plus::plusd;/*取成员函数的地址*/
    cout << ff4(Plus(), 1.1, 2.2) << endl;

    // lambda表达式
    std::function<int(int, int)> ff5 = [](const int a, const int b){return a + b; };

    cout << ff5(1, 2) << endl;


    return 0;

}

验证使用包装器后只实例化一份

#pragma warning(disable : 4996)
#include
#include
#include 
#include
using namespace std;

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 func(double i)
{
    return i / 2;
}
struct Functor
{
    double operator()(double d)
    {
        return d / 3;
    }
};
int main()
{
    // 函数名
    std::function<double(double)> f1 = func;
    cout << useF(f1, 11.11) << endl;
    // 函数对象
    std::function<double(double)> f2 = Functor();
    cout << useF(f2, 11.11) << endl;
    // lamber表达式
    std::function<double(double)> f3 = ([](double d)->double { return d / 4; });

    cout << useF(f3 , 11.11) << endl;
    return 0;
}

C++11_第25张图片

11.2std::function包装器的使用场景

包装器的使用场景是简化可调用对象的类型。

针对一些要使用switch或者if调用各种方法的时候就很方便了。举个简单四则计算器的例子,里面判断+、-、*、÷需要一个个判,而在网络中的部分判断特别多很麻烦。这时候就可以优化。

如果没有包装器的时候下面的部分value部分就要使用函数指针或者仿函数了,会麻烦。

map<string,std::function<int(int,int)>> opMap=
{
    {"+",[](int x,int y){return x+y;}},
    {"-",[](int x,int y){return x-y;}},
    {"*",[](int x,int y){return x*y;}},
    {"/",[](int x,int y){return x/y;}}
};
int main()
{
    st.push(opMap[str](left,right));
}

11.3std::function小结

总结一下,std::funtion是包装各种可以调用的对象,统一可调用对象类型,并且指定了参数和返回类型。

为什么有std::funciton,因为不包装前可调用类型存在很多问题:

  1. 函数指针类型太复杂,不方便使用和理解。
  2. 仿函数类型是一个类名,没有指定调用参数和返回值。比如之前的红黑树封装map和set的时候需要KeyOfValue的模板参数,而直接一个KeyofValue根本看不见参数和返回值,得去看operator()才能看出来。
  3. lambda表达式在语法层,看不到类型(用auto接收),底层有类型,基本是lambda_uuid,也很难看。

std::bind调整可调用类型的参数。

11.4std::bind

std::bind调整可调用类型的参数。通常是进行一些参数调节。

std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。

一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M 可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作。

std::bind的函数原型:

// 原型如下:
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2) 
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);

使用方法:

#include
#include 

using namespace std;

int Plus(int a, int b)
{
    return a + b;
}
class Sub
{
public:
    int sub(int a, int b)
    {
        return a - b;
    }
};
int main()
{
    //表示绑定函数plus 参数分别由调用 func1 的第一,二个参数指定
    std::function<int(int, int)> f1 = std::bind(Plus, placeholders::_1,placeholders::_2);
    cout << f1(1, 2) << endl;

    //想把plus绑定成一个值+10
    std::function<int(int)> f2 = bind(Plus, 10, placeholders::_1);
    cout << f2(5) << endl;
    return 0;
}

使用场景:某些函数调用有一个参数是固定的,比如说非静态成员函数

  • 绑定固定的可调用对象

成员函数固定要传一个参数,可以把这个参数提前进行绑定。

#include
#include 

using namespace std;

int Plus(int a, int b)
{
    return a + b;
}
class Sub
{
public:
    int sub(int a, int b)
    {
        return a - b;
    }
};
int main()
{
    //表示绑定函数plus 参数分别由调用 func1 的第一,二个参数指定
    std::function<int(int, int)> f1 = std::bind(Plus, placeholders::_1,placeholders::_2);
    cout << f1(1, 2) << endl;

    //想把plus绑定成一个值+10
    std::function<int(int)> f2 = bind(Plus, 10, placeholders::_1);
    cout << f2(5) << endl;

    //只有std::function包装器的时候
    std::function<int(Sub, int, int)> ff4 = &Sub::sub;/*取成员函数的地址*/
    cout << ff4(Sub(), 1, 2) << endl;

    //绑定固定的可调用对象,包装器就不用每次都传对象了
    std::function<int(int, int)> f3 = bind(&Sub::sub, Sub(), placeholders::_1, placeholders::_2);
    cout << f3(1, 2) << endl;
    return 0;
}

image-20220201140221939

  • 交换参数顺序

附加价值,比如说原来的交换参数不习惯的话可以换一下。

#include
#include 

using namespace std;

int Plus(int a, int b)
{
    return a + b;
}
class Sub
{
public:
    int sub(int a, int b)
    {
        return a - b;
    }
};
int main()
{
    //表示绑定函数plus 参数分别由调用 func1 的第一,二个参数指定
    std::function<int(int, int)> f1 = std::bind(Plus, placeholders::_1,placeholders::_2);
    cout << f1(1, 2) << endl;

    //想把plus绑定成一个值+10
    std::function<int(int)> f2 = bind(Plus, 10, placeholders::_1);
    cout << f2(5) << endl;

    //只有std::function包装器的时候
    std::function<int(Sub, int, int)> ff4 = &Sub::sub;/*取成员函数的地址*/
    cout << ff4(Sub(), 1, 2) << endl;

    //绑定固定的可调用对象,包装器就不用每次都传对象了
    std::function<int(int, int)> f3 = bind(&Sub::sub, Sub(),placeholders::_2,placeholders::_1);
    cout << f3(1, 2) << endl;
    return 0;
}

12.线程库

在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。

sleep_untilsleep_for

12.1thread类接口

函数名 功能
thread() 构造一个线程对象,没有关联任何线程函数,即没有启动任何线程
thread(fn,args1,args2,…) 构造一个线程对象,并关联函数fn,args1,args2,…为线程函数的参数
get_id() 获取线程id
joinable() 线程是否还在执行,joinable代表的是一个正在执行中的线程
join() 该函数调用后会阻塞住线程,当线程结束后,主线程继续执行(阻塞式等待)
detach() 线程分离

12.2get_id()和this_thread::get_id()

  1. 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态。
  2. 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。
#include 
using namespace std;
int main()
{
     thread t1;
     cout << t1.get_id() << endl;
     return 0;
}

如果想在线程的函数执行体中知道是哪个线程在调用则在该部分调用this_thread::get_id()

#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;



void ThreadFunc2(int* x)
{
    cout << this_thread::get_id() << endl;
    *x += 10;
}
int main()
{
    int a = 10;

    thread t2(ThreadFunc2, &a);
    cout << "a=" << a << endl;//输出10
    t2.join();

    return 0;
}

12.3关联线程函数

当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。线程函数一般情况下可按照以下三种方式提供:

  • 函数指针
  • lambda表达式
  • 函数对象

在Linux的原生pthread库中给线程传的就是函数指针。在C++中就可以使用模板了。

C++11_第26张图片

Fn为可调用对象(函数指针,仿函数对象,lambda,function/bind包装过的可执行对象),args是参数列表。和原生phread库相比,不是只能传void*的函数指针。

  • 无参构造可以用作线程池。

  • thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行。

  • 可以通过joinable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效

    • 采用无参构造函数构造的线程对象
    • 线程对象的状态已经转移给其他线程对象
    • 线程已经调用join或者detach结束
#pragma warning(disable : 4996)
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

mutex mtx;
void ThreadFunc(int a)
{
    cout << "Thread1" << a << endl;
}
class TF
{
public:
    void operator()()
    {
        cout << "Thread3" << endl;
    }
};
int main()
{
    //线程函数为函数指针
    thread t1(ThreadFunc, 10);

    //线程函数为lambda表达式
    thread t2([] {cout << "Thread2" << endl; });

    //线程函数为仿函数对象
    TF tf;
    thread t3(tf);


    t1.join();
    t2.join();
    t3.join();
    cout << "Main thread" << endl;
    return 0;
}
// example for thread::operator=
#include        // std::cout
#include          // std::thread, std::this_thread::sleep_for
#include          // std::chrono::seconds
 
void pause_thread(int n) 
{
  std::this_thread::sleep_for (std::chrono::seconds(n));
  std::cout << "pause of " << n << " seconds ended\n";
}

int main() 
{
  std::thread threads[5];                         // default-constructed threads

  std::cout << "Spawning 5 threads...\n";
  for (int i=0; i<5; ++i)
    threads[i] = std::thread(pause_thread,i+1);   // move-assign threads

  std::cout << "Done spawning threads. Now waiting for them to join:\n";
  for (int i=0; i<5; ++i)
    threads[i].join();

  std::cout << "All threads joined!\n";

  return 0;
}

12.4线程函数参数

线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。

Tips:如果是类成员函数作为线程参数时,必须将this作为线程函数参数

#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;


void ThreadFunc1(int& x)
{
    x += 10;
}
void ThreadFunc2(int* x)
{
    *x += 10;
}
int main()
{
    int a = 10;

    thread t2(ThreadFunc2, &a);
    cout << "a=" << a << endl;//输出10
    t2.join();

    // 在线程函数中对a修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,但其实际引用的是线程栈中的拷贝
    // 如果先搞更改外部实参时,需要使用std::ref(a)
    thread t1(ThreadFunc1, std::ref(a));
    t1.join();
    cout << "a=" << a << endl;//输出30

    return 0;
}

12.5原子性操作库

多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。

比如:经典的i++问题

对于一条i++语句,其汇编并不是一条件语句,至少是三条,取数据,加数据,放回数据。因此对于不是原子变量的操作要想保证线程安全通常得加锁。

  • 假如要完成对数字的++操作,该种方式需要加锁在循环内部导致开锁解锁的上下文切换花费大
#include 
#include 
#include 
#include 
#include 
using namespace std;

int main()
{
    int n; cin >> n;
    vector<thread> vthreads;
    vthreads.resize(n);
    mutex mtx;
    int N = 1000;
    int x = 0;
    for (auto& td : vthreads)
    {
        td = thread([&mtx, &N,&x] 
            {
                for (int i = 0; i < N; ++i)
                {
                    mtx.lock();
                    cout << this_thread::get_id() << ":" << x << endl;;
                    ++x;
                    //this_thread::sleep_for(chrono::milliseconds(2000));/*等其他线程申请锁*/
                    mtx.unlock();
                }           
            }
        );
    }
    for (auto& td : vthreads)
    {
        td.join();
    }
    cout << n << "个线程并行对x++了" << N << "次,x="<<x<< endl;
    return 0;
}
  • 而该种方式的++会导致两个线程变成串行
#include 
#include 
#include 
#include 
#include 
using namespace std;

int main()
{
    int n; cin >> n;
    vector<thread> vthreads;
    vthreads.resize(n);
    mutex mtx;
    int N = 1000;
    int x = 0;
    for (auto& td : vthreads)
    {
        td = thread([&mtx, &N,&x] 
            {
                mtx.lock();/*变成串行*/
                for (int i = 0; i < N; ++i)
                {
                    cout << this_thread::get_id() << ":" << x << endl;;
                    ++x;
                    //this_thread::sleep_for(chrono::milliseconds(2000));/*等其他线程申请锁*/
                }    
                mtx.unlock();
            }
        );
    }
    for (auto& td : vthreads)
    {
        td.join();
    }
    cout << n << "个线程并行对x++了" << N << "次,x="<<x<< endl;
    return 0;
}

从上述两个场景可以看出,虽然加锁可以解决,但是加锁有一个缺陷就是:只要一个线程在对sum++时,其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁。

那还有什么额外的实现原子性操作?

pthread库下有type __sync_fetch_and_add(type* pb,type value)实现,而pthread库的mutex互斥锁底层也是XCHG指令。

C++11中引入了原子操作。所谓原子操作:即不可被中断的一个或一系列操作,C++11引入 的原子操作类型,使得线程间数据的同步变得非常高效。

原子性的操作是底层指令集汇编实现的,linux和windows实现方式不同,c++11将两者封装好了。其保证互斥访问的机制类似自旋锁。

其原子的声明格式:atomic name

C++11_第27张图片

使用原子++

#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

int main()
{
    int n; cin >> n;
    vector<thread> vthreads;
    vthreads.resize(n);
    mutex mtx;
    int N = 1000;
    atomic<int> x = 0;
    //atomic_int x ={0};另一种定义
    for (auto& td : vthreads)
    {
        td = thread([&mtx, &N,&x] 
            {
                for (int i = 0; i < N; ++i)
                {
                    ++x;          
                }           
            }
        );
    }
    for (auto& td : vthreads)
    {
        td.join();
    }
    cout << n << "个线程并行对x++了" << N << "次,x="<<x<< endl;
    return 0;
}

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

#include 
int main()
{
    atomic<int> a1(0);
    //atomic a2(a1);   // 编译失败
    atomic<int> a2(0);
    //a2 = a1;               // 编译失败
    return 0;
}

12.6锁

C++11_第28张图片

12.6.1mutex种类

  1. std::mutex

C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动。

mutex最常用的三个函数:

函数名 函数功能
lock() 上锁:锁住互斥量
unlock() 解锁:释放对互斥量的所有权
try_lock() 尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞

调用lock的情况:

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

调用try_lock()时的情况:

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

2.recursive_mutex

std::recursive_mutex为递归锁。

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

递归函数中如果使用递归锁会识别是不是自己锁自己。

  1. 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。

  1. recursive_timed_mutex是递归时间锁。

12.6.1lock和unlock

lockunlock在pthread库中使用频繁,这里用法几乎一致。

声明一个mutex类型对象,将其作为参数进行加锁解锁即可。

12.6.2lock_guard

直接使用lock()unlock()是有缺陷的。

当在函数中执行的时候如何保证无论是正常执行,还是中途返回,还是抛异常的情况都一定释放锁。

lock_guard没有中途加减锁,要这个功能的是unique_lock

12.6.2.1 lock和unlock缺陷场景

中途返回场景:

#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

mutex mtx;
void func()
{
    mtx.lock();
    FILE* fout = fopen("test.txt","r");
    if( fout == nullptr) return;   //这样会导致离开带走了锁,导致后面的线程申请不了锁。
    //因此在离开的时候必须带锁 
    //if( fout== nullptr) {mtx.unlock();return;}
    mtx.unlock();
}
int main()
{
    try {
        func();
    }
    catch (exception& e)
    {

    }
}

抛出异常场景

#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

mutex mtx;
void func()
{
    mtx.lock();
    char* p =new char[n];//失败抛出异常
    mtx.unlock();
}
int main()
{
    try {
        func();
    }
    catch (exception& e)
    {

    }
}
12.6.2.2 lock_guard解决方法

C++11采用RAII的方式对锁进行了封装,即lock_guard和unique_lock。

lock_guard处理做法

  • 保护整个函数作用域
#pragma warning(disable : 4996)
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

namespace Y {
    template<class Lock>
    class lock_guard
    {
    public:
        lock_guard(Lock& lock)
            :_lock(lock)
        {
            _lock.lock();
            cout << "加锁" << endl;
        }
        ~lock_guard()
        {
            _lock.unlock();
            cout << "解锁" << endl;
        }
        lock_guard(const Lock& lock) =delete;
        lock_guard& operator=(const lock_guard&) = delete;
    private:
        Lock& _lock;
    };
}
mutex mtx;
void func()
{
    Y::lock_guard<mutex> lg(mtx);
    FILE* fout = fopen("test.txt", "r");
    if (fout == nullptr)
    {
        return;
    }
    int n; cin >> n;
    char* p = new char[n];
}
int main()
{
    func();
    return 0;
}

C++11_第29张图片

但是如果不想保护整个函数作用域,只想保护一部分,用{}进行匿名作用域。注意使用newdelete会面临和lockunlock一样的问题。

  • 匿名作用域
#pragma warning(disable : 4996)
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

namespace Y {
    template<class Lock>
    class lock_guard
    {
    public:
        lock_guard(Lock& lock)
            :_lock(lock)
        {
            _lock.lock();
            cout << "加锁" << endl;
        }
        ~lock_guard()
        {
            _lock.unlock();
            cout << "解锁" << endl;
        }
        lock_guard(const Lock& lock) =delete;
        lock_guard& operator=(const lock_guard&) = delete;
    private:
        Lock& _lock;
    };
}
mutex mtx;
void func()
{
    { //匿名作用域
        Y::lock_guard<mutex> lg(mtx);
        FILE* fout = fopen("test.txt", "r");
        if (fout == nullptr)
        {
            return;
        }
    }
    int n; cin >> n;
    char* p = new char[n];
}
int main()
{
    func();
    return 0;
}

通过上述代码可以看到,lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封装,在需要加锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题。

lock_guard的缺陷:太单一,用户没有办法对该锁进行控制,因此C++11又提供了 unique_lock

12.6.3unique_lock

假设需要在作用域中途解锁,就需要使用unique_lock。只要在lock_guard中加入lockunlock就行。

使用场景:

对于中间一部分代码是线程安全的,为了提高并发和并行可以在线程安全部分代码前面进行取消互斥锁。

C++11_第30张图片

与lock_guard不同的是,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所管理的互斥量的指针)。

补充:https://blog.csdn.net/zzhongcy/article/details/85230200

12.6.4相关题

  • 支持两个线程交替打印,一个打印奇数,一个打印偶数
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

int main()
{
    mutex mtx;
    condition_variable c;
    int n = 100;
    bool flag = true;

    thread t1([&]() {
        int i = 0;
        while (i < n)
        {
            unique_lock<mutex> lock(mtx);
            c.wait(lock, [&]()->bool {return flag; });

            cout << i << endl;
            flag = false;
            i += 2;
            c.notify_one();
        }
    });

    thread t2([&]() {
        int i = 1;
        while (i < n)
        {
            unique_lock<mutex> lock(mtx);
            c.wait(lock, [&]()->bool {return !flag; });

            cout << i << endl;
            flag = true;
            i += 2;
            c.notify_one();
        }
    });

    t1.join();
    t2.join();
    return 0;
}

12.6.5补充

对于Mutex变量来说,全局的变量在.h中会在多个.cc文件中拥有且可见容易有链接问题。而static又只能在当前文件可见。因此真要处理成全局的就使用单例模式。把需要的封装成一个类,再把这个类变成单例类。或者简单使用就封装成lambda使用引用抓捕。

你可能感兴趣的:(C++,c++)