C++ 左值、右值、左值引用、右值引用

前言

本文介绍C++11的各种引用的概念,理解清楚各种引用的概念,非常有助于理解基于c11引用的各种操作。

左右值概念

C++ 里有左值和右值,但C++按标准里的定义实际更复杂,规定了下面这些值类别(value categories):
在这里插入图片描述

  • 名词解释:
    一个 lvalue 是通常可以放在等号左边的表达式,左值
    一个 rvalue 是通常只能放在等号右边的表达式,右值
    一个 glvalue 是 generalized lvalue,广义左值
    一个 xvalue 是 expiring value,将亡值
    一个 prvalue 是 pure rvalue,纯右值

  • 左值 lvalue 是有标识符、可以取地址的表达式,最常见的情况有:
    变量、函数或数据成员的名字
    返回左值引用的表达式,如 ++x、x = 1、cout << ’ ’
    字符串字面量如 “hello world”
    在函数调用时,左值可以绑定到左值引用的参数,如 T&。一个常量只能绑定到常左值引用,如 const T&。

  • 纯右值 prvalue 是没有标识符、不可以取地址的表达式,一般也称之为临时对象,即临时对象就是一种右值,最常见的情况有:
    返回非引用类型的表达式,如 x++、x + 1、make_shared(42)
    除字符串字面量之外的字面量,如 42、true

  • 变量是左值

左右值区别

  • 左值持久,右值短暂
    左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。
    由于右值引用只能绑定到临时对象,所以使用右值引用的代码可以自由地接管所引用的对象的资源,即可以进行移动。

  • 移动右值,拷贝左值
    如果一个类既有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数,赋值操作的情况类似。

StrVec v1, v2;
v1 = v2; // 拷贝赋值,因为v2是左值
v1 = std::move(v2); // 强制变成右值了,使用移动赋值

StrVec getVec();
v2 = getVec();  // 移动赋值,因为getVec()是右值
  • 把 std::move(ptr1) 看作是一个有名字的右值。为了跟无名的纯右值 prvalue 相区别,C++ 里目前就把这种表达式叫做 xvalue。
    跟左值 lvalue 不同,xvalue 仍然是不能取地址的——这点上,xvalue 和 prvalue 相同。所以,xvalue 和 prvalue 都被归为右值 rvalue。
shared_ptr<shape> ptr1{new circle()}; // new circle() 就是一个纯右值
shared_ptr<shape> ptr2 = std::move(ptr1); // 可以把 std::move(ptr1) 看作是一个有名字的右值。为了跟无名的纯右值 prvalue 相区别,C++ 里目前就把这种表达式叫做 xvalue。

左值引用lvalue reference

当我们使用术语引用reference时,指的其实是左值引用(lvalue reference)。

  • 引用即别名,引用并非对象,它是为一个已经存在的对象所起的另一个名字。定义了一个引用之后,对其进行的所有操作都是在与之绑定的对象上进行的。

  • 引用必须初始化,一旦初始化完成,引用将和它的初始值对象一直绑定在一起:
    引用在初始化后,不可以改变;
    引用类型必须和绑定的对象的类型一致,但常量引用会丢失精度;
    引用类型的初始值必须是一个变量;
    常量引用可以引用常量;

  • 引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起;

    int c = 2;
    ra = c; // 赋值操作,把c赋值给b,即赋值给a
    // &ra = c; // 错误,引用在初始化后,不可以改变

// Non-const lvalue reference to type 'int' cannot bind to a temporary of type 'int'
    const int e = 10;
// int &re = e; // 错误,引用类型的初始值必须是一个对象
    const int &re = e;
//    int &re1 = e * 42; // 错误,引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起,a * 42是一个右值
    const int &re1 = e * 42; // 正确,可以将一个const引用绑定到一个右值上

    double d = 10.0F;
// Non-const lvalue reference to type 'int' cannot bind to a value of unrelated type 'double'
// int &rd = d; // 错误,引用类型必须和绑定的对象的类型一致,添加const后可以
    double &rd = d;
    const int &rd1 = d; // 正确,Clang-Tidy: Narrowing conversion from 'double' to 'int'

右值引用rvalue reference

为了支持移动操作,新标准引入了一种新的引用类型——右值引用。

  • 所谓右值引用就是必须绑定到右值的引用,我们通过&&而不是&来获得右值引用。

  • 右值引用有一个重要的性质:只能绑定到一个将要销毁的对象。
    因此,我们可以自由地将一个右值引用的资源“移动”到另一个对象中。

  • 一个右值引用也不过是某个对象的另一个名字而已。

  • 对于左值引用,我们不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式。
    右值引用有着完全相反的绑定特性:我们可以将一个右值引用绑定到这类表达式上,但不能将一个右值引用直接绑定到一个左值上。

  • 返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符,都是返回左值的表达式的例子。我们可以将一个左值引用绑定到这类表达式的结果上。
    返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符,都生成右值。我们不能将一个左值引用绑定到这类表达式上,但我们可以将一个const的左值引用或者一个右值引用绑定到这类表达式上。

std::move

虽然不能将一个右值引用直接绑定到一个左值上,但我们通过std::move函数可以显式地将一个左值转换为对应的右值引用类型。

  • move调用告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它。

  • 调用move就意味着承诺:
    除了对移后源对象进行赋值或销毁它外,我们将不再使用它。
    在调用move之后,我们不能对移后源对象的值做任何假设。
    我们可以销毁一个移后源对象,也可以赋予它新值,但不能使用一个移后源对象的值。

  • 不要随意使用移动操作
    通过在类代码中小心地使用move,可以大幅度提升性能。
    但由于一个移后源对象具有不确定的状态,对其调用std::move是危险的。当我们调用move时,必须绝对确认移后源对象没有其他用户。
    而如果随意在普通用户代码(与类实现代码相对)中使用移动操作,很可能导致莫名其妙的、难以查找的错误,而难以提升应用程序性能。

示例

    int a = 1;
    int &ra = a; // 引用必须要初始化
//    int &ra2 = 1; // 错误,引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起
//    int &&rra = a; // 错误,不能将一个右值引用绑定到左值上
//    int &ra3 = a * 42; // 错误,引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起,a * 42是一个右值
    const int &ra3 = a * 42; // 正确,可以将一个const引用绑定到一个右值上
    int &&rra2 = a * 42; // 正确,将rra2绑定到一个右值上
    int &&rra2 = a * 42; // 正确,将rra2绑定到一个右值上

//    int &&rrra = rra2; // 错误,表达式rra2是左值,变量是左值
    int &&rrra = std::move(rra2); // 正确,使用std::move将左值显式的变为右值
### C++左值引用引用的概念及区别 #### 定义与特性 在 C++ 中,表达式的分类对于理解引用至关重要。左值 (lvalue) 表达式通常表示具有持久状态的对象,而 (rvalue) 则指临时对象或字面量。 - **左值引用 (`T&`)**:只能绑定到左值,允许修改所引用的对象。 - **引用 (`T&&`)**:专门设计来绑定到,支持移动语义和完美转发[^1]。 #### 使用场景对比 | 类型 | 非常量左值引用 `T&` | 常量左值引用 `const T&` | 非常量引用 `T&&` | | --- | --- | --- | --- | | 绑定至左值 | 支持 | 支持 | 不支持 | | 绑定至 | 不支持 | 支持 | 支持 | 非常量左值引用主要用于传递参数以便函数内部能改变调用者提供的变量;常量左值引用则广泛应用于避免大对象复制开销的同时保持只读访问权限。特别地,在现代 C++ 编程实践中,非常量引用主要服务于两个目的: - 实现高效的资源转移——即所谓的“移动语义”,通过直接接管现有资源而非深拷贝; - 协助模板元编程中的“完美转发”。 ```cpp // 移动语义示例 class Widget { public: // 拷贝构造函数 Widget(const Widget &other); // 移动构造函数 Widget(Widget &&other) noexcept; }; std::vector<Widget> v; v.push_back(Widget()); // 调用了移动构造函数而不是拷贝构造函数 ``` #### 特殊情况说明 得注意的是,虽然常规情况下引用不允许绑定到左值上,但在特定条件下可以通过标准库提供的 `std::move()` 函数显式转换为可被引用捕获的状态。这并不意味着原对象真正变成了,而是告诉编译器该对象即将废弃,从而启用潜在的性能优化措施[^2]。 ```cpp int a = 42; int&& rref = std::move(a); // 显式将左值转成引用可以接受的形式 ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

baiiu

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

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

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

打赏作者

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

抵扣说明:

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

余额充值