C++11详解

x33g5p2x  于2022-05-27 转载在 其他  
字(26.7k)|赞(0)|评价(0)|浏览(642)

C++11

C++11简介

在2003年C标准委员会曾经提交了一份技术勘误表(简称TC1),使得C03这个名字已经取代了C98称为C11之前的最新C标准名称。不过由于TC1主要是对C98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C98/03标准。从C0x到C11,C标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比C98/03,C11则带来了数量可观的变化,其中包含了约140个新特性,以及对C03标准中约600个缺陷的修正,这使得C11更像是从C98/03中孕育出的一种新语言。相比较而言,C11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。

列表初始化

C++98中{}的初始化问题

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

int a[] = {1,2,3,4};
int b[5] = {0};

对于一些自定义的类型,却无法使用这样的初始化。比如:

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

这样在C98中无法通过编译,导致每次定义vector时,都需要先把vector定义出来,然后使用循环对其赋初始值,非常不方便。C11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。

C++11内置类型的列表初始化

int main()
{
    // 内置类型变量
    int x1 = {10};
    int x2{10};
    int x3 = 1+2;
    int x4 = {1+2};
    int x5{1+2};
    // 数组
    int a[5] {1,2,3,4,5};
    int b[]{1,2,3,4,5};
    // 动态数组,在C++98中不支持
    int* p1 = new int[5]{1,2,3,4,5};
    // 标准容器
    vector<int> v{1,2,3,4,5};
    map<int, int> m{{1,1}, {2,2,},{3,3},{4,4}};
    return 0;	
}

总结:

C++11里面扩展了{}初始化使用,基本都可以使用他来初始化,建议还是按旧的用法使用,一般new[]建议这样来初始化

自定义类型的列表初始化

  1. 标准库支持单个对象的列表初始化
class A
{
public:
	A(int x = 0, int y = 0): _x(x), _y(y)
	{}
private:
    int _x;
    int _y;
};
int main()
{
    A a{1,2};
    return 0;
}

自定义类型对象可以使用{}初始化,必须要有对应参数类型和个数的构造函数,因为用{}初始化,会调用对应的构造函数。

STL当中的列表初始化

多个对象想要支持列表初始化,需给该类(模板类)添加一个带有initializer_list类型参数的构造函数即
可。注意:initializer_list是系统自定义的类模板,该类模板中主要有三个方法:begin()、end()迭代器
以及获取区间中元素个数的方法size()。

#include<map>
#include<vector>
#include<list>
int main()
{
    vector<int> v1 = {1,2,3,4,5};
    vector<int> v1{1,2,3,4,5};//可以省略=号
    
    auto lt1 = {1,2,3,4};//lt1的类型为initializer_list<int>
    //所以使用{1,2,3,4}初始化本质上调用了initializer_list作为参数的构造函数
    initializer_list<int> lt1 = {1,2,3,4};
    
    map<string,int> dict = {pair<string,int>("sort",1),pair<string,int>("sort",1)};
    map<string,int> dict = {{"sort",1},{"sort",1}};
    
    return 0;
}

C++11用{}初始化好像是万能的,一个自定义类型调用{}初始化,本质是调用对应的构造函数

模拟实现vector当中的initializer_list作为参数的构造函数:

vector(initializer_list<T> lt)
    :_start(nullptr)
    ,_finish(nullptr)
    ,_endofstorage(nullptr)
{
    typename initializer_list<T>::iterator it = lt.begin();
    auto it = it.begin();
    while(it != lt.end())
    {
        push_back(*it);
        ++it;
    }
}

本质上下面这种初始化是调用了一个initializer_list作为参数的构造函数来初始化:

vector<int> v1 = {1,2,3,4,5};

总结:

  • 自定义类型对象可以使用{}初始化,必须要有对应参数类型和个数的构造函数,因为用{}初始化,会调用对应的构造函数
  • STL容器支持{}初始化,容器里面有支持一个initializer_list作为参数的构造函数

变量类型推导

在定义变量时,必须先给出变量的实际类型,编译器才允许定义,但有些情况下可能不知道需要实际类型怎么给,或者类型写起来特别复杂

auto

int main()
{
    int i = 0;
    auto p = &i;//只能用来推导类型
    auto pf = strcpy;
    cout<<typeid(p).name()<<typeid(pf).name()<<endl;
    return 0;
}

auto自动推导对象类型:

decltype

decltype是根据表达式的实际类型推演出定义变量时所用的类型

decltype和auto的区别:

int main()
{
    int i = 0;
    auto p = &i;//只能用来推导类型
    auto pf = strcpy;
    decltype(pf) pf1;//可以作为推导类型来创建对象或者变量
    cout<<typeid(pf).name()<<endl<<typeid(pf1).name()<<endl;
    return 0;
}

可以作为推导类型来创建对象或者变量

STL当中的变化

array

固定大小的数组容器

下面这两个有什么区别呢?

#include<array>
int main()
{
    array<int,10> a1;
    int a2[10];
    return 0;
}

这两基本没有什么区别,唯一最大的区别是:

a1[13] = 1;
a2[13] = 1;

a1只要越界一定报错,而a2只要越界不一定报错,[]对越界检查使用了assert,更严格安全,还有就是a1是有迭代器的,C++11增加这个感觉没什么用

forward_list

单链表,这个容器里面没有尾插尾删,因为尾插尾删效率低,并且实现了insert_after,C++11增加这个属实没感觉到有什么用

unordered_map和unordered_set

对于unordered_map和unordered_set的添加还是让人高兴的,它们的底层使用哈希表实现的,它们的介绍请前往博主的C++专栏阅读

针对旧容器,基本都增加了移动构造,移动赋值,所有插入数据接口函数,都增加右值引用版本这些接口都是用来提高效率,下面我们来看一下什么是右值引用:

右值引用和移动语义

左值和右值

在说右值引用之前,我们得先谈谈什么是左值,什么是右值,可能大家理解的左值和右值的概念是赋值号左边的就是左值,赋值号右边的就是右值,其实不然,左值也可能出现在右边,比如:

int main()
{
    int a = 10;
    a = 20;
    int b = 10;
    b = a;
}

这里的a是左值,b也是左值,b = a,发现左值也可以出现在赋值号右边,所以这些说法是不正确的,正确的说法应该是:左值可以获取地址,而右值不能获取地址。

左值都是可以获取地址,左值基本都可以出现在赋值符号的左边,可以修改,但是const修饰的左值,只能获取地址,不能赋值,右值不能出现赋值符号的左边,也就是不能修改右值,不能取地址

左值引用是给左值取别名,左值引用不能引用右值,const左值引用可以引用左值,也可以引用右值:

int main()
{
    int a = 10;
    int& rt = a;
    //int& rt = 10;//error,左值引用不能引用右值
    const int& rt = 10;//const左值引用可以引用右值
    return 0;
}

当形参是const修饰的左值引用时,实参既可以是左值也可以是右值:

void push_back(const T& x)
{}

右值匹配了右值引用,左值匹配了左值引用,需要注意的是,我们说当形参是const修饰的左值引用时,实参既可以是左值也可以是右值,有人说那这里为什么f(1)不匹配第一个函数呢?是因为编译器会去找最匹配的。

右值引用只能引用右值不能引用左值:

int a = 10;
int&& r2 = a;//error

右值引用可以引用move以后的左值:

int&& r3 = std::move(a);

右值引用作为形参时,实参只能传右值:

void push_back(T&& x)
{}

右值引用不可以连续引用:

int&& r1 = 10;
int&& r4 = r1;//error,右值引用只能引用右值,不能引用左值

右值引用可以引用move后的左值:

int&& r4 = std::move(r1);

因此关于左值与右值的区分不是很好区分,一般认为:

  1. 普通类型的变量,因为有名字,可以取地址,都认为是左值。
  2. const修饰的常量,不可修改,只读类型的,理论应该按照右值对待,但因为其可以取地址(如果只是const类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间),C++11认为其是左值。
  3. 如果表达式的运行结果是一个临时变量或者对象,认为是右值。
  4. 如果表达式运行结果或单个变量是一个引用则认为是左值。
  5. 右值引用可以引用move后的左值

总结:

  • 不能简单地通过能否放在=左侧右侧或者取地址来判断左值或者右值,要根据表达式结果或变量的性质判断
  • 能得到引用的表达式一定能够作为引用,否则就用常引用。

C++11对右值进行了严格的区分:

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

左值引用的使用场景

在传参时:
左值引用的场景,引用传参可以减少拷贝

在引用返回时:

左值引用,引用返回可以减少拷贝,但是效果不明显,不使用引用返回,编译器会优化一次拷贝,使用引用返回,编译器不会优化

本质上引用都是用来减少拷贝,提高效率

1、左值引用解决大部分的场景(做参数,做返回值)

2、右值引用是左值引用一些盲区的补充

比如我们写一个to_string函数来讲解(这是一个整形转字符串的函数):

string to_string(int val)
{
    string str;
    while(val)
    {
        int i = val % 10;//取到最低的十进制位
        str += ('0' + i);
        val /= 10;
    }
    reverse(str.begin(),str.end());//逆置
    return str;
}
int main()
{
    string s = to string(1234);
    cout<<s.c_str()<<endl;
    return 0;
}

此时的to_string函数只能值返回,如果是引用返回就会出问题,因为临时对象出了作用域就销毁了,相当于s成了野指针,我们只能传值返回,传值返回会有一次拷贝构造,本来是str去拷贝构造临时对象,然后临时对象再去拷贝构造s,但是编译器做了优化,直接用str去拷贝构造s,这个临时对象小的话放在寄存器,大的话放在调用它的函数的栈帧当中。还记不记得前面说的将亡值:函数按照值的方式进行返回时产生的临时对象就是将亡值

当这个变量或者对象出了作用域在,这种场景就可以用左值引用,在这里我们可以硬用左值引用返回,将里面的string对象写成静态的就可以引用返回了,因为这个变量出了作用域还在:

string& to_string(int val)
{
    //线程安全问题
    static string str;
    str.clear();
    while(val)
    {
        int i = val % 10;//取到最低的十进制位
        str += ('0' + i);
        val /= 10;
    }
    reverse(str.begin(),str.end());//逆置
    return str;
}

但是这样写会有多线程安全问题,而且每次进来还需要将str清理一次,所以左值引用无法解决局部对象返回,只能传值返回

那么右值引用返回呢?

string&& to_string(int val)
{
    //线程安全问题
    string str;
    while(val)
    {
        int i = val % 10;//取到最低的十进制位
        str += ('0' + i);
        val /= 10;
    }
    reverse(str.begin(),str.end());//逆置
    return (move)str;
}

右值引用并不会改变局部变量的生命周期,返回的也是str的别名,出了作用域str也就销毁了,所以几乎没有使用右值引用返回的场景。

总结:str在按照值返回时,必须创建一个临时对象,临时对象创建好之后,str就被销毁了,最后使用返回的临时对象构造s,s构造好之后,临时对象就被销毁了。仔细观察会发现:str、临时对象、s每个对象创建后,都有自己独立的空间,而空间中存放内容也都相同,相当于创建了三个内容完全相同的对象,对于空间是一种浪费,程序的效率也会降低,而且临时对象确实作用不是很大,那能否对该种情况进行优化呢?

下面就用到了右值引用的场景:

右值引用的场景

移动语义

C11提出了移动语义概念,即:将一个对象中资源移动到另一个对象中的方式,可以有效缓解在值传递时空间的浪费问题,在C11中如果需要实现移动语义,必须使用右值引用。string类的移动构造函数

//移动构造
String(String&& s)
    :_str(nullptr)
    , _size(0)
    , _capacity(0)
{
    this->swap(s);
}

我们自己模拟实现string,下面的场景就用到了右值引用:

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<assert.h>
#include<algorithm>
using namespace std;
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);
    }
    //拷贝构造
    String(const String& s)
        :_str(nullptr)
        , _size(0)
        , _capacity(0)
    {
        cout << "拷贝构造:String(const String& s)"<< endl;
        String tmp(s._str);
        swap(tmp);
    }
    //移动构造
    String(String&& s)
        :_str(nullptr)
        , _size(0)
        , _capacity(0)
    {
        cout << "移动构造:String(String && s)" << endl;
        this->swap(s);
    }
    // 拷贝赋值
    String& operator=(const String& s)
    {
        cout << "String& operator=(const string& s) -- 深拷贝" << endl;
        String tmp(s);
        swap(tmp);

        return *this;
    }
    // s1.swap(s2)
    void swap(String& s)
    {
        ::swap(_str, s._str);
        ::swap(_size, s._size);
        ::swap(_capacity, s._capacity);
    }
    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)
    {
        push_back(ch);
        return *this;
    }
    String to_string(int val)
    {
        String str;
        while (val)
        {
            int i = val % 10;//取到最低的十进制位
            str += ('0' + i);
            val /= 10;
        }
        reverse(str.begin(), str.end());//逆置
        return str;
    }
    const char* c_str() const
    {
        return _str;
    }
    char& operator[](size_t pos)
    {
        assert(pos < _size);
        return _str[pos];
    }

    void clear()
    {
        _str[0] = '\0';
        _size = 0;
    }
    ~String()
    {
        if (_str) delete[] _str;
    }
private:
    char* _str;
    size_t _size;
    size_t _capacity;
};
int main()
{
    String s1("hello");
    String s2("world");
    //s1.to_string(22222)没有接收对象
    String s3 = s1.to_string(22222);//有接收对象
    return 0;
}

当我们去调用to_string函数时,发现外面没有接收对象和有接收对象它都调用了一次移动构造,为什么是这样呢?

当外面有接收对象时:
因为str对象的生命周期在创建好临时对象后就结束了,即将亡值,编译器做了优化,用str去拷贝构造返回对象时,把str识别为右值,C++11认为其为右值,在用str构造临时对象时,就会采用移动构造,即将str中资源转移到临时对象中。而临时对象也是右值,因此在用临时对象构造s3时,也采用移动构造,将临时对象中资源转移到s3中,整个过程,只需要创建一块堆内存即可,既省了空间,又大大提高程序运行的效率。但是编译器做了优化,直接用str移动构造s。

总结:外面没有接收对象时,str去构造临时对象,str被编译器优化成了右值(如果没有优化,这里是拷贝构造),这里调用了移动构造。外面有接收对象时,str去构造临时对象,这里一次移动构造,然后临时对象再去构造那个接收的对象,又一次移动构造,两次移动构造,编译器做了优化,直接用str移动构造s

但是也有人会这样使用:

int main()
{
    String s1;
    String s;
    s = s1.to_string(12345678);
    cout << s.c_str() << endl;
    return 0;
}

可以看到调用to_string打印了一次移动构造,而且还有一次赋值构造,后面那个拷贝构造不用管,是因为在赋值构造里面写了现代写法调用的。

当没有移动构造时:一次拷贝构造和一次赋值构造

当有移动构造时:一次移动构造和一次赋值构造

这样的开销还是很大的,所以还要移动赋值:

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

可以看到调用了移动构造和移动赋值涉及深拷贝的类,除了实现拷贝构造和拷贝赋值还要实现移动构造和移动赋值,面对这个类传值返回的函数场景就能进一步减少拷贝,提高效率

总结:

  1. 左值引用通常在传参和传返回值的过程中减少拷贝,一般是利用左值引用的语法特性,别名的特性,减少拷贝
  2. 右值引用,一般是利用深拷贝的类,需要实现移动构造和移动赋值,利用移动构造和移动赋值在传参和传返回值过程中间接转移资源,减少拷贝

下面我们来看移动构造在C++中的一些改变:

int main()
{
    string s1("hello world");
    string s2("1111");
    //在C++98中第二个比第一个效率高
    swap(s1,s2);
    s1.swap(s2);
    
    //在C++11中效率没有差别
    swap(s1,s2);
    s1.swap(s2);
    return 0;
}

在C98中第二个比第一个效率高,因为第一个调用三次拷贝构造来进行交换,二是第二个仅仅交换成员即可,我们可以看到C11中swap函数是调用了移动构造的,string的成员函数swap是调用移动赋值

在C++11中容器中push_back接口增加了右值引用作为参数的接口:

容器里面插入时使用右值引用也会减少拷贝

int main()
{
    vector<string> v;
    string s("11111");
    v.push_back(s);//左值 一次拷贝构造
    v.push_back("222222");//右值 两次移动
    
    v.push_back(move(s));//对一个左值进行move,它的资源可能会被转走
    return 0;
}

完美转发

完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。

所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相
应实参是右值,它就应该被转发为右值。这样做是为了保留在其他函数针对转发而来的参数的左右值属性进
行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)。

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)//模板里面的T&& 做参数,不再局限是右值引用,叫做万能引用
//既可以引用左值,也可以引用右值
{
    Fun(t);//t会退化成左值
    Fun(std::forward<T>(t));//不想t被退化就加forward
}
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;
}

模板里面的T&& 做参数,不再局限是右值引用,叫做万能引用,既可以引用左值,也可以引用右值,在模板里面调用函数时,参数t会退化成左值。

当我们不完美转发时:

可以看到不管传左值还是右值,调用的都是左值引用。

当我们完美转发时:

Fun(std::forward<T>(t))

此时就传的是左值就调用的是左值引用参数的函数,传的是右值就调用的是右值引用参数的函数

默认成员函数

原来C++类中,有6个默认成员函数:

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝赋值重载
  5. 取地址重载
  6. const取地址重载
    最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。
    C++11新增了两个:移动构造函数和移动赋值运算符重载。

针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:

  • 如果你没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载都没有实现。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
  • 如果你没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载都没有实现,那么编译器会自动生成一个默认移动赋值。默认生成的移动赋值函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
  • 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

我们有一种方法可以强制默认生成,设置成default,尽管条件不满足仍然生成:

Person(const Person& p) = default;
Person(Person&& p) = default;

可变参数模板

printf函数当中就有可变参数:

int main()
{
    int a;
    int b;
    double d;
    cin>>a>>b>>d;
    printf("%d,%f\n",a,d);
    printf("%d,%d,%f\n",a,b,d);
    return 0;
}

Args是一个模板参数包,args是一个函数形参参数包,声明一个参数包Args…args,这个参数包中可以包含0到任意个模板参数。

template<class ...Args>
void ShowList(Args... args)
{}
int main()
{
    return 0;
}
//可以算出有多少个参数,但是不能通过下标取出每一个参数
void ShowList(Args... args)
{
    for(size_t i = 0;i<sizeof...(args);++i)
    {
        //cout<<args[i]<<" ";
        cout<<i<<" ";
    }
    cout<<endl;
}
int main()
{
    ShowList(1);
    ShowList(1,'A');
    ShowList();
    ShowList(1,3.33,std::string("hello"));
    
    return 0;
}

我们可以算出有多少个参数,但是不能通过下标取出每一个参数

有一种方式取出每一个参数:

void ShowList()//递归终止函数
{
    cout<<endl;
}
//可以通过递归调用来获取参数
template<class T,class ...Args>
void ShowList(T x,Args... args)
{
    cout<< x <<endl;
    ShowList(args...);
}
int main()
{
    ShowList(1);
    ShowList(1,'A');
    ShowList();
    ShowList(1,3.33,std::string("hello"));
    
    return 0;
}

emplace_back和push_back的区别

emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象,那么在这里我们可以看到除了用法上,和push_back没什么太大的区别

int main()
{
	std::list< std::pair<int, char>> mylist;
	// emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
	// 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别
	mylist.emplace_back(10, 'a');
	mylist.emplace_back(20, 'b');
	for (auto e : mylist)
		cout << e.first << ":" << e.second << endl;
	return 0;
}

我们试一下带有拷贝构造和移动构造的String,我们为了方便验证,我们用一下我们上面自己实现的string类:

1、传左值对比:

int main()
{
	std::list<std::pair<int,String>> mylist;
	// 1、传左值对比 -- 没有区别
	std::pair<int, bit::string> kv(1, "11111");
	mylist.push_back(kv);
	mylist.emplace_back(kv);
    return 0;
}

我们发现传左值没有区别,都调用了构造和拷贝构造。

2、传右值对比:

如果push_back/emplace_back的参数对象及其成员都实现了移动构造

int main()
{
	// 下面我们试一下带有拷贝构造和移动构造的bit::string,再试试呢
	// 我们会发现其实差别也不到,emplace_back是直接构造了,push_back
	// 是先构造,再移动构造,其实也还好。
	std::list<std::pair<int,String>> mylist;

	// 2、传右值对比 -- push_back是构造+移动构造  
	//                 emplace_back是直接构造
	// 形态有区别,如果push_back/emplace_back的参数对象及其成员都实现了移动构造,本质区别不大
	// 因为构造出来+移动构造,和直接构造成本差不多
	// 但是如果push_back/emplace_back的参数对象及其成员没有实现移动构造
	// 那么emplace_back还是直接构造,push_back则是构造+拷贝构造,代价就大了
	// 结论:稳妥一点呢用emplace_back更好,因为他可以不依赖参数对象是否提供移动构造
	mylist.push_back(make_pair(2, "sort"));
	mylist.push_back({ 40, "sort" });

	cout << endl;
	mylist.emplace_back(make_pair(2, "sort"));
	mylist.emplace_back(10, "sort");

	return 0;
}

可以看到push_back调用了构造和移动构造,而emplace_back调用了构造,形态有区别,如果push_back/emplace_back的参数对象及其成员都实现了移动构造,本质区别不大,因为构造出来+移动构造,和直接构造成本差不多

那么当我们不写移动构造时:

但是如果push_back/emplace_back的参数对象及其成员没有实现移动构造,那么emplace_back还是直接构造,push_back则是构造+拷贝构造,代价就大了

我们看pair当中的移动构造和拷贝构造是强制生成的,也就是如果不写强制生成默认的:

pair中first和second如果是自定义类型成员,当pair调用拷贝构造时, 自定义类型成员first和second就需要调用它们的拷贝构造,pair调用移动构造,自定义类型成员first和second就需要调用它们的移动构造,没有移动构造就调用拷贝构造

所以在前面当没有实现移动构造时,push_back在插入pair时调用了自定义类型成员的构造和拷贝构造

结论
稳妥一点用emplace_back更好,因为它可以不依赖参数对象是否提供移动构造

lambda表达式

可调用类型概念:
类型定义的对象可以像函数一样去调用

可调用类型主要有:

  • 函数指针
  • 仿函数,仿函数就是一个类+重载operator() 相比函数指针好用多了
  • lambda表达式
  • 包装器

我们可以看到sort函数就有接口里面有参数仿函数:

假如我们想要实现对一个货物进行排序,它可以分别按照名字排序,也可以按照价格排序,可以按照评价排序:

struct Goods
{
	string _name;  // 名字
	double _price; // 价格
	int _evaluate; // 评价
};

struct ComparePriceLess
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price < gr._price;
	}
};

struct ComparePriceGreater
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price > gr._price;
	}
};

struct CompareEvaluateGreater;
struct CompareEvaluateLess;

int main()
{
	Goods gds[] = { { "苹果", 2.1, 5}, { "香蕉", 3, 4}, { "橙子", 2.2,3}, { "菠萝", 1.5, 4} };
	sort(gds, gds + sizeof(gds) / sizeof(gds[0]), ComparePriceLess());
	sort(gds, gds + sizeof(gds) / sizeof(gds[0]), ComparePriceGreater());
	// 增加一个比较方式,就要提供一个对应的仿函数
	return 0;
}

按照ComparePriceLess排序:

按照ComparePriceGreater排序:

但是这样我们增加一个比较方式,就要提供一个对应的仿函数

lambda表达式可以解决:

首先我们来看一下什么是lambda表达式:

lambda表达式语法

lambda表达式书写格式:

[capture-list] (parameters) mutable -> return-type { statement }
  • lambda表达式各部分说明
    [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来
    的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。

(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起
省略

mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修
饰符时,参数列表不可省略(即使参数为空)。

->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分
可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。

{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

  • 注意:
    在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{};该lambda函数不能做任何事情。
  • 捕获列表说明:

捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
[var]:表示值传递方式捕捉变量var
[=]:表示值传递方式捕获所有父作用域中的变量(包括this)
[&var]:表示引用传递捕捉变量var
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
[this]:表示值传递方式捕捉当前的this指针

  • 注意:
    a. 父作用域指包含lambda函数的语句块
    b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
    比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量 [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量 c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
    d. 在块作用域以外的lambda函数捕捉列表必须为空。
    e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
    f. lambda表达式之间不能相互赋值,即使看起来类型相同

实现一个比较整数小于的lambda,如何实现?

lambda表达式写起来和函数类似,有参数,返回值,函数体。多的就是还有捕捉列表

auto lessFunc = [](const int a,const int b)->bool{return a<b;};
cout<<lessFunc(1,2)<<endl;
int main()
{
    //实现一个比较整数小于的lambda,如何实现
    //lessFunc lambda对象可以像函数一样调用
    //最简单的lambda
    auto f1 = []{cout<<"hello world"<<endl;};
    f1();
    auto lessFunc = [](const int a,const int b)->bool{return a<b;};
    cout<<lessFunc(1,2)<<endl;
    return 0;
}

下面探索捕捉列表是干嘛的,实现交换x和y的lambda表达式:

  1. 普通实现
int main()
{
    //lambda表达式写起来和函数类似,有参数,返回值,函数体。多的就是还有捕捉列表

    //下面探索捕捉列表是干嘛的
    //实现交换x和y的lambda表达式
    //1.普通实现
    int x = 1;
    int y = 2;
    auto swap1 = [](int& a, int& b)
    {
        int tmp = a;
        a = b;
        b = tmp;
    };
    swap1(x, y);
    return 0;
}

  1. 要求不传参数,交换x和y
int main()
{
    //lambda表达式写起来和函数类似,有参数,返回值,函数体。多的就是还有捕捉列表
    int x = 1;
    int y = 2;
    //2.要求不传参数,交换x和y
    auto swap2 = [x,y]()
    {
        //这里面不能用外面的x,y
        //lambda想用外面函数中的对象,需要用到捕捉列表
        int tmp = x;
        x = y;//传值过来的不能修改,加mutable就好了
        y = tmp;
    };
    return 0;
}

这里x和y是以传值方式捕捉,本质就是拷贝过来的,并且不能修改,传值过来的不能修改,加mutable就好了:

auto swap2 = [x,y]()mutable

但是mutable的作用就是让传值捕捉的对象可以修改,但是你修改的是传值拷贝的对象,不影响外面的对象,实际中mutable意义不大,除非你就是想传值捕捉过来,lambda中修改不影响外面的值

所以需要传引用

auto swap2 = [&x,&y]()mutable
int main()
{
    int x = 1;
    int y = 2;
    //传引用捕捉
    auto swap2 = [&x,&y]()
    {
        //这里面不能用外面的x,y
        //lambda想用外面函数中的对象,需要捕捉
        int tmp = x;
        x = y;//传值过来的不能修改,加mutable就好了
        y = tmp;
    };
    return 0;
}

还可以引用捕捉全部的变量:

int main()
{   
    int x = 1;
    int y = 2;
    //引用捕捉全部变量
    auto swap2 = [&]()mutable
    {
        //这里面不能用外面的x,y
        //lambda想用外面函数中的对象,需要捕捉
        int tmp = x;
        x = y;//传值过来的不能修改,加mutable就好了
        y = tmp;
    };
    return 0;
}

lambda表达式解决上面说的货物排序的问题:

struct Goods
{
	string _name;  // 名字
	double _price; // 价格
	int _evaluate; // 评价
};
int main()
{
	Goods gds[] = { { "苹果", 2.1, 5}, { "香蕉", 3, 4}, { "橙子", 2.2,3}, { "菠萝", 1.5, 4} };
	sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& gl, const Goods& g2)
	{
		return gl._price < g2._price;
	});

	sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& gl, const Goods& g2)
	{
		return gl._price > g2._price;
	});

	sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& gl, const Goods& g2)
	{
		return gl._evaluate < g2._evaluate;
	});

	sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& gl, const Goods& g2)
	{
		return gl._evaluate > g2._evaluate;
	});
	return 0;
}

lambda表达式之间不能相互赋值,即使看起来类型相同,允许使用一个lambda表达式拷贝构造一个新的副本,可以将lambda表达式赋值给相同类型的函数指针

void(*PF)();
int main()
{
	auto f1 = []{cout << "hello world" << endl; };
	auto f2 = []{cout << "hello world" << endl; };
    f1 = f2;//这里会编译失败,提示找不到operator=()
    auto f3(f2);
	return 0;
    PF = f2;
}

我们来看一下lamada表达式的汇编语言,发现底层是调用了operator(),这说明了它底层是调用了一个仿函数:

我们定义一个lambda表达式,对我们而言是类型是匿名,实际编译器会把它转换成仿函数,这个仿函数的名称是lambda_uuid,uuid是随机生成的,为了保证不同的lambda表达式类型名称是不一样的。

接下来我们看下一种可调用类型:

可调用类型有:

  1. 函数指针(C语言)
  2. 仿函数
  3. lambda(匿名函数),我们看起来是匿名,但是编译器看起来不是匿名(掌握格式和原理)
  4. 包装器

包装器

function包装器,包装器是对可调用对象的包装,function包装器也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。

那么我们来看看,我们为什么需要function呢?首先来看下面的程序

ret = func(x);

上面func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能是lamber表达式对象?所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下!
为什么呢?我们继续往下看:

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

通过上面的程序验证,我们会发现useF函数模板实例化了三份。
包装器可以很好的解决上面的问题:

该模板的第一个参数是返回值,后面的是参数列表的类型,所以我们这样用:

int main()
{
    //包装函数指针
    std::function<double(double)> f1(f);
    f1(10.1);

    //包装仿函数对象
    // 函数对象
    std::function<double(double)> f2 = Functor();
    cout << f2(10.1) << endl;

    //包装lambda
    std::function<double(double)> f3 = [](double d)->double {return d / 4; };
    f3(10.1);

    useF(f1, 10.1);
    useF(f2, 10.1);
    useF(f3, 10.1);

}

我们发现此时就没有实例化出三份,只实例化出一份useF,因为传进来的F类型都是std::function<double(double)>。

包装器也可以对成员函数和静态成员函数进行包装:

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)> func4 = Plus::plusi;
    cout << func4(1, 2) << endl;

    //类的非静态成员函数
    std::function<double(Plus, double, double)> func5 = &Plus::plusd;//类的成员函数需要取地址
    cout << func5(Plus(), 1.1, 2.2) << endl;//需要传对象进去
    return 0;
}

在包装类的非静态成员函数时需要传匿名对象进去,那么有没有办法不传呢?

bind

std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可
调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而
言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M
可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺
序调整等操作。

int main()
{
    // 使用bind进行优化
	// 需要绑定的参数,直接绑定值,不需要绑定的参数给 placeholders::_1 、  placeholders::_2.... 进行占位
	std::function<int(int, int)> func4 = std::bind(&Sub::sub, Sub(), placeholders::_1, placeholders::_2);
	cout << func4(1, 3) << endl;

	// 还可以调整参数顺序
	std::function<int(int, int)> func5 = std::bind(&Sub::sub, Sub(), placeholders::_2, placeholders::_1);
	cout << func5(1, 3) << endl;
//
    return 0;
}

thread

#include<thread>
void F1(int n)
{
    for(int i = 0;i<n;i++)
    {
        cout<<i<<endl;
    }
}
int main()
{
    thread t1(F1,10);
    thread t2(F1,10);
    
    t1.join();
    t2.join();
    return 0;
}

创建一个空线程,不执行

创建一个执行fn可调用对象的线程:

#include<thread>
void F1(int n)
{
    for(int i = 0;i<n;i++)
    {
        cout<<i<<endl;
    }
}
int main()
{
    int n;
    cin>>n;
    thread t1([n,&i]
    {
		while(i<n)
        {
            cout<< i <<endl;
            ++i;
        }
    });
    thread t2([n,&i]
    {
		while(i<n)
        {
            cout<< i <<endl;
            ++i;
        }
    });
    
    t1.join();
    t2.join();
    return 0;
}

移动赋值使用场景:

#include<thread>
#include<vector>
#include<mutex>
int main()
{
    int n;
    cin>>n;
    vector<thread> works(n);
    std::mutex mtx;
    for(auto& thd : works)
    {
        //移动赋值使用场景
        thd = thread([&mtx](){
            for(int i = 0;i<10;++i)
            {
                mtx.lock();
                cout<<this_thread::get_id()<<":"<<i<<endl;  
                mtx.unlock();    
            }
        });
    }
    for(auto& thd : works)
    {
        thd.join();
    }
    return 0;
}

注意:++操作不是原子的

#include<thread>
#include<vector>
#include<mutex>
int main()
{
    int n;
    cin>>n;
    vector<thread> works(n);
    std::mutex mtx;
    size_t x = 0;
    for(auto& thd : works)
    {
        //移动赋值使用场景
        thd = thread([&mtx,&x](){
            for(int i = 0;i<100000;++i)
            {
                //mtx.lock();
                ++x; 
                //mtx.unlock();    
            }
        });
    }
    for(auto& thd : works)
    {
        thd.join();
    }
    cout<<x<<endl;
    return 0;
}

所以我们需要加锁,锁加在外面比较好,++操作太快了,避免频繁的线程切换,保存上下文的消耗

锁加在外面和里面的效率对比:

#include<thread>
#include<vector>
#include<mutex>
int main()
{
    int n;
    cin>>n;
    vector<thread> works(n);
    std::mutex mtx;
    size_t x = 0;
    size_t total_time = 0;
    for(auto& thd : works)
    {
        //移动赋值使用场景
        thd = thread([&mtx,&x,&total_time](){
            size_t begin = clock();
            for(int i = 0;i<100000;++i)
            {
                mtx.lock();
                ++x; 
                mtx.unlock();    
            }
            size_t end = clock();
            total_time += (end-begin);
            cout<<this_thread::get_id()<<":"<<end-begin<<endl;
        });
    }
    for(auto& thd : works)
    {
        thd.join();
    }
    cout<<x<<endl;
    cout<<total_time<<endl;
    return 0;
}

明显加在外面效率高

有什么做法能让不加锁,就能保证线程安全呢?在C++库当中还有相关的原子操作

原子操作:

#include<thread>
#include<vector>
#include<mutex>
int main()
{
    int n;
    cin>>n;
    vector<thread> works(n);
    atomic<size_t> x = 0;//保证x的一些操作是原子的
    atomic<size_t> total_time = 0;
    for(auto& thd : works)
    {
        //移动赋值使用场景
        thd = thread([&x,&total_time](){
            size_t begin = clock();
            for(int i = 0;i<100;++i)
            {
                ++x;    
            }
            size_t end = clock();
            total_time += (end-begin);
            cout<<this_thread::get_id()<<":"<<end-begin<<endl;
        });
    }
    for(auto& thd : works)
    {
        thd.join();
    }
    cout<<x<<endl;
    cout<<total_time<<endl;
    return 0;
}

线程安全笔试题

实现两个线程交替打印1-100,一个线程打印奇数,一个线程打印偶数

先看这个版本:

#include<thread>
#include<vector>
#include<mutex>
int main()
{
    int i = 1;
    //大部分情况没问题,并不能完全保证一定是交替打印
    //打印奇数
    mutex mtx;
    thread t1([&i](){
        while(i<100)
        {
            mtx.lock();
            cout<<this_thread::get_id()<<":"<<i<<endl;
            i += 2;
            mtx.unlock();        
        }
    });
    
    //极端场景下:假设主线程执行到这里时间片用完了,进入休眠排队
    //此时t2线程还没有创建
    int j = 2;
    //打印偶数
    thread t2([&j](){
        while(j<100)
        {
            mtx.lock();
            cout<<this_thread::get_id()<<":"<<i<<endl;
            j += 2;
            mtx.unlock();
        }
    });
    
    t1.join();
    t2.join();
    
    return 0;
}

这个代码是有问题的:极端场景下:考虑到线程切换,假设主线程执行到21行代码这里时间片用完了,进入休眠排队,此时t2线程还没有创建,那么t1线程就会再次获取锁就会继续打印

假设某次unlock以后,t1时间片到了,进入休眠排队,此时也会导致t2连续获取到锁打印

在说下面正确代码时,我们先说两个锁:

lock_guard

lock_guard 类是一个mutex封装者,它为了拥有一个或多个mutex而提供了一种方便的 RAII style 机制。当一个lock_guard对象被创建后,它就会尝试去获得给到它的mutex的所有权。当控制权不在该lock_guard对象所被创建的那个范围后,该lock_guard就会被析构,从而mutex被释放。unique_lock也是类似。

lock_guardd的实现:

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对象要被销毁,调用析构函数自动解锁,可以有效避免死锁
问题。
lock_guard的缺陷:太单一,用户没有办法对该锁进行控制,因此C++11又提供了unique_lock。

unique_lock

与lock_gard类似,unique_lock类模板也是采用RAII的方式对锁进行了封装,并且也是以独占所有权的方式管理mutex对象的上锁和解锁操作,即其对象之间不能发生拷贝。在构造(或移动(move)赋值)时,unique_lock 对象需要传递一个 Mutex 对象作为它的参数,新创建的unique_lock 对象负责传入的 Mutex 对象的上锁和解锁操作。使用以上类型互斥量实例化
unique_lock的对象时,自动调用构造函数上锁,unique_lock对象销毁时自动调用析构函数解锁,可以很方便的防止死锁问题。
与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所管理的互斥量的指针)。
#include<thread>
#include<vector>
#include<mutex>
int main()
{
    int i = 1;
    mutex mtx;
    
    //打印奇数
    thread t1([&i](){
        while(i<100)
        {
            //mtx.lock();
            {
                std::lock_guard<mutex> lock(mtx);//构造的时候加锁,lock析构的时候自动解锁
                //std::unique_lock<mutex> lock(mtx);//unique_lock和lock_guard差别不大,构造的时候加锁,lock析构的时候自动解锁
                cout<<this_thread::get_id()<<":"<<i<<endl;
                i += 2;
            }
            //mtx.unlock();
            
        }
    });
    
    //极端场景下:假设主线程执行到这里时间片用完了,进入休眠排队
    //此时t2线程还没有创建
    int j = 2;
    //打印偶数
    thread t2([&j](){
        while(j<100)
        {
            std::lock_guard<mutex> lock(mtx);//构造的时候加锁,lock析构的时候自动解锁
            cout<<this_thread::get_id()<<":"<<i<<endl;
            j += 2;
        }
    });
    
    t1.join();
    t2.join();
    
    return 0;
}

在解决上面问题我们要引入条件变量:

第一个构造一定阻塞

第二个构造,pred返回false就调用wait阻塞

pred如果一直是false,那么被唤醒以后也会继续wait阻塞

pred返回true就不调用wait阻塞

本质是通过flag和条件变量配合控制互斥

#include<iostream>
using namespace std;
#include<thread>
#include<vector>
#include<mutex>
#include<condition_variable>
int main()
{
    int i = 1;
    //大部分情况没问题,并不能完全保证一定是交替打印
    //打印奇数
    mutex mtx;
    condition_variable cv;
    bool flag = true;
    thread t1([&i, &mtx, &cv, &flag]() {
        while (i < 100)
        {
            std::unique_lock<mutex> lock(mtx);
            cv.wait(lock, [&flag]() { return flag; });//flag最开始要是true,t1线程刚开始要先运行,因为是奇数

            cout << this_thread::get_id() << ":" << i << endl;
            i += 2;
            flag = false;

            cv.notify_one();

        }
        });

    int j = 2;
    //打印偶数
    thread t2([&j,& mtx, &cv, &flag]() {
        while (j < 100)
        {
            std::unique_lock<mutex> lock(mtx);
            cv.wait(lock, [&flag]() { return !flag; });

            cout << this_thread::get_id() << ":" << j << endl;
            j += 2;
            flag = true;

            cv.notify_one();
        }
        });

    t1.join();
    t2.join();

    return 0;
}

t1打印运行时,t2肯定没有打印运行

t2可能有三种状态:

  1. 时间片用完了,休眠排队
  2. wait
  3. lock

先说第一种情况:当t2时间片用完了,休眠排队,t1打印完后,notify_one没有notify到t2,t1继续获取锁,但是此时flag是false,t1会wait,wait时会把锁解了,当t2被切回来时,然后t2继续获取到锁,此时!flag是true,不会wait,t2就进行打印运行了。

第二种情况:t2在wait,t1打印完后,notify_one到t2,t2被唤醒继续完成打印任务

第三种情况:t2在lock,t1打印完后出了作用域会将锁自动解了,然后t2就获取到了锁,即使t1竞争更大再次获取到锁,他也会在wait那里阻塞(阻塞时会解锁)。然后t2肯定能拿到锁

相关文章