前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【译】理解C和C++中的左值和右值

【译】理解C和C++中的左值和右值

作者头像
用户6557940
发布2022-07-24 16:42:13
1.1K0
发布2022-07-24 16:42:13
举报
文章被收录于专栏:Jungle笔记

关于左值和右值的理解:

  • 赋值号左边的是左值,右边的是右值?
  • 可以写在赋值号左边的是左值,否则是右值?
  • 有明确内存地址的是左值,在内存中没有明确地址的是右值?
  • ……

网上看到了这一篇文章,于是乎逐字翻译了下(没有完全直译,附注部分也没有译完)。

原文地址:

https://eli.thegreenplace.net/2011/12/15/understanding-lvalues-and-rvalues-in-c-and-c/

尽管术语“左值”和“右值”在C和C++编程中并不经常使用,但一旦使用到左值和右值,它们的含义好像并非那么清楚。我们最常遇到这些术语的地方可能是编译过程中的error或者warning字段的提示信息。比如我们用gcc编译如下一段代码:

代码语言:javascript
复制
int foo() {return 2;}

int main()
{
    foo() = 2;

    return 0;
}

编译器将会提示:

代码语言:javascript
复制
test.c: In function 'main':
test.c:8:5: error: lvalue required as left operand of assignment

这段代码的确不合法,并且我们也不会如此写代码。但编译器里的error字段信息的确提到了lvalue,即左值。这个术语通常不会在C和C++教程里提到。我们再用g++编译下段代码:

代码语言:javascript
复制
int& foo()
{
    return 2;
}

现在报的error会是:

代码语言:javascript
复制
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,即右值。那么到底左值和右值具体是什么含义呢?这即是我在本文想要深入探讨的。

一个简单的定义

本小节旨于提出一个简化版的关于左值和右值的定义,然后在其余小节将逐步准确地丰富这个定义。

一个左值,代表一个在内存中占有确定位置的对象,简言之,左值在内存中有地址;

右值是什么呢?我们这么来定义:非左即右。一个对象不是左值就是右值,如果能够通过左值的定义判断一个对象是左值,那么它就是左值;否则就是右值。通过上述左值的定义也可以看出,右值在内存中没有确定位置的地址。

简单示例

上述定义可能有点模糊,因此本节举几个简单例子来说明一下。假设有一个int型变量,其声明和定义如下:

代码语言:javascript
复制
int var;
var = 4;

赋值操作要求一个左值作为其左操作数,var就是一个左值,因为var是一个int变量,在内存中有确定位置。相反,下列代码是错误的:

代码语言:javascript
复制
4 = var;       // ERROR!
(var + 1) = 4; // ERROR!

不论是常量4,还是表达式var+1都不是左值(都是右值),因为它们只是表达式的临时的结果,可能只是在计算过程中保存在了临时的寄存器中,而在内存中并没有确定地址。因此,赋值给一个不具有明确地址的对象的操作,是无意义的。

现在再倒回去看上面报的两处error。foo()函数返回的int型对象只保存在临时的寄存器中,不具有明确地址,赋值给foo()当然会报错。

不过,并不是所有对于函数返回值的赋值操作都是无效的(invalid),C++中的引用,让这样的操作变得合法:

代码语言:javascript
复制
int globalvar = 20;

int& foo()
{
    return globalvar;
}

int main()
{
    foo() = 10;
    return 0;
}

上述代码,foo()函数返回值是一个int型的引用,引用,是一个左值。C++中这样的性质,对于一些运算符的重载的实现,是非常重要的。最常见的例子,运算符[],可以实现对容器的随机访问:

代码语言:javascript
复制
std::map<int, float> mymap;
mymap[10] = 5.6;

对mymap[10]的赋值操作是合法的,因为非const重载std::map::operator[]返回一个引用,而引用是可以被赋值的。

可更改的左值

最开始在C语言中对左值的定义是“可以放在赋值运算符左边的对象”。然而,当ISO C增加了const关键字后,这个定义必须被修正。比如:

代码语言:javascript
复制
const int a = 10; // 'a' is an lvalue
a = 10;           // but it can't be assigned!

上述代码说明,并不是所有的左值都可以被赋值,可以被赋值的,只能说是“可更改的左值”。C99标准正式地定义了可修改的左值

一个左值不是数组类型,没有不完全类型,不能有const修饰,如果它是一个结构体或者联合union,则不能有任何用const修饰的成员(包括)。

左值和右值之间的转化

通常来说,如果要构造一个对象,需要一个右值作为参数。比如,操作符“+”需要两个右值作为参数,然后返回一个右值:

代码语言:javascript
复制
int a = 1;     // a is an lvalue
int b = 2;     // b is an lvalue
int c = a + b; // + needs rvalues, so a and b are converted to rvalues
               // and an rvalue is returned

如前所述,a和b都是左值,但在上述代码的第三行,它们却经历一次从左值到右值的转换。所有的非数组、非函数或不完全类型都可以转换成右值。

反过来呢?右值可以转换成左值吗?不可以!这会严重违背我们之前对左值的定义!【1】

当然,右值可以通过显式转换成左值。比如,解引用运算符“*”将使用一个右值作为操作数,但其结果是一个左值。比如,下述代码是合法的:

代码语言:javascript
复制
int arr[] = {1, 2};
int* p = &arr[0];
*(p + 1) = 10;   // OK: p + 1 is an rvalue, but *(p + 1) is an lvalue

反过来,运算符&作用于一个左值,返回一个右值:

代码语言:javascript
复制
int var = 10;
int* bad_addr = &(var + 1); // ERROR: lvalue required as unary '&' operand
int* addr = &var;           // OK: var is an lvalue
&var = 40;                  // ERROR: lvalue required as left operand
                            // of assignment

运算符“&”在C++中还有一个作用——定义引用类型,称为“左值引用”。右值不能赋给一个非const的左值引用,因为这要求一个无效的从右值到左值的转换。

代码语言:javascript
复制
std::string& sref = std::string();  // ERROR: invalid initialization of
                                    // non-const reference of type
                                    // 'std::string&' from an rvalue of
                                    // type 'std::string'

右值可以赋给一个const的左值引用。因为这个引用是const修饰,不能通过引用被修改,所以修改右值是可以的。这样的性质,使得在C++中将一个值的常量引用作为参数传入函数十分常见,这也避免了临时对象不必要的复制和构造。

CV限定的右值

如果我们仔细阅读,C++ standard discussing lvalue-to-rvalue conversions【2】中写道:

一个非函数、非数组类型的左值T可以被转换为右值,如果T不是一个class类型,那么这个右值是一个非cv限定的T的版本。否则,这个右值的类型是T。

非cv限定是什么鬼?cv是用于描述const和volatile类型的限定符

一个类型(非cv限定的完全类型、或是不完全类型、或空类型)都有三个对应的cv限定的版本:cosnt限定的版本,volatile限定的版本和cv限定的版本。cv限定和非cv限定的版本是两种不同的类型,但它们要有相同的representation(?)和对齐要求。

这和右值有什么联系?在C中,右值不会有cv限定符,只有左值有。在C++中,类的右值可以有cv限定符,但内置类型(int、double等)不可以。比如:

代码语言:javascript
复制
#include <iostream>

class A {
public:
    void foo() const { std::cout << "A::foo() const\n"; }
    void foo() { std::cout << "A::foo()\n"; }
};

A bar() { return A(); }
const A cbar() { return A(); }


int main()
{
    bar().foo();  // calls foo
    cbar().foo(); // calls foo const
}

main函数中的第二个调用实际上会调用A的foo() const函数,因为cbar()的返回值是一个const A,这与A不同。注意,cbar()的返回值是一个右值,所以这是一个cv限定的右值的例子。

右值引用(C++11)

C++11标准中介绍到了两个及其重要的概念:右值引用移动语义。一些文章对这些特性有全面的论述【3】。本文笔者仍将举一些简单的例子,以此来证明对左值右值的深入理解,是如何帮助我们去探究语言的一些重要的概念的。

本文的前述内容讲述了左值和右值的主要区别,即左值可以被修改,而右值不能。但C++11对这个区别来了个大反转,它允许在一些特殊场合下对右值的引用,故可以修改它们。

在接下来这个例子,我们考虑实现一个简单版本的integer vector:

代码语言:javascript
复制
class Intvec
{
public:
    explicit Intvec(size_t num = 0)
        : m_size(num), m_data(new int[m_size])
{
        log("constructor");
    }

    ~Intvec()
    {
        log("destructor");
        if (m_data) {
            delete[] m_data;
            m_data = 0;
        }
    }

    Intvec(const Intvec& other)
        : m_size(other.m_size), m_data(new int[m_size])
    {
        log("copy constructor");
        for (size_t i = 0; i < m_size; ++i)
            m_data[i] = other.m_data[i];
    }

    Intvec& operator=(const Intvec& other)
    {
        log("copy assignment operator");
        Intvec tmp(other);
        std::swap(m_size, tmp.m_size);
        std::swap(m_data, tmp.m_data);
        return *this;
    }
private:
    void log(const char* msg)
{
        cout << "[" << this << "] " << msg << "\n";
    }

    size_t m_size;
    int* m_data;
};

我们定义了普通的构造函数、析构函数、复制构造函数和重载赋值运算符,每个函数里都增加了log,以便于我们查看程序调用了哪个函数。

接下来运行整个简单的代码,这里是将v1复制到v2:

代码语言:javascript
复制
Intvec v1(20);
Intvec v2;

cout << "assigning lvalue...\n";
v2 = v1;
cout << "ended assigning lvalue...\n";

打印的结果是:

代码语言:javascript
复制
assigning lvalue...
[0x28fef8] copy assignment operator
[0x28fec8] copy constructor
[0x28fec8] destructor
ended assigning lvalue...

注意——这里准确地展示了在赋值运算符=里发生了什么。但假如我们想将一个右值赋值给v2:

代码语言:javascript
复制
cout << "assigning rvalue...\n";
v2 = Intvec(33);
cout << "ended assigning rvalue...\n";

尽管我只是给v2赋值了一个刚构造的vector,这里展示了一个更通用的场景:临时的右值被构建并被赋给v2(这种场景是存在的,比如函数返回一个vector)。现在查看log:

代码语言:javascript
复制
assigning rvalue...
[0x28ff08] constructor
[0x28fef8] copy assignment operator
[0x28fec8] copy constructor
[0x28fec8] destructor
[0x28ff08] destructor
ended assigning rvalue...

欧!这里看起来有很多工作。尤其是,这里有多余的一对构造函数和析构函数,是用于创建和析构临时对象的。这让人震惊,因为在赋值运算符内部,另一个临时的拷贝正在进行(创建和析构),不过这是个没有用的额外的工作。

C++11制定的右值引用,使得我们可以实现“移动语义”,尤其是一个“”移动赋值运算符【5】。接下来我们添加另一个运算符=:

代码语言:javascript
复制
Intvec& operator=(Intvec&& other)
{
    log("move assignment operator");
    std::swap(m_size, other.m_size);
    std::swap(m_data, other.m_data);
    return *this;
}

在这里,“&&”语法是新的右值引用,它的作用如同其名,是一个右值的引用,并且在调用之后就被析构。我们可以理解为,它“偷”了右值内部的值,之后它便没有什么作用了。打印的log为:

代码语言:javascript
复制
assigning rvalue...
[0x28ff08] constructor
[0x28fef8] move assignment operator
[0x28ff08] destructor
ended assigning rvalue...

这里发生的,是移动赋值运算符被调用,因为一个右值被赋给了v2。Intvec(33)中创建了临时对象,所以构造函数和析构函数仍会被调用。但赋值运算符里的另一个临时对象就不再需要了。这个操作符将右值的内部缓存转换成它自己的,所以右值的析构函数释放时,会将我们这个对象的缓冲区也给释放了。

再次说明,上述示例只是右值引用和移动语义的冰山一角。正如你们所想,右值引用和移动语义是及其复杂的一个分支,需要考虑一些特殊的场景和目标。笔者在这里只是简单的展示了在C++中左值和右值的区别。

结论

也许我们可以在不关心左值和右值的情况下写出很多C++代码,至多把它们当做是编译器里error信息中一些奇怪的术语。然而,本文的目的是想帮助各位对C++代码有更深入的理解,并且能更加容易理解程序语言专家们制定的规范。

另一方面,C++11中介绍了右值引用和移动语义,新的C++规范中关于左值右值的论述越来越重要。要真正理解C++这些新特性,就必须深入理解左值和右值。

【1】右值可以被显示地赋给左值,应当使用左值的地方,右值不能被隐式地转换。

【2】C++11标准section 4.1。

【3】谷歌搜索“右值引用”可以发现很多相关主题的文章。

【4】从异常和安全的角度,这是一个标准的拷贝赋值运算符的实现。通过使用复制构造函数,然后不抛出异常std::swap,确保了如果异常抛出,不会有尚未初始化的内存在某个中间状态出现。

【5】现在你们知道为什么我坚持把operator= 称作拷贝赋值运算符。在C++11里,这个区别尤其重要。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-05-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Jungle笔记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档