C++ 中的左值和右值

一、前言一直以来,我都对C++中左值(lvalue)和右值(lvalue)的概念模糊不清。我认为是时候好好理解他们了,因为这些概念随着C++语言的进化变得越来越重要。二、左值和右值——一个友好的定义首先,让我们避开那些正式的定义。在C++中,一个左值是指向一个指定内存的东西。另一方面,右值就是不指向任何地方的东西。通常来说,右值是暂时和短命的,而左值则活的很久,因为他们以变量的形式(v…

大家好,又见面了,我是你们的朋友全栈君。

一、前言

一直以来,我都对C++中左值(lvalue)和右值(lvalue)的概念模糊不清。我认为是时候好好理解他们了,因为这些概念随着C++语言的进化变得越来越重要。

二、左值和右值——一个友好的定义

首先,让我们避开那些正式的定义。在C++中,一个左值是指向一个指定内存的东西。另一方面,右值就是不指向任何地方的东西。通常来说,右值是暂时和短命的,而左值则活的很久,因为他们以变量的形式(variable)存在。我们可以将左值看作为容器(container)而将右值看做容器中的事物。如果容器消失了,容器中的事物也就自然就无法存在了。
让我们现在来看一些例子:

int x = 666; //ok

在这里,666是一个右值。一个数字(从技术角度来说他是一个字面常量(literal constant))没有指定的内存地址,当然在程序运行时一些临时的寄存器除外。在该例中,666被赋值(assign)给xx是一个变量。一个变量有着具体(specific)的内存位置,所以他是一个左值。C++中声明一个赋值(assignment)需要一个左值作为它的左操作数(left operand):这完全合法。
对于左值x,你可以做像这样的操作:

int* y = &x;  //ok

在这里我通过取地址操作符&获取了x的内存地址并且把它放进了y&操作符需要一个左值并且产生了一个右值,这也是另一个完全合法的操作:在赋值操作符的左边我们有一个左值(一个变量),在右边我们使用取地址操作符产生的右值。
然而,我们不能这样写:

int y;
666 = y; //error!

可能上面的结论是显而易见的,但是从技术上来说是因为666是一个字面常量也就是一个右值,它没有一个具体的内存位置(memory location),所以我们会把y分配到一个不存在的地方。
下面是GCC给出的变异错误提示:

error: lvalue required as left operand of assignment

赋值的左操作数需要一个左值,这里我们使用了一个右值666
我们也不能这样做:

int* y = &666;//   error~

GCC给出了以下错误提示:

error: lvalue required as unary ‘&’ operand`

&操作符需要一个左值作为操作数,因为只有一个左值才拥有地址。

三、返回左值和右值的函数

我们知道一个赋值的左操作数必须是一个左值,因此下面的这个函数肯定会抛出错误:lvalue required as left operand of assignment

int setValue()
{
    return 6;
}

// ... somewhere in main() ...

setValue() = 3; // error!

错误原因很清楚:setValue()返回了一个右值(一个临时值6),他不能作为一个赋值的左操作数。现在,我们看看如果函数返回一个左值,这样的赋值会发生什么变化。看下面的代码片段(snippet):

int global = 100;

int& setGlobal()
{
    return global;    
}

// ... somewhere in main() ...

setGlobal() = 400; // OK

该程序可以运行,因为在这里setGlobal()返回一个引用(reference),跟之前的setValue()不同。一个引用是指向一个已经存在的内存位置(global变量)的东西,因此它是一个左值,所以它能被赋值。注意这里的&:它不是取地址操作符,他定义了返回的类型(一个引用)。
可以从函数返回左值看上去有些隐晦,它在你做一些进阶的编程例如实现一些操作符的重载(implementing overload operators)时会很有作用,这些知识会在未来的章节中讲述。

四、左值到右值的转换

一个左值可以被转换(convert)为右值,这完全合法且经常发生。让我们先用+操作符作为一个例子,根据C++的规范(specification),它使用两个右值作为参数并返回一个右值(译者按:可以将操作符理解为一个函数)。
让我们看下面的代码片段:

int x = 1;
int y = 3;
int z = x + y;   // ok

等一下,xy是左值,但是加法操作符需要右值作为参数:发生了什么?答案很简单:xy经历了一个隐式(implicit)的左值到右值(lvalue-to-rvalue)的转换。许多其他的操作符也有同样的转换——减法、加法、除法等等。

五、左值引用

相反呢?一个右值可以被转化为左值吗?不可以,它不是技术所限,而是C++编程语言就是那样设计的。
在C++中,当你做这样的事:

int y = 10;
int& yref = y;
yref++;        // y is now 11

这里将yref声明为类型int&:一个对y的引用,它被称作左值引用(lvalue reference)。现在你可以开心地通过该引用改变y的值了。
我们知道,一个引用必须只想一个具体的内存位置中的一个已经存在的对象,即一个左值。这里y确实存在,所以代码运行完美。
现在,如果我缩短整个过程,尝试将10直接赋值给我的引用,并且没有任何对象持有该引用,将会发生什么?

int& yref = 10;  // will it work?

在右边我们有一个临时值,一个需要被存储在一个左值中的右值。在左边我们有一个引用(一个左值),他应该指向一个已经存在的对象。但是10 是一个数字常量(numeric constant),也就是一个左值,将它赋给引用与引用所表述的精神冲突。
如果你仔细想想,那就是被禁止的从右值到左值的转换。一个volitile的数字常量(右值)如果想要被引用,需要先变成一个左值。如果那被允许,你就可以通过它的引用来改变数字常量的值。相当没有意义,不是吗?更重要的是,一旦这些值不再存在这些引用该指向哪里呢?
下面的代码片段同样会发生错误,原因跟刚才的一样:

void fnc(int& x)
{
}

int main()
{
    fnc(10);  // Nope!
    // This works instead:
    // int x = 10;
    // fnc(x);
}

我将一个临时值10传入了一个需要引用作为参数的函数中,产生了将右值转换为左值的错误。这里有一个解决方法(workaround),创造一个临时的变量来存储右值,然后将变量传入函数中(就像注释中写的那样)。将一个数字传入一个函数确实不太方便。

六、常量左值引用

先看看GCC对于之前两个代码片段给出的错误提示:

error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’

GCC认为引用不是const的,即一个常量。根据C++规范,你可以将一个const的左值绑定到一个右值上,所以下面的代码可以成功运行:

const int& ref = 10;  // OK!

当然,下面的也是:

void fnc(const int& x)
{
}

int main()
{
    fnc(10);  // OK!
}

背后的道理是相当直接的,字面常量10volatile的并且会很快失效(expire),所以给他一个引用是没什么意义的。如果我们让引用本身变成常量引用,那样的话该引用指向的值就不能被改变了。现在右值被修改的问题被很好地解决了。同样,这不是一个技术限制,而是C ++人员为避免愚蠢麻烦所作的选择。
应用:C++中经常通过常量引用来将值传入函数中,这避免了不必要的临时对象的创建和拷贝。
编译器会为你创建一个隐藏的变量(即一个左值)来存储初始的字面常量,然后将隐藏的变量绑定到你的引用上去。那跟我之前的一组代码片段中手动完成的是一码事,例如:

// the following...
const int& ref = 10;

// ... would translate to:
int __internal_unique_name = 10;
const int& ref = __internal_unique_name;

现在你的引用指向了真实存在的事物(知道它走出作用域外)并且你可以正常使用它,除了不能改变他指向的值。

const int& ref = 10;
std::cout << ref << "\n";   // OK!
std::cout << ++ref << "\n"; // error: increment of read-only reference ‘ref’

 

七、C++11中的右值引用

右值引用及其相关的move语义是C++11新引入的最强大的特性之一。前文说到,左值(非const)可以被修改(赋值),但右值不能。但C++11引入的右值引用特性,打破了这个限制,允许我们获取右值的引用,并修改之。让我们先看点代码:
定义一个类Intvec及其赋值操作符重载函数如下:

class Intvec
{
public:
    ...
    Intvec& operator=(const Intvec& other)
    {
        log("copy assignment operator");
        Intvec tmp(other);  //构造一个临时对象,因为other为const,不能被修改
        std::swap(m_size, tmp.m_size);
        std::swap(m_data, tmp.m_data);  
        //跟临时对象交换值,临时对象晰构时会delete [] m_data
        return *this;
    }
private:
    size_t m_size;  
    int* m_data;     //存放int数组,构造时动态分配
};  

代码要点:

  1. 代码使用了copy-swap策略,即先分配资源再更改自身状态,这样可以保证当资源分配失败的时候,自身能够维持原先状态,《高效C++》有条规则描述这个主题。所以先根据other拷贝构造一个临时对象tmp, 然后与tmp进行swap,m_data交换给了tmp之后,也会随着tmp的晰构而被释放。

  2. 之所以把other声明为const,有两个理由,其一是赋值操作不应该更改other,其二是可以传入一个右值。其实这样的声明随处可见。

假设现有类型为Intvec的对象v,用一个新对象给它赋值:

v = Intvec(33);

这句代码合法,它构造一个临时对象,为右值,传入到Intvec的赋值运算符重载函数中。这个代码是可以工作,而且通常情况下都比较高效。但是如果Intvec里包含某些m_handle成员,创建和释放m_handle比较昂贵,那么拷贝构造越少越好。这种情况,我们设想一下,如果v能跟Intvec(33)临时对象直接进行内部数据交换,而不需要在重载函数里使用Intvec tmp(other);构造一个新对象出来swap,那该有多好!
如你所料,C++11引入的“右值引用”和“move语义”就可以实现这个目标,新的语法很简单,我们重载一个新的赋值操作运算符函数:

Intvec& operator=(Intvec&& other)
{
    log("move assignment operator");
    std::swap(m_size, other.m_size);
    std::swap(m_data, other.m_data);
    return *this;
}

对于v = Intvec(33);这种写法就会调用此版本的重载函数(即传入一个右值)。
&&语法声明右值引用,表示一个指向右值的引用,通过这个引用,可以修改右值。

参考链接:https://www.jianshu.com/p/94b0221f64a5

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/126251.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • pycharm断点调试教程_pycharm怎么debug

    pycharm断点调试教程_pycharm怎么debug前言如果你不会用IDE开发工具的debug,你在调试代码的时候可能会用print输出去调试,那样效率比较低。我们可以用Pycharm的debug来调试,当然如果你用的Jetbranis的其他产品,操作方法也是一样的。Pycharm的Debug(1)开启debug的方式:右键debug项目 工具栏的甲壳虫(2)常用按钮图解debugger栏:stepover(单步调试)程序代码越过子函数,但子函数会执行,且不进入。 stepinto(进入)在单步执行时,遇到子函数就进入.

    2022年8月26日
    6
  • Mybatis模糊查询的四种方式

    Mybatis模糊查询的四种方式Mybatis 模糊查询的四种方式 1 根据姓名模糊查询员工信息 1 1 方式一步骤一 编写配置文件步骤二 测试步骤三 分析此种方式需要在调用处手动的去添加 通配符 1 2 方式二说明 使用方式一可以实现模糊查询 但是有一点不方便的地方就是 在测试类中 调用 selectList 方法传参时需要调用者手动的添加 号通配符 显然是麻烦的 能否在映射配置文件中直接将 号写好呢 有的朋友可能会这么想 好办 直接在配置文件中这么写 形如 1 测试后发现 程序会报错 原因是 缺少单引号 这个

    2025年7月24日
    0
  • ABAP WDA

    ABAP WDA一、20181217-20181226笔记selection_options和alv 二、相关服务1、事务码:SICF默认SERVICE,执行。Service:default_host/sap/option/*default_host/sap/public/bc/*default_host/sap/bc/wdvddefault_host/sap/bc/webdynp…

    2022年7月12日
    17
  • 直播界的新玩法:你又套路用户!只要钱到位,榜单全干碎

    直播界的新玩法:你又套路用户!只要钱到位,榜单全干碎今天早上好心市民王先生(公众号:hxsmwxs)在翻看AppStore榜单的时候,发现今天凌晨(25号0:00分)榜单更新后有三款应用刷榜,乍一看是两款游戏,一款应用,但好心市民王先生(公众号:hxsmwxs)在下载之后发现了其中一款的秘密,就是下面这款【战舰世界】看看它的排名变化,真是舍得花钱啊下面我们一步步的分析这款应用所有用户点开从名称到截图乍一看就是一款游戏,但从描述中不难发现,他就是一…

    2022年6月5日
    57
  • idea配置svn仓库

    idea配置svn仓库IntelliJIDEA使用教程(总目录篇)首先,使用的时候,自己得先在电脑上安装个小乌龟。也就是svn啦。第一步安装小乌龟。如下:具体安装好像没什么具体要求,一路next,就好。如上图箭头所示,在安装TortoiseSVN的时候,默认commandlineclienttools,是不安装的,这里建议勾选上。这个我不确定我当时选没选,不过呢,你给安装上,也是没问题的。把上面的勾选取…

    2022年5月14日
    103
  • 通过bindservice方法开启的服务,通过什么方法解绑_controller调用多个service

    通过bindservice方法开启的服务,通过什么方法解绑_controller调用多个service绑定本地服务AndroidManifest.xml中声明服务:&lt;serviceandroid:name=".TestLocalService"&gt;&lt;intent-filter&gt;&lt;actionandroid:name="maureen.intent.action.BIND_LOCAL…

    2022年9月18日
    1

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号