内容简介:本文翻译自 Eli Bendersky’s website。原文:翻译者:nettee
本文翻译自 Eli Bendersky’s website。
原文: Understanding lvalues and rvalues in C and C++
翻译者:nettee
我们在 C/C++ 编程中并不会经常用到 左值 (lvalue)
和 右值 (rvalue)
两个术语。然而一旦遇见,又常常不清楚它们的含义。最可能出现两这个术语的地方是在编译错误或警告的信息中。例如,使用 gcc
编译以下代码时:
int foo() {return 2;} int main() { foo() = 2; return 0; }
你会得到:
test.c: In function 'main': test.c:8:5: error: lvalue required as left operand of assignment
没错,这个例子有点夸张,不像是你能写出来的代码。不过错误信息中提到了左值 (lvalue)。另一个例子是当你用 g++
编译以下代码:
int& foo() { return 2; }
现在错误信息是:
testcpp.cpp: In function 'int& foo()': testcpp.cpp:5:12: error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'
同样的,错误信息中提到了术语右值 (rvalue)。那么,在 C 和 C++ 中, 左值 和 右值 到底是什么意思呢?我这篇文章将会详细解释。
简单的定义
这里我故意给出了一个 左值 和 右值 的简化版定义。文章剩下的部分还会进行详细解释。
左值 (lvalue, locator value) 表示了一个占据内存中某个可识别的位置(也就是一个地址)的对象。
右值 (rvalue) 则使用排除法来定义。一个表达式不是 左值 就是 右值 。 那么,右值是一个 不 表示内存中某个可识别位置的对象的表达式。
举例
上面的术语定义显得有些模糊,这时候我们就需要马上看一些例子。我们假设定义并赋值了一个整形变量:
int var; var = 4;
赋值操作需要左操作数是一个左值。 var
是一个有内存位置的对象,因此它是左值。然而,下面的写法则是错的:
4 = var; // 错误! (var + 1) = 4; // 错误!
常量 4
和表达式 var + 1
都不是左值(也就是说,它们是右值),因为它们都是表达式的临时结果,而没有可识别的内存位置(也就是说,只存在于计算过程中的每个临时寄存器中)。因此,赋值给它们是没有任何语义上的意义的——我们赋值到了一个不存在的位置。
那么,我们就能理解第一个代码片段中的错误信息的含义了。 foo
返回的是一个临时的值。它是一个右值,赋值给它是错误的。因此当编译器看到 foo() = 2
时,会报错——赋值语句的左边应当是一个左值。
然而,给函数返回的结果赋值,不一定总是错误的操作。例如,C++ 的引用让我们可以这样写:
int globalvar = 20; int& foo() { return globalvar; } int main() { foo() = 10; return 0; }
这里 foo
返回一个引用。 引用一个左值
,因此可以赋值给它。实际上,C++ 中函数可以返回左值的功能对实现一些重载的操作符非常重要。一个常见的例子就是重载方括号操作符 []
,来实现一些查找访问的操作,如 std::map
中的方括号:
std::map<int, float> mymap; mymap[10] = 5.6;
之所以能赋值给 mymap[10]
,是因为 std::map::operator[]
的重载返回的是一个可赋值的引用。
可修改的左值
左值一开始在 C 中定义为“可以出现在赋值操作左边的值”。然而,当 ISO C 加入 const
关键字后,这个定义便不再成立。毕竟:
const int a = 10; // 'a' 是左值 a = 10; // 但不可以赋值给它!
于是定义需要继续精化。不是所有的左值都可以被赋值。可赋值的左值被称为 可修改左值 (modifiable lvalues) 。C99标准定义可修改左值为:
[…] 可修改左值是特殊的左值,不含有数组类型、不完整类型、const 修饰的类型。如果它是 struct
或 union
,它的成员都(递归地)不应含有 const 修饰的类型。
左值与右值间的转换
通常来说,计算对象的值的语言成分,都使用右值作为参数。例如,两元加法操作符 '+'
就需要两个右值参数,并返回一个右值:
int a = 1; // a 是左值 int b = 2; // b 是左值 int c = a + b; // + 需要右值,所以 a 和 b 被转换成右值 // + 返回右值
在例子中, a
和 b
都是左值。因此,在第三行中,它们经历了隐式的 左值到右值转换
。除了数组、函数、不完整类型的所有左值都可以转换为右值。
那右值能否转换为左值呢?当然不能!根据左值的定义,这违反了左值的本质。【注:右值可以显式地赋值给左值。之所以没有隐式的转换,是因为右值不能使用在左值应当出现的位置。】
不过,右值可以通过一些更显式的方法产生左值。例如,一元解引用操作符 '*'
需要一个右值参数,但返回一个左值结果。考虑这样的代码:
int arr[] = {1, 2}; int* p = &arr[0]; *(p + 1) = 10; // 正确: p + 1 是右值,但 *(p + 1) 是左值
相反地,一元取地址操作符 '&'
需要一个左值参数,返回一个右值:
int var = 10; int* bad_addr = &(var + 1); // 错误: 一元 '&' 操作符需要左值参数 int* addr = &var; // 正确: var 是左值 &var = 40; // 错误: 赋值操作的左操作数需要是左值
在 C++ 中 '&'
符号还有另一个功能——定义引用类型。引用类型又叫做“左值引用”。因此,不能将一个右值赋值给(非常量的)左值引用:
std::string& sref = std::string(); // 错误: 非常量的引用 'std::string&' 错误地使用右值 'std::string` 初始化
常量的 左值引用可以使用右值赋值。因为你无法通过常量的引用修改变量的值,也就不会出现修改了右值的情况。这也使得 C++ 中一个常见的习惯成为可能:函数的参数使用常量引用接收参数,避免创建不必要的临时对象。
(未完待续)
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 理解原型其实是理解原型链
- 要理解深度学习,必须突破常规视角去理解优化
- 深入理解java虚拟机(1) -- 理解HotSpot内存区域
- 荐 【C++100问】深入理解理解顶层const和底层const
- 深入理解 HTTPS
- 深入理解 HTTPS
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
代码大全(第2版)
[美] 史蒂夫·迈克康奈尔 / 金戈、汤凌、陈硕、张菲 译、裘宗燕 审校 / 电子工业出版社 / 2006-3 / 128.00元
第2版的《代码大全》是著名IT畅销书作者史蒂夫·迈克康奈尔11年前的经典著作的全新演绎:第2版不是第一版的简单修订增补,而是完全进行了重写;增加了很多与时俱进的内容。这也是一本完整的软件构建手册,涵盖了软件构建过程中的所有细节。它从软件质量和编程思想等方面论述了软件构建的各个问题,并详细论述了紧跟潮流的新技术、高屋建瓴的观点、通用的概念,还含有丰富而典型的程序示例。这本书中所论述的技术不仅填补了初......一起来看看 《代码大全(第2版)》 这本书的介绍吧!
HTML 压缩/解压工具
在线压缩/解压 HTML 代码
HEX HSV 转换工具
HEX HSV 互换工具