理解 C/C++ 中的左值和右值

栏目: C++ · 发布时间: 5年前

内容简介:本文翻译自 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 修饰的类型。如果它是 structunion ,它的成员都(递归地)不应含有 const 修饰的类型。

左值与右值间的转换

通常来说,计算对象的值的语言成分,都使用右值作为参数。例如,两元加法操作符 '+' 就需要两个右值参数,并返回一个右值:

int a = 1;     // a 是左值
int b = 2;     // b 是左值
int c = a + b; // + 需要右值,所以 a 和 b 被转换成右值
               // + 返回右值

在例子中, ab 都是左值。因此,在第三行中,它们经历了隐式的 左值到右值转换 。除了数组、函数、不完整类型的所有左值都可以转换为右值。

那右值能否转换为左值呢?当然不能!根据左值的定义,这违反了左值的本质。【注:右值可以显式地赋值给左值。之所以没有隐式的转换,是因为右值不能使用在左值应当出现的位置。】

不过,右值可以通过一些更显式的方法产生左值。例如,一元解引用操作符 '*' 需要一个右值参数,但返回一个左值结果。考虑这样的代码:

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++ 中一个常见的习惯成为可能:函数的参数使用常量引用接收参数,避免创建不必要的临时对象。

(未完待续)


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Head First HTML5 Programming

Head First HTML5 Programming

Eric Freeman、Elisabeth Robson / O'Reilly Media / 2011-10-18 / USD 49.99

What can HTML5 do for you? If you're a web developer looking to use this new version of HTML, you might be wondering how much has really changed. Head First HTML5 Programming introduces the key featur......一起来看看 《Head First HTML5 Programming》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具