• C++11->右值引用


    左值引用与右值引用

    左值与右值

    左值:是一类允许取地址,可以改变其值,允许放在赋值号左边的,也·可以放在右边的变量或者指针。

    右值:是一类不能取地址,不能修改变量的值,只允许放在赋值号右边的表达式,以值返回的函数,或者程序生成的临时变量,或者匿名对象等,另外右值也称为“将亡值”“:临时变量,匿名对象,局部对象的返回都是”将亡值“

    左值引用

    对左值进行引用称为左值引用,对于const的左值引用即可以接受左值,又可以接受右值引用。

    int a=10;
    int &ra=a;
    int *pa =&a;
    int *&rpa=pa;
    const int &rb=a;
    const int &rc=10;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    右值引用

    对右值的引用称为右值引用,右值引用是很奇怪的。一但通过右值引用,右值就支持修改,且可以取到地址;这意味着右值引用可能为右值开辟一块可以访问的空间,延长了右值的生命周期,也就变成了左值

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8XdJjfZZ-1664536606968)(./C++11-%E5%8F%B3%E5%80%BC%E5%BC%95%E7%94%A8.assets/image-20220929222138927-1664461301580-3.png)]

    左值引用与右值引用比较

    左值引用:

    1. 左值引用只能引用左值,不能引用右值,但是左值引用可以引用已被右值引用引用过的对象。
    2. const左值引用即可以引用左值也可以引用右值,但是一般我们只考虑左值引用的场景

    右值引用:

    1. 右值引用只能引用右值,不能引用左值,一但引用右值后,引用就具有的左值的属性,可以取地址,可以辅助
    2. 右值引用可以引用move后的左值

    右值引用的应用场景

    移动构造和移动赋值

    对于自定义类型,左值引用可以减少深拷贝,这是左值引用的优势,像:对于重载后置的自增自减符,以值返回的函数等是无法避免深拷贝问题,但是我们知道以值返回的最后会生成一个临时变量,也就是一个“将亡值”,对于这个将亡值,它的资源必然会被释放,我们是否可以直接利用它的资源呢?—这就是移动构造和移动赋值

    移动构造

    void Swap(string& s)
    		{
    			std::swap(_str, s._str);
    			std::swap(_size, s._size);
    			std::swap(_capacity, s._capacity);
    		}
    		// 移动构造
    		string(string&& s)
    			:_str(nullptr)
    			, _size(0)
    			, _capacity(0)
    		{
    			cout << "string(string&& s) -- 移动构造" << endl;
    			Swap(s);
    			//一但右值引用后,可以取地址就变为左值了,这也就是为什么可以使用接受左值的Swap
                 //如果你仍想保持右值属性,可以通过完美转发
    		}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AaV5NHPJ-1664536606978)(./C++11-%E5%8F%B3%E5%80%BC%E5%BC%95%E7%94%A8.assets/image-20220930111439524-1664507683061-1.png)]

    移动赋值

    // 移动赋值
    		string& operator=(string&& s)
    		{
    			cout << "string& operator=(string&& s) -- 移动赋值" << endl;
    			Swap(s);
    			//一但右值引用后,可以取地址就变为左值了,这也就是为什么可以使用接受左值的Swap
    			return *this;
    		}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AKewzzPL-1664536606980)(./C++11-%E5%8F%B3%E5%80%BC%E5%BC%95%E7%94%A8.assets/image-20220930112836795.png)]

    右值引用引用左值—move

    通过移动构造和移动赋值,可以转移“将亡值”的资源;如果在某些需求下:可以通过move改变左值的属性,将左值暂时成为右值,但是这是要承担左值资源被转移的风险。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZoK3rJIt-1664536606983)(./C++11-%E5%8F%B3%E5%80%BC%E5%BC%95%E7%94%A8.assets/image-20220930114100295-1664509261360-3.png)]

    STL

    很多STl容器接口都填添加了右值引用如

    vector,list等的push,等接口,可以解决临时对象深拷贝问题

    void push_back (const value_type& val);
    void push_back (value_type&& val);
    
    • 1
    • 2

    完美转发

    万能引用

    1. 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
    2. 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
    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
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    完美转发

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

    这样就可以解决,右值引用后变为左值的而想继续保持右值的需求

    完美转发的场景

    template
    struct ListNode
    {
    ListNode* _next = nullptr;
    ListNode* _prev = nullptr;
    T _data;
    };
    template
    class List
    {
    typedef ListNode Node;
    public:
    List()
    {
    _head = new Node;
    _head->_next = _head;
    _head->_prev = _head;
    }
    void PushBack(T&& x)
    {
    //Insert(_head, x);
    Insert(_head, std::forward(x));
    }
    void PushFront(T&& x)
    {
    //Insert(_head->_next, x);
    Insert(_head->_next, std::forward(x));
    }
    void Insert(Node* pos, T&& x)
    {
    Node* prev = pos->_prev;
    Node* newnode = new Node;
    newnode->_data = std::forward(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 lt;
    lt.PushBack("1111");
    lt.PushFront("2222");
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    新的类功能

    c++11之前类有6个默认的成员函数:构造函数,析构函数,拷贝构造函数,拷贝赋值重载,取地址重载,const取地址;

    c++11之后新增了2个:默认移动构造函数,默认移动赋值重载

    默认移动构造函数

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

    默认移动赋值重载

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

    如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值

    强制生成指定默认函数的关键子字—default

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

    Person(const Person& p)
    :_name(p._name)
    ,_age(p._age)
    {}
    Person(Person&& p) = default;
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

    Person(const Person& p)
    :_name(p._name)
    ,_age(p._age)
    {}
    Person(Person&& p) = delete;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    final与override

    final:

    1. 修饰虚函数表示虚函数不能被重写
    2. 修饰类表示类不能被继承

    override:

    检测派生类虚函数是否重写了基类的某个虚函数,如果没有重写就编译报错

    可变参数模板

    C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。

    // Args是一个模板参数包,args是一个函数形参参数包
    // 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
    template 
    void ShowList(Args... args)
    {}
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

    递归函数展开参数包

    模板的实例化是在编译阶段,由于模板ShowList无法匹配无参的函数,因此这里特殊code只有一个参数的函数。

    // 递归终止函数
    template 
    void ShowList(const T& t)
    {
    cout << t << endl;
    }
    // 展开函数
    template 
    void ShowList(T value, Args... args)
    {
    cout << value <<" ";
    ShowList(args...);
    }
    int main()
    {
    ShowList(1);
    ShowList(1, 'A');
    ShowList(1, 'A', std::string("sort"));
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    逗号表达式展开参数包

    1. sizeof…(args)显示参数个数

    2. 这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)…}将会展开成((printarg(arg1),0),(printarg(arg2),0), (printarg(arg3),0), etc… ),最终会创建一个元素值都为0的数组int arr[sizeof…(args)]由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包

    template 
    void PrintArg(T t)
    {
    cout << t << " ";
    }
    //展开函数
    template 
    void ShowList(Args... args)
    {
    int arr[] = { (PrintArg(args), 0)... };
    cout << endl;
    }
    int main()
    {
    ShowList(1);
    ShowList(1, 'A');
    ShowList(1, 'A', std::string("sort"));
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    STL容器中的empalce相关接口函数

    template 
    void emplace_back (Args&&... args);
    
    • 1
    • 2
    1. 首先我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用。

    2. emplcae在开辟空间时会使用定位new直接对内存初始化,不用像push或者insert那样需要通过拷贝构造/移动构造来初始化空间。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QE6QuRaR-1664536606985)(./C++11-%E5%8F%B3%E5%80%BC%E5%BC%95%E7%94%A8.assets/image-20220930191510710-1664536512167-5.png)]

  • 相关阅读:
    [汇编语言]寄存器
    白杨SEO:做个世界杯公众号怎么样?以2022年卡塔尔世界杯来做微信搜一搜的SEO流量实战举例
    类与对象(十七)----继承extend
    第四十九章 Applications
    大数据-之LibrA数据库系统告警处理(ALM-12048 网络写包错误率超过阈值)
    隆云通五要素微气象仪
    吃豆人游戏-第12届蓝桥杯Scratch选拔赛真题精选
    人工智能基础第一次作业
    常用 Shell 脚本
    Tomcat以及UDP
  • 原文地址:https://blog.csdn.net/qq_55439426/article/details/127128158