左值引用与右值引用

目录

右值 与 左值

右值引用

右值引用的用处

move

引用折叠

forward


右值 与 左值

c++11增加了一个新的类型,右值引用,记作:&& 。

● 左值是指在内存中有明确的地址,我们可以找到这块地址的数据(可取地址)。

● 右值是只提供数据,无法找到地址(不可取地址)。

○ 所有有名字的变量都是左值,而右值是匿名的。

一般情况下位于等号左边的是左值,位于等号右边的是右值,但是也可以出现左值给左值赋值的情况。

c++11中右值分为两种情况:一个是将亡值,另一个是纯右值。

● 纯右值:非引用返回的临时变量,运算表达式产生的临时变量,原始字面量,lambda表达式等。

● 将亡值:与右值引用相关的表达式,比如:T&& 类型函数的返回值,std::move()的返回值等。

右值引用

右值引用就是对右值引用的类型。因为右值是匿名的,所以我们只能通过引用的方式找到它。无论是左值引用还是右值引用都必须被初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。通过右值引用,该右值所占的内存又可以被使用。

int&& value = 520; // 右值引用,520是字面量,是右值
class Test
{
public:
    Test()
    {
        cout << "构造函数" << endl;
    }
    Test(const Test& a)
    {
        cout << "拷贝构造函数" << endl;
    }
};
Test getObj()
{
    return Test();
}
int main()
{
    int a1;
    //int &&a2 = a1;        // 报错 右值引用不能被左值初始化
    //Test& t1 = getObj();   // 右值不能初始化左值引用
    Test && t2 = getObj(); //函数返回的临时对象是右值,可以被引用
    const Test& t3 = getObj();// 常量左值引用是万能可以接收左值,右值,常量左值,常量右值
    const int& t3 = a1; //被左值初始化
    return 0;
}

右值引用的用处

在c++用对象初始化对象时会调用拷贝构造,如果这个对象占用堆内存很大,那么这个拷贝的代价就是非常大的,在某些情况,如果想要避免对象的深拷贝,就可以使用右值引用进行性能的优化。

class Test
{
public:
    Test() : m_num(new int(100))
    {
        cout << "构造函数" << endl;
    }
    Test(const Test& a) : m_num(new int(*a.m_num))
    {
        cout << "拷贝构造函数" << endl;
    }
    ~Test()
    {
        delete m_num;
    }
    int* m_num;
};
Test getObj()
{
    Test t;
    return t;
}
int main()
{
    Test t = getObj();
    cout << "t.m_num: " << *t.m_num << endl;
    return 0;
};

这段代码在调用Test t = getObj(); 的时候调用了拷贝构造函数,对返回的临时对象进行了深拷贝得到了对象t,在getObj函数中创建的对象虽然进行了内存申请操作,但是没有使用就被释放掉了。如果我们在函数结束后仍然可以利用在函数里面申请的空间就极大的节省了创建对象和释放对象的时间。这个操作就需要我们的右值引用来完成。

右值引用具有移动语义,移动语义可以将堆区资源,通过浅拷贝从一个对象转移到另一个对象这样就能减少不必要的临时对象的创建,拷贝以及销毁,大幅度提高性能。

class Test
{
public:
    Test() : m_num(new int(100))
    {
        cout << "构造函数" << endl;
    }
    Test(const Test& a) : m_num(new int(*a.m_num))
    {
        cout << "拷贝构造函数" << endl;
    }
    // 添加移动构造函数,参数是右值引用
    Test(Test&& a) : m_num(a.m_num) 
    {
        a.m_num = nullptr;
        cout << "移动构造函数" << endl;
    }
    ~Test()
    {
        delete m_num;
        cout << "析构函数" << endl;
    }
    int* m_num;
};
Test getObj()
{
    Test t;
    return t;
}
int main()
{
    Test t = getObj(); // 因为getObj 返回的是右值,所以调用移动构造函数
    cout << "t.m_num: " << *t.m_num << endl;
    return 0;
};

● 在上面的代码中添加了 移动构造函数(参数为右值引用类型),这样在进行 Test t = getObj();并没有调用构造函数进行深拷贝,而是调用的(浅拷贝)移动构造,提高了性能。

● 本例子中,getObj()返回值是一个右值,在进行赋值操作的时候如果  等号 右边是一个右值,那么移动构造函数就会被调用。

结论:需要动态申请大量的资源的类,应该设计移动构造,提高程序的效率。需要注意的是在提供移动构造的同时,一般也会提供左值引用拷贝构造函数,左值初始化新对象时会走拷贝构造。

move

c++11添加了右值引用,却不能左值初始化右值引用,在一些特定的情况下免不了需要左值初始化右值引用(用左值调用移动构造),如果想要用左值初始化一个右值引用需要借助std::move() 函数。move() 函数可以将左值转换为右值。

#includeusing namespace std;
class Test
{
public:
    int a = 3;
    int* m_num = &a;
    Test() : m_num(new int(100))
    {
        cout << "构造函数" << endl;
    }
    Test(const Test& other) : m_num(new int(*other.m_num))
    {
        cout << "拷贝构造函数" << endl;
    }
    // 添加移动构造函数
    Test(Test&& a) : m_num(a.m_num)
    {
        a.m_num = nullptr;
        cout << "移动构造函数" << endl;
    }
    ~Test()
    {
        delete m_num;
        cout << "析构函数" << endl;
    }
};
Test getObj()
{
    Test t;
    return t;
}
int main()
{
    Test t = getObj();
    Test t2 = move(t); //此处调用移动构造,因为move返回一个右值
    return 0;
};

引用折叠

c++中,并不是所有情况下&&都代表右值引用,在模板和自动类型推导(auto)中,如果是模板参数需要指定为T&&,如果是自动类型推导需要指定为auto &&,这两种情况下&&被称作 未定的引用类型。另外 const T &&表示 一个右值引用,不是未定引用类型。

templatevoid fun(T&& param)
{
    work(forward(param))
}
int main()
{
    fun(10); //对于 f(10) 来说传入的实参 10 是右值,因此 T&& 表示右值引用
    int x = 1;
    fun(x);//对于 f(x) 来说传入的实参是 x 是左值,因此 T&& 表示左值引用
    return 0;
}

因为T&& 或者auto&&这种未定引用类型作为参数时,有可能被推导成右值引用,也有可能被推导为左值引用,在进行类型推导时右值引用会发生变化,这种变化被称作引用折叠。折叠规则如下:

● 通过右值 推导T&&或者auto&& 得到的是一个右值引用类型,const T &&表示 一个右值引用

● 通过非右值(右值引用、左值、左值引用、常量右值引用、常量左值引用)推导T&& 或者auto&&得到的是一个左值引用类型

int main()
{
    int&& a1 = 1;       //右值推导为               右值引用
    auto&& bb = a1; //右值引用推导为       左值引用
    auto&& bb1 = 2; //右值推导为              右值引用
    int a2 = 1;        
    int &a3 = a2;          //左值推导为                      左值引用
    auto&& cc = a3;    //左值引用推导为               左值引用
    auto&& cc1 = a2; //左值推导为                       左值引用
    const int& s1 = 1; // 常量左值引用
    const int&& s2 = 1;// 常量右值引用
    auto&& dd = s1;    // 常量左值引用推导为              左值引用
    auto&& ee = s2;   // 常量右值引用推导为               左值引用
    return 0;
}

forward

右值引用类型是独立于值的,一个右值引用作为函数参数的形参时,在函数内部转发该参数给内部其他函数时,他就变成了一个左值(当右值被命名是编译器会认为他是个左值),并不是原来的类型了。如果按照参数原来的类型转发到另一个函数,可以使用c++11的  std::forward()函数,该函数实现的功能称之为完美转发。

// 函数原型
template  T&& forward(typename remove_reference::type& t) noexcept;
template  T&& forward(typenameremove_reference::type&& t) noexcept;
// 精简之后的样子
std::forward(t);

● 当T为左值引用类型时,t将会被转换为左值

● 当T不是左值引用类型时,t将被转换为T类型的右值

#include using namespace std;
templatevoid printValue(T& t)
{
    cout << "左值引用: " << t << endl;
}
templatevoid printValue(T&& t)
{
    cout << "右值引用:" << t << endl;
}
templatevoid testForward(T && v)
{
    printValue(v);
    printValue(move(v));
    printValue(forward(v));
    cout << endl;
}
int main()
{
    testForward(520);
    int num = 1314;
    testForward(num);
    testForward(forward(num));
    testForward(forward(num));
    testForward(forward(num));
    return 0;
}