您现在的位置是:首页 >学无止境 >c++积累8-右值引用、移动语义网站首页学无止境

c++积累8-右值引用、移动语义

ytuglt 2023-04-23 19:16:52
简介c++积累8-右值引用、移动语义

1、右值引用

1.1 背景

c++98中的引用很常见,就是给变量取个别名,具体可以参考c++积累7
在c++11中,增加了右值引用的概念,所以c++98中的引用都称为左值引用

1.2 定义

右值引用就是给右值取个名字,右值有了名字之后就成了普通变量,可以像使用左值一样使用。

语法:数据类型&& 变量名=右值

示例:

#include <iostream>

class AA {
public:
    int m_a = 9;
};

AA getTemp() {
    return AA();
}

int main() {
    using namespace std;

    int &&a = 3; // 3是右值,给它起个名字叫a
    int b = 8; // b 是左值, 8是右值
    int &&c = b + 5; // b+5是右值,给它取个名字叫c

    AA &&aa = getTemp();// getTemp()返回值是右值(临时变量),给它起个名字叫aa

    cout << "a= " << a << endl;
    cout << "b = " << b << endl;
    cout << "c = " << c << endl;
    cout << "aa.m_a= " << aa.m_a << endl;

    return 0;
}

在这里插入图片描述

1.3 常量左值引用

常量左值引用是一个万能的引用类型,它可以绑定非常量左值、常量做值、右值,在绑定右值的时候,常量左值引用可以像右值引用一样将右值的生命期延长,缺点是只能读不能改

    int a = 1;
    const int &ra = a; // a是非常量左值
    const int b = 2;
    const int &rb = b; // b是常量左值
    const int &rc = 1; // 1是右值

2、移动语义

2.1 背景

如果一个对象中有堆区资源,需要编写拷贝构造函数和赋值函数,实现深拷贝。
深拷贝把对象中堆区资源复制了一份,如果资源(被拷贝的资源)是临时对象,拷贝完就没有什么意义了,这样会造成没有意义的资源申请和释放操作。
如果能够直接使用对象拥有的资源,可以节省资源申请和释放的时间。c++11增加的移动语义就能够做到这一点。

2.2 定义

移动语义增加两个构造函数:移动构造函数 、 移动赋值函数
移动构造函数语法:
类名(类名&& 源对象){…}
移动赋值函数语法:
类名& operator=(类名&& 源对象){…}

demo:

#include <iostream>
#include <string.h>

using namespace std;

class AA {
public:
    int *m_data = nullptr; //数据成员,指向堆区资源的指针
    AA() = default; // 启用默认构造函数

    void alloc() {  // 给数据成员m_data分配内存
        m_data = new int;  // 分配内存
        memset(m_data, 0, sizeof(int)); //初始化已分配的内存
    }

    AA(const AA &a) {  //拷贝构造函数 - 拷贝语义
        cout << "调用了拷贝构造函数 。
";  // 显示自己被调用的日志
        if (m_data == nullptr) alloc(); // 如果没有分配内存,就分配
        memcpy(m_data, a.m_data, sizeof(int)); //把数据从源对象中拷贝过来
    }

    AA(AA &&a) {  //拷贝构造函数 - 移动语义
        cout << "调用了移动语义拷贝构造函数 。
";  // 显示自己被调用的日志
        if (m_data != nullptr) delete m_data; // 如果已经分配内存,先释放
        m_data = a.m_data; // 把资源从源对象中转移过来
        a.m_data = nullptr; // 把源对象中的指针置空
    }

    AA &operator=(const AA &a) { //赋值函数 - 拷贝语义
        cout << "调用了赋值函数。
"; // 显示自己被调用的日志
        if (this == &a) return *this; // 避免自我赋值
        if (m_data == nullptr) alloc(); // 如果没有分配内存,就分配
        memcpy(m_data, a.m_data, sizeof(int)); // 把数据从源对象中拷贝过来
        return *this;
    }

    AA &operator=(AA &&a) { //赋值函数 - 移动语义
        cout << "调用了移动语义赋值函数。
"; // 显示自己被调用的日志
        if (this == &a) return *this; // 避免自我赋值
        if (m_data != nullptr) delete m_data; // 如果已经分配内存,先释放
        m_data = a.m_data; // 把资源从源对象中转移过来
        a.m_data = nullptr; // 把源对象中的指针置空
        return *this;
    }

    ~AA() { // 析构函数
        cout << "调用析构函数" << endl;
        if (m_data != nullptr) {
            delete m_data;
            m_data == nullptr;
        }
    }

};

int main() {
    AA a1; // 创建对象a1
    a1.alloc(); // 分配堆区资源
    *a1.m_data = 3; // 给堆区内存赋值
    cout << "*a1.m_data = " << *a1.m_data << ",addr = " << a1.m_data << endl;

    AA a2 = a1; // 调用拷贝构造函数 - 这个地方a1是左值就调用拷贝语义构造函数,如果是右值,则调用移动语义构造函数
    cout << "*a2.m_data = " << *a2.m_data << ",addr = " << a2.m_data << endl;

    AA a3;
    a3 = a1; // 调用赋值函数
    cout << "*a3.m_data = " << *a3.m_data << ",addr = " << a3.m_data << endl;

    auto f = [] { // 返回AA类对象的lambda函数
        AA aa;
        aa.alloc();
        *aa.m_data = 10;
        return aa;
    };
    AA a4 = f(); // lambda函数返回临时对象,是右值,将调用移动构造函数
    cout << "*a4.m_data = " << *a4.m_data << ",addr = " << a4.m_data << endl;

    AA a6;
    a6 = f(); // lambda函数返回临时对象,是右值,将调用移动赋值函数
    cout << "*a6.m_data = " << *a6.m_data << ",addr = " << a6.m_data << endl;

    return 0;
}

在这里插入图片描述

2.3、说明

1 std::move() 左值转换为右值
对于一个左值,会调用拷贝构造函数,但是有些左值是局部变量,声明周期也很短,我们也想使用移动,C++为了解决这种问题,提供了std::move()方法来将左值转义为右值,从而方便使用移动语义。
左值对象被转移资源后,不会立刻析构,只有在离开自己的作用域的时候才会析构,如果继续使用左值的资源,可能会发生意想不到的错误。
2 没有提供移动构造、赋值函数,使用拷贝构造、赋值函数
如果没有提供移动构造/赋值函数,只提供了拷贝构造/赋值函数,编译器找不到移动构造/赋值函数就会去寻找拷贝构造/赋值函数
3 c++11中的所有容器都实现了移动语义,避免对含有资源的对象发生无畏的拷贝
4 移动语义对于拥有资源(如内存、文件句柄)的对象有效,如果是基本类型,使用移动语义没有意义

3、完美转发 std::forward()

如果模版中(包含类模版和函数模版)函数的参数写成 T&& 参数名, 那么函数既可以接收左值引用,右可以接受右值引用。

模版函数:std::forward(参数)用于转发参数。如果参数是一个右值,转发之后仍是右值引用,如果参数是一个左值,转发之后仍是左值引用。

#include <iostream>

using namespace std;
void func1(int &i ){
    cout<< "参数是左值"<< i << endl;
}

void func1(int && i){
    cout << "参数是右值" << i << endl;
}

//template<typename T>
//void func2(T &i){
//    cout << "func2 1111" << endl;
//    func1(i);
//}
//
//template<typename T>
//void func2(T&& i){
//    cout << "func2 2222" << endl;
//    func1(move(i));
//}

template<typename T>
void func2(T&& i){
    func1(forward<T>(i));
}

//void func2(int& i){
//    func1(i);
//}
//
//void func2(int&&i ){
//    func1(move(i));
//}

int main(void) {
    int a = 3;

    func2(a);
    func2(9);
}

std::forward

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。