【C++进阶】右值引用和移动语义

1. 引言

C++11中引入了右值引用和移动语义,可以避免无谓的复制,提高了程序性能

  • 提高程序性能:传统的拷贝构造函数会对对象进行深拷贝,这会导致内存的频繁分配和释放,降低程序的性能。而移动构造函数利用右值引用,可以将对象的资源所有权转移,避免了不必要的内存分配和释放,从而提高程序性能。
  • 支持移动语义:移动语义是指将一个对象的资源所有权转移给另一个对象,而不是进行拷贝。移动语义在实现容器类时特别有用,可以避免不必要的拷贝和内存分配,提高程序的效率。
  • 支持完美转发:右值引用还可以用于完美转发,即将一个函数的参数转发给另一个函数,同时保留参数的值类别(左值或右值)。这在模板编程中特别有用,可以避免不必要的拷贝和内存分配,提高程序的效率。
  • 支持移动语义的STL容器:C++11中的STL容器(例如vector、list等)都支持移动语义,可以提高程序的效率。

左值是表达式结束后仍然存在的持久对象,右值是指表达式结束时就不存在的临时对象

  • 区分左值和右值的便捷方法是看能不能对表达式取地址,如果能则为左值,否则为右值
  • 将亡值是C++11新增的、与右值引用相关的表达式,比如:
    • 右值引用(例如:int&& x = 42;
    • std::move返回的右值引用(例如:int x = 42; int&& y = std::move(x);
    • std::forward返回的右值引用(例如:template void func(T&& t) { f(std::forward(t)); }

2. 不同值的辨析

C++11中的所有的值必将属于左值、将亡值、纯右值三者之一,将亡值和纯右值都属于右值

如何区分将亡值和纯右值:

  • 将亡值是指一个对象即将被移动,但它的生命周期仍然可以延长的值。而纯右值是指一个临时对象,它的生命周期只在当前表达式中存在,不能被延长。
  • 在 C++11 中,可以通过std::move函数将一个对象转换为将亡值。在某些情况下,编译器也会自动将一个对象识别为将亡值,例如返回一个std::vector对象时。而纯右值则通常是字面量、临时对象或者返回值表达式。
  • 区分将亡值和纯右值的关键在于它们的生命周期。如果一个对象的生命周期可以被延长,那么它就是将亡值;如果一个对象的生命周期只在当前表达式中存在,那么它就是纯右值。

区分表达式的左右值属性:如果可对表达式用&符取址,则为左值,否则为右值

左值 lvalue 是有标识符、可以取地址的表达式,最常见的情况有:

  • 变量、函数或数据成员的名字
  • 返回左值引用的表达式,如 ++x、x = 1、cout << ’ ’

纯右值 prvalue 是没有标识符、不可以取地址的表达式,一般也称之为“临时对象”,最常见的情况有:

  • 返回非引用类型的表达式,如 x++、x + 1、std::make_shared(42)
  • 字符串字面量和除字符串字面量之外的字面量,如 42、true

3. &&的特性

右值引用允许我们将一个右值(即临时对象或表达式的结果)绑定到一个可以修改的引用上,从而避免了不必要的内存拷贝和对象构造。下面是一个右值引用的例子:

#include <iostream>
#include <vector>

using namespace std;

void print(vector<int>&& v)
{
   
   
    for (auto i : v)
        cout << i << " ";
    cout << endl;
}

int main()
{
   
   
    vector<int> v1{
   
   1, 2, 3};
    print(move(v1)); // 将v1作为右值引用传递给print函数
    return 0;
}

右值引用就是对一个右值进行引用的类型,因为右值没有名字,所以我们只能通过引用的方式找到它。

无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所把绑定对象的内存,只是该对象的一个别名。

通过右值引用的声明,该右值又“重获新生”,其生命周期其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。

总结:

  • 左值和右值是独立于它们的类型的,右值引用类型可能是左值也可能是右值
  • auto&&或函数参数类型自动推导的T&&是一个未定的引用类型,被称为universal references,它可能是左值引用也可能是右值引用类型,取决于初始化的值类型
  • 所有的右值引用叠加到右值引用上仍然是一个右值引用,其他引用叠加都为左值引 用,当T&&为模板参数时,输入左值,它会变成左值引用,而输入右值时则变为具名的右值引用
  • 编译器会将已命名的右值引用视为左值,而将未命名的右值引用视为右值

4. 左值引用和右值引用

左值引用和右值引用的主要区别在于它们绑定到不同类型的值上。左值引用绑定到左值上,而右值引用绑定到右值上。

左值是指可以取地址的表达式,通常是已命名的变量或对象。例如:

int x = 10;
int& lref = x; // 左值引用绑定到 x 上

右值是指不能取地址的表达式,通常是临时创建的对象或字面量。例如:

int&& rref = 5; // 右值引用绑定到字面量 5 上

左值引用和右值引用的另一个区别是它们对于移动语义和完美转发的支持。右值引用通常用于实现移动语义和完美转发,而左值引用则用于传递和修改已命名的对象。如下是一个简单的示例:

#include <iostream>
#include <vector>
using namespace std;

void printVector(vector<int>& v) // 左值引用参数
{
   
   
    for (auto i : v)
        cout << i << " ";

    cout << endl;
}

void printVector(vector<int>&& v) // 右值引用参数
{
   
   
    for (auto i : v)
        cout << i << " ";

    cout << endl;
}

int main()
{
   
   
    vector<int> v1 = {
   
   1, 2, 3, 4, 5};
    vector<int> v2 = {
   
   6, 7, 8, 9, 10};

    // 左值引用参数
    printVector(v1);

    // 右值引用参数
    printVector(move(v2));

    return 0;
}

函数printVector()接受一个右值引用参数,第一次调用该函数时,它将接受一个左值 v1,因为 v1 是已命名的对象;第二次调用该函数时,它将接受一个右值 move(v2),因为move()返回一个右值引用,将 v2 转换为右值。

5. 右值引用优化性能

第一个例子是在函数中传递一个大型对象,可以使用右值引用来避免深拷贝和提高性能。例如:

#include <iostream>
#include <vector>

// 接受一个 std::vector<int> 的右值引用
void process(std::vector<int>&& vec)
{
   
   
    // 对 vec 进行一些操作
    std::cout << "The vector has " << vec.size() << " elements.\n";
    // ...
}

int main()
{
   
   
    std::vector<int> v = {
   
   1, 2, 3, 4, 5};

    // 调用 process 时将 v 转换为右值引用
    process(std::move(v));

    // 此时 v 已经被移动,无法再使用
    // std::cout << v[0] << std::endl; // 会导致编译错误

    return 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ricky_0528

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值