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;
算术运算符的运算对象和求值结果都是右值,但是上面的例子使用的却是左值a和b。
该原则有一个例外情况——右值引用(见本文第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个重要原则,我们必须保证接下来:
- 所引用的对象要被销毁
- 该对象没有其他的代码在使用
上述代码,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++为了区分开左值和右值创建的。我们可以使用右值引用实现内存的移动操作,避免二次开销。
本文深入探讨C++中的左值与右值概念,解析其定义、性质及应用,包括左值引用和右值引用的使用原则,以及类型转换的方法。
2281





