【深度C++】之“左值与右值”

本文深入探讨C++中的左值与右值概念,解析其定义、性质及应用,包括左值引用和右值引用的使用原则,以及类型转换的方法。

0. 左值与右值

【深度C++】之“表达式与运算符”中,我们曾提到过:

表达式有一个性质,就是可以求值,对表达式求值将得到一个结果

这个结果到底是什么呢?是一个值?还是一个对象?如果是一个对象的话,我可不可以再修改它的内容?

这就是本文要讨论的左值与右值的性质。

C++中,对表达式求值得到的结果是一个对象或者一个函数,它不是左值(lvalue),就是右值(rvalue)

1. 左值与右值的定义

在C语言中,左值右值的区别,仅仅是因为赋值运算符=的两个运算对象,左侧的值和右侧的值。

我们可以利用赋值运算符判断对象是左值还是右值(编译器会报错),但是在C++中,只是沿用了他们的叫法,他们的区别非常的大。

《C++ Primer(第5版)》中描述左值和右值:

当对象被用作左值的时候,用的是对象的身份(在内存中的位置);当一个对象被用作右值的时候,用的是对象的值(内容)

例如,取地址运算符&作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值。

int k = 0;
int *pk = &k;
// 不可以写成&k = ***这样的形式 

上述代码&k返回一个右值,它的值是指向对象k的地址;通过赋值运算符=,将对象k的地址(右值) 赋值给了一个新定义的左值对象pk

左值右值是表达式的属性,也是运算符处理运算对象时对运算对象的要求及返回的结果的属性。详细的每类运算符对于运算对象的要求和结果的左右值属性,请参考【深度C++】之“优先级表及左右值”

2. 关联与区别

2.1 生存时间

左值持久,右值短暂

左值有持久的状态,通常是程序员声明、定义的对象。

右值要么是字面值常量,要么是表达式求值过程中创建的临时对象

2.2 访问关系

从定义直观的去理解,右值关注它的值(内容),不能被修改;左值可以获得它的身份(内存中的位置,内存中的地址),因此可以被修改,也可以通过地址获得它的值(内容)。

这里可以简单的理解为“读写”关系,右值可以读,不可以写;左值既可以读,也可以写。

但是他们不仅仅是“读写”的区别。

一个例外情况const限定符constexpr关键字修饰的对象,即使它是左值,也不可以被修改。

还有一个例外情况,就是使用右值引用绑定的右值对象,我们是可以对它进行赋值的,只不过我们不能对移后源对象的内容,作任何假设(见第3节)。

2.3 替代关系

2.3.1 左值替代右值

区分它们还有一个原则:需要右值的地方,都可以使用左值来替代。

例如:

int a = 5, b = 8;
int c = a + b;

算术运算符的运算对象和求值结果都是右值,但是上面的例子使用的却是左值ab

该原则有一个例外情况——右值引用(见本文第3节):获得右值引用的地方,不可以使用左值替代。

int a = 0;
int &&ra = a;  // 编译器报错

我们定义了一个右值引用ra,应该使用右值赋值给它,因此不可以使用a

那么如何赋值右值引用呢?刚提过,算术运算符返回右值:

int a = 0;
int &&temp = a + 3;

a + 3返回一个创建的临时对象,它是一个右值。可以使用右值引用&&temp绑定该右值。

2.3.2 右值赋给左值

右值就是可以赋值给左值的,这是最原初的用法。

这里简要提一下“引用”的案例,如下。

【深度C++】之“引用”中,我们提到过,可以将非常量类型的目标类型甚至是临时量绑定给const &,这是初始化一个引用类型的特殊情况。

int a = 0;
const int &ref = a + 3;
// a + 3 返回一个右值,是一个临时量,可以赋值给一个引用
// 但这个引用必须是const引用

【深度C++】之“const”中,我们解释了这个操作的原因,即有临时变量的创建。

接下来我们详细介绍左值引用和右值引用。

3. 左值引用与右值引用

要注意,左值和右值的内容已经讨论完毕,它们是表达式的一种属性。现在讨论的是另外的两类复合类型:左值引用右值引用,请读者区分开。

3.1 右值引用的声明和初始化

左值引用,就是我们之前一直说的引用,使用&运算符来声明。为左值引用初始化的一定是左值。如下:

int a = 0;
int &lr_a = a;

右值引用,使用&&运算符声明。为右值引用初始化的一定是右值,不可以使用左值代替(2.3例外情况)。如下:

int a = 0;
int &&rr_a = a + 3;

可以将右值引用绑定在要求转换的表达式、字面常量和返回右值的表达式上,不可以将右值引用绑定在左值上。因此下面的代码十分费解:

int &&rr_1 = 42;
int &&rr_2 = rr_1;  // 编译错误

rr_1单独使用,是一个表达式——一个没有运算符的表达式,它返回的是左值;因此不可以将右值引用绑定到左值rr_1上。

3.2 右值引用的使用原则

我们是可以使用右值引用进行赋值的(2.2例外情况),这也很意外,因为明明是右值的引用,却可以进行内容上的修改。因为右值引用是具有表达式的左值属性(如上例中的rr_1)。

int a = 0;
int &&rr_a = a + 3;
rr_a = 6;

使用右值引用有2个重要原则,我们必须保证接下来:

  1. 所引用的对象要被销毁
  2. 该对象没有其他的代码在使用

上述代码,a + 3的结果是一个临时量,它肯定是会在作用域结束后被销毁的。我们也没有机会访问到该临时量的真正身份,只能通过赋值=运算得到它的值。

对于返回左值的表达式:

返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符,都是返回左值的表达式的例子。我们可以将一个左值引用绑定到这类表达式的结果上。

对于返回右值的表达式:

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

(以上两句话摘选自摘选自《C++ Primer》第5版本,抽象却精湛,建议背诵)

使用右值引用的两个原则,在对象移动时,可以完美的使用。C++引入右值引用,也是为了进行对象的移动,详细内容,请参考【深度C++】之“对象移动”

3.3 类型转换

既然是数据类型,就存在转换关系。

我们可以使用标准库函数std::move得到左值的右值引用类型。

int &&rr_1 = 42;
int &&rr_2 = std::move(rr_1);

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

我们必须保证,接下来除了对rr_1赋值或销毁它,我们不再使用它。我们不能对rr_1的值作任何假设。

《C++ Primer(第5版)》推荐使用std::move而不是move,可以避免潜在的命名冲突。

4. 总结

左值右值是表达式的属性,也是运算符处理运算对象时对运算对象的要求及返回的结果的属性。右值要么是字面值常量,要么是表达式求值过程中创建的临时对象。左值通常是程序员创建的对象。

左值通常持久,右值通常是编译器创建的临时变量。左值大部分情况下可以修改,右值不可以,有两个例外情况。可以在要求使用右值的地方使用左值代替,有一个例外情况

左值引用右值引用是两种复合数据类型,他们都是左值,是C++为了区分开左值和右值创建的。我们可以使用右值引用实现内存的移动操作,避免二次开销。

评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值