C++ | 左值、右值、将亡值和引用的概念 | 聊聊我对它们的深入理解

news2024/11/24 0:25:01

文章目录

    • 前言
    • 左右值的辨析
      • 一个特殊的问题
      • 将亡值
    • 引用的深刻理解

前言

这篇文章是我在探究完美转发这个语法点时,引发的相关问题思考,为了使自己的理解更深刻,故写下这篇博客

左右值的辨析

首先需要明白两个概念:类型(type)和值类别(value category),看似差不多的两个概念其实毫不相干。类型指的是数据类型,int,char这样的内置类型,类型主要是用来区别它们的字节大小。除了内置类型还有自定义类型,自定义类型中的类型还表征了结构,像C语言的结构体,由于结构(或者说内置类型的顺序)的不同引发的内存对齐问题。所以类型表征的是大小,结构,表征这个数据是怎样的(how?)

而值类别呢,就是关于变量的左右值属性,先说结论,我认为值类别表征了数据的存储位置(where?),左右值也是第一个需要辨析的重要概念。在之前写的博客中,我说可以通过是否能取地址判断左右值。如果能取地址,说明这个变量是左值,我们可以通过地址修改它,如果不能取地址,则变量是右值,我们不能通过地址修改它。比如int num = 10;这行代码,将10存储到变量num中,num对应一个地址,后续可以通过地址修改num的值,所以我们称num为左值(num在表达式的左边,这是左的含义),表达式右边的10就是一个右值,我们无法通过10的地址修改10。

以上的分析是从高级语言的角度展开的,之前我只能做到这样的理解,但是现在我们可以从一个更高的角度理解左右值,从计算机体系结构的角度理解int num = 10;这行代码,在语言层面,它表示创建一个int类型的变量num,并初始化为10。但跳出高级语言,全新的理解是:num才不是什么变量名,num对应了一个地址,是位于进程地址空间上的栈区的地址,int也不是什么类型,int表示从该地址往后8字节的空间被进程使用了,要以8字节为一个整体,修改该地址上的内容。而10呢?它是一个字面常量,用二进制表示为00001010,根据赋值对象的不同再进行提升或截断,比如10要赋值给int对象,所以被提升为00000000 … 00001010(前面多出7个全0的序列,每个序列有8个0),存储时再根据大小端字节序将这些字面值从代码区拷贝到刚才的栈区地址上。

继续分析,这行代码被编译后会被放到进程地址空间的正文代码区,系统怎么知道你要用10初始化num?因为正文代码区的存储了10的二进制序列,代码区中还有10要放入的地址(没有什么num,只有一串地址),以及把10放到地址上的指令,这些信息都会在代码中表示。并且,程序的正文代码区也是有地址的,代码区存储系统要执行的指令。现在回头看右值的概念,不能取地址的就是右值,10有地址吗?当然有,没有地址系统怎么访问正文代码区,怎么知道你要初始化的值是10,所以不能取地址不是因为没有地址,而是因为这个地址你不能知道,地址位于只读数据区(代码区的数据可不能随便修改,当然是只读数据区)或者说该地址上的数据只有在程序运行后才会被系统读取,你要取地址,编译器直接出手,谁能保证你不会做一些危害系统安全的事,编译器可不会给你这些地址,于是程序编译失败。

所以你看,直接创建的局部变量,全局变量,new出来的变量都是左值,为什么?就是因为栈区,堆区,静态区都是系统允许你访问的区域,我们对这些区域拥有写入的权限,所以系统可以给你它们的地址。但是像什么字面常量,临时变量(隐式类型转换表达式产生的中间值,函数返回产生的中间值…),匿名对象就是右值,因为程序编译后,它们位于代码区或者你没有修改这些数据的必要,所以系统才不会把地址给你,这些空间就像系统的私人空间,你不能随便的访问,只有在程序运行后,为了运行程序,系统才会访问这些空间

最后总结一下,不能取地址就是右值的说法有些不准确,或者说我不太认同这种说法,我认为只要数据位于的区域你没有权限访问,这些数据就是右值,你有权限访问的区域,存储的数据是左值。并且这个权限不是语言限制的,而是系统限制的访问权限,语言位于系统之上,我们可以突破语言的限制,但是底层系统的限制我们无法突破,也不能突破

一个特殊的问题

字符串字面值是左值(?)

如果以是否能取地址作为左右值判断的标准,那么字符串字面值确实左值
在这里插入图片描述
比如"abc"这个字符串,我们写一段程序,输出它的地址,make编译这份源文件,结果是可以编过的,再运行可执行文件,地址也被正常的打印出来。但是这个地址是什么类型的呢?由于g++编译器的typeid打印结果不好观察,这里我使用vs的编译环境,使用typeid打印"abc"字符串的类型与其地址的类型
在这里插入图片描述
可以看到字符串字面值的类型是const修饰的char数组,大小为4(最后有个’\0’),这里又涉及到const修饰值的问题,先不管它。回到最开始的问题,以是否能修改作为判断标准,字符串字面值还是左值吗?我们先看一下字符串字面值位于地址空间的哪个区域
在这里插入图片描述

这是进程地址空间的划分,下面是低地址,上面是高地址,我们可以通过打印初始化全局数据区变量的地址,栈区变量以及堆区变量的地址,判断"abc"这个字符串是存储在哪块区域的

#include <iostream>
using namespace std;
int c = 10;

int main()
{
    int a = 10;
    int* b = new int(10);
    cout << "栈区变量的地址      :" << &a << endl;
    cout << "堆区变量的地址      :" << b << endl;
    cout << "初始化全局变量的地址:" << &c << endl;
    cout << "字符串字面值的地址  :" << &("abc") << endl;

    return 0;
}

在这里插入图片描述
结果很明显,栈区向下增长,地址最高,堆区向上增长,地址次高,初始化全局数据区的地址在两者之下,而字符串字面值的地址比初始化全局数据区还低,通过进程地址空间的划分,我们可以得知字符串字面值被存储在正文代码区。因此,程序被编译为可执行文件后,"abc"这个字符串被存储在了正文代码区。与字符串数组和通常的字面常量不同,字符串数组在程序运行之后才被存储到栈区或者堆区中(从代码区中拷贝到其他可修改的区域),虽然通常的字面常量在程序被编译好后就被存储在了正文代码区,但是它没有表征具体信息的字段,比如数据的类型,有几个字节,但是字符串字面值是有的,代码区中有信息表示它的类型,大小,所以我们可以根据这些信息使程序打印出字符串字面值的地址的类型。

但是我们可以通过这个地址修改字符串字面值的值吗?我想的是,虽然可以通过强制类型转换去除变量的const属性,但是字符串字面值存储在正文区,正文区的数据肯定不能修改,所以我认为字符串字面值是右值,但是我一搜索“修改字符串常量”就被这篇文章打脸,仔细一看,文章并不简单,其中的修改方法是从系统角度修改页的权限,得到代码区的写入权限,所以可以修改字符串字面值。如果从系统的角度出发,我们可以直接修改页的权限,获取可读数据区的写权限,那么所有的数据都是可写的,所有的数据都是左值?显然我们不能这样理解,我们应该从高级语言的角度上理解,代码区的数据就是不可修改的,我们对代码区只有读权限,因此字符串字面值是右值。这个结论与网上的大多数结论相反,究其原因,只是我对左右值的判断依据与大部分人不同,我认为不能简单的将左右值用是否能取地址来区分,这只是方便初学者理解的一种说法,学习到现在,我认为区分左右值的依据应该是是否能在语言层面上修改数据,能修改的数据就是左值,不能修改的数据就是右值,而是否能修改的本质是我们对地址空间的权限,对正文代码区只有读权限,对栈区,堆区以及静态区我们有读写权限,无论语言怎么限制(这里点名const),我们都能通过一些特殊手段,突破这个限制,绕过编译器的检查,非法的篡改被语言级别限制的数据。比如函数的返回值,虽然返回值是一个临时变量,具有常属性(这是语言级别的限制),但是它还是存储在栈区,我们当然可以非法篡改,具体可以看函数栈帧理解这篇文章。

将亡值

在这里插入图片描述
有意思的是,由于C++11引入了右值引用,将亡值这一概念随之被提出,我们探讨的对象又复杂了起来。刚才我所说的左值与右值对应着图片上的lvalue(传统意义上的左值)和rvalue(传统意义上的右值),rvalue中的r除了right的意思,还可以理解为read,表示只读。而rvalue包括了将亡值xvalue和纯右值prvalue,由于将亡值的出现,我们需要将这些概念重新梳理一遍

glvalue(泛左值)= lvalue(传统意义上的左值)+ xvalue(将亡值)
rvalue(传统意义上的右值)= prvalue(纯右值)+ xvalue(将亡值)

我们通常讨论的左值并不是gvalue(泛左值),而是lvalue,通常讨论的右值是rvalue,它包含了将亡值xvalue和纯右值pvalue,其中的将亡值与右值引用息息相关,匿名对象和函数返回值都是将亡值,它们都具有常属性,并且生命周期较短,在下一条语句执行前资源就会被释放。具体的比如隐式类型转换产生的中间变量,为了调用类的函数而定义的匿名对象,这些变量似乎都是工具人,被创建只是为了其他语句的成功执行。可以预见的是这些将亡值都不是字面常量,而是程序运行后,在栈上,堆上创建的变量,虽然这些变量都有地址,但是我们没有必要知道这些地址,因为它们都是程序运行中产生的中间值,被创建只是为了完成其他代码,当代码执行完,将亡值就会被释放,它默默地来,也默默地走,编译器甚至不让我们知道它们的“姓名”。根据将亡值存储在可修改数据区这一条件,我们能得到将亡值是一个左值的结论,我们可以通过一些特殊手段修改将亡值,虽然编译器为将亡值添加了限制,我们无法修改将亡值(将亡值的生命周期太短了,一行代码执行完就被释放,所以要修改它的值也是比较困难的,但是将亡值存储在可修改的数据区上,理论上我们是可以修改的,就像我的文章中对函数返回值的篡改一样)。

高级语言中,这样的中间值肯定是不允许使用者修改的,为了程序的正确运行,我们也没有修改的必要,所以语言对这样的中间值加了限制,我们无法修改它们的值,一个典型的例子,int &x = 1.1;这行代码肯定是无法通过编译的,分析一下,假设现在的代码是int x = 1.1; 将一个浮点数赋值给一个整形,两者的数据存储规则都不一样,肯定是无法直接赋值的,这里要发生隐式类型转换,将1.1这个浮点数转换成int类型的数据,用一个中间变量接收转化的结果,最后再把中间变量的值赋给x,这个过程中谁是工具人已经很明显了。这就是将亡值的产生原因,如果代码是int &x = 1.1;呢?这个x只是一个引用,并不是一个变量,所以无法接收数据,而最后引用是1.1的引用吗?x的类型是int类型的引用,int类型的引用肯定无法引用浮点数,所以x是中间变量的引用,但是我们可以访问中间变量吗?不行,你想访问。编译器直接出手,这就是语法的规则限制:无法访问将亡值

但是C++这门语言非常自由,我们可以直接访问内存啊,只要知道中间变量在内存中的地址,我们就可以修改,哪管什么编译器的限制,编译器只会限制明显的修改将亡值的情况,我们不通过变量直接修改将亡值就可以绕过编译器的检查。具体可以看我对函数返回值的篡改这篇文章,C++允许你做很多细致的操作,只要你遵守它的规则,就能用C++做很多事,它会非常好用。但是自由也是有代价的,规则的限制只能限制那些本就会遵守规则的人,无法限制那些无视规则的人。
在这里插入图片描述
现在,我想这张图也能解释清楚了,为什么将亡值即属于glvalue(泛左值),还属于rvalue(传统意义上的右值),这两个看似矛盾的归类,其实就是由自由导致的,C++成也自由,败也自由,如果你遵守C++的规则,那么将亡值就是右值,语言限制你不能去修改它,一些直接修改的操作不被编译器允许,即使存储将亡值的数据区是可写的,但是遵守规则导致的就是将亡值的无法修改,将其视为右值也是有理有据的。但是你不遵守C++的规则,你就可以修改将亡值,谁让将亡值存储在了可写的数据区,将其视为左值也是没问题的。综上,我们可以认为由于C++这门语言的自由,诞生了即是左值又是右值的矛盾的值类别:将亡值。

而纯右值prvalue就是字面常量,1,‘a’,这样的数据,它们被编译器编译后就是一些二进制数据,被嵌入到代码区中,作为代码的一部分,我们无法在语言层面上修改代码区的数据。lvalue(传统意义上的左值)呢,就是我们经常定义的变量,有名字的变量,就算被const修饰,我们依然可以修改它们的值,谁让它们存储在可写的数据区呢?但是除了这两个典型的左右值,还有一些工具人,它们被叫做将亡值,至于它的值类别是属于左值还是右值,我们无法做出具体的分类界定,它的左右值属性将由使用者决定

引用的深刻理解

(int &x = 1.1;这行代码涉及到引用,所以再补充一下我对引用的理解)

相较于C语言,C++引入了一种语法:引用,这篇文章不谈引用的基本使用,我们需要深刻的理解为什么C语言没有引用,而C++有呢?因为它比指针使用方便,不需要写&和*吗?确实这是一个方面,但是这只是引用的一种语法表现,并不是引用的出现原因。在Linux文件系统中,最顶层的文件对应着底层结构中的一个inode文件,inode作为文件的唯一标识符,一份文件只有一个inode编号,但是可以有很多顶层文件使用这个inode编号,用不同的文件名映射相同的inode,这就是硬链接。还有网络中的进程与端口号之间的关系,通过端口号肯定能找到一个进程,并且只能找到一个进程,就像文件名一样,不同的端口号可以指向同一个进程,进程就是底层唯一的结构,不管上层怎么变,进程只有一个,而端口号随便几个。端口号和文件名就有了一些解耦的意思,用户不能(不用)直接接触底层的结构,而是接触较高层的一些结构,不仅降低使用成本,还减少了用户直接接触底层结构会带来的风险。这样的加一层在软件设计中非常的常见,说一个我个人的观点,我认为C++的引用也有点这样的意思,语言的设计者鼓励我们多使用引用,而少使用或者不使用指针,就是为了减少使用者对底层结构(地址)的直接接触,将使用者与地址解耦,减少直接接触地址可能存在的风险。当然,这只是左值引用的理解,还有一个右值引用。通过左值引用我们知道,可以通过上层的结构(引用)接触底层结构(地址)上的数据,而右值包括了纯右值和将亡值,对于纯右值,由于其存储在代码区,我们不能修改它的地址,所以右值引用会对被引用的右值做一个拷贝,将数据拷贝到可写数据区中,用户对右值引用的修改变成了对一份拷贝的修改(右值引用是可以修改的,不了解的读者可以写简单的代码验证一下),毕竟不能修改代码区上的数据是系统规定的,语言不能脱离系统设计啊。而对于将亡值的右值引用,就涉及到移动构造和移动拷贝的问题,这是C++11带来的一块语法糖,如果后续代码还要使用将亡值所拥有的资源,我们可以用右值引用作为函数形参,定义一个移动构造或者移动赋值函数,将工具人的资源转移到自己的左值上,这里心疼将亡值1秒钟,由于移动构造和移动赋值不是我们的重点,我只是简单的提一下。

所以引用就是别名,不是变量的别名,而是地址的别名。我们通过不同的别名访问的都是同一块地址空间,并且由于引用的书写比指针简单,在C++中能使用引用就不使用指针了,使用者不直接接触地址,程序出错的可能也就小了,C++设计者的心思已经被我们狠狠拿捏,毕竟引用的理解和书写比起指针真的太简单了,初学者为什么要在地址中绕来绕去,直接用引用代替指针,学习成本不就减低了吗?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/158266.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

1577_AURIX_TC275_MTU中检测控制相关寄存器

全部学习汇总&#xff1a; GreyZhang/g_TC275: happy hacking for TC275! (github.com) 开篇介绍的功能室之前看过很多次的一个握手的功能。快速行以及快速列模式的测试中&#xff0c;这个行列其实是对应的存储的bit阵列信息。一个对应相应的字&#xff0c;另一个则对应bit序列…

【Linux】进程创建、终止、等待、替换、shell派生子进程的理解…

柴犬&#xff1a; 你好啊&#xff0c;屏幕前的大帅哥or大美女&#xff0c;和我一起享受美好的今天叭&#x1f603;&#x1f603;&#x1f603; 文章目录一、进程创建1.调用fork之后&#xff0c;内核都做了什么&#xff1f;2.如何理解fork函数有两个返回值&#xff1f;3.如何理…

(短信服务)java SpringBoot 阿里云短信功能实现发送手机验证码

一.阿里云准备工作 1.阿里云短信服务-注册账号 阿里云官网: https://www.aliyun.com/ 点击官网首页注册按钮。 2.阿里云短信服务-设置短信签名&#xff08;阿里云提供测试的签名&#xff0c;暂时可以跳过&#xff09; 注册成功后&#xff0c;点击登录按钮进行登录。登录后…

简单方式调用WebService服务

好久没有进行过WebService开发了&#xff0c;由于项目需要&#xff0c;重拾WebService&#xff0c;记录一下简单的服务调用方法。拿到需求&#xff0c;仅半页word&#xff0c;其他的就没有了&#xff0c;为了快速开发&#xff0c;尝试过使用插件逆向生成调用的一大堆类&#xf…

AWVS安装与激活

AWVS安装与激活 1.AWVS简介 AWVS&#xff08;Acunetix Web Vulnerability Scanner&#xff09;是一款知名的网络漏洞扫描工具&#xff0c;通过网络爬虫测试网站安全&#xff0c;检测流行的Web应用攻击&#xff0c;如跨站脚本、sql 注入等。据统计&#xff0c;75% 的互联网攻击…

pmp备考全攻略

我这里分享一下我备考的经验&#xff0c;如何对大家有帮助也可以稍微给点支持&#xff0c;让更多人了解&#xff01; 一&#xff0c;我的pmp备考经验 1.一阶段&#xff1a;铺底&#xff0c;花费时间1.5周左右 主要是熟悉考试框架和内容&#xff0c;通过看网盘资料里的章节重…

vue3+ts实现自定义按钮导航

效果图 点击对应按钮&#xff0c;相应按钮被激活&#xff0c;背景平移至激活按钮&#xff0c;字体高亮&#xff0c;其余按钮重置&#xff0c;由于ele没有类似tab&#xff0c;就简单记录下。 实现 <template><div class"tab_wrapper"><spanv-for&q…

这些技巧你值得学会

技巧一&#xff1a;多图合并为PDF文件 处理合并多份PDF文件外&#xff0c;使用PS的【PDF演示文稿】工具&#xff0c;也能一同将多张图片合并成PDF文档&#xff01;通过合并的方式&#xff0c;不但能够批量归纳汇总图片&#xff0c;而且还能根据自身需求&#xff0c;将图片与PD…

Visual studio C++程序内使用Sqlite3

Visual studio C程序内使用Sqlite3 前言 本篇讲解了如何在Visual studio开发的C桌面应用程序内使用Sqlite数据库&#xff0c;Sqlite的语法和Mysql是一样的&#xff0c;所以本篇文章不对数据库语法做过多介绍&#xff0c;介绍一些常用Sqlite的API ★提高阅读体验★ &…

二叉树常见题目

目录 一、判断一棵树是否为另一棵树的子树 二、判断是否对称二叉树 三、翻转二叉树 四、二叉树构建及遍历 五、根据二叉树创建字符串 六、二叉树的最近公共祖先 七、根据前序遍历和中序遍历构造二叉树 八、根据后序遍历和中序遍历构造二叉树 九、二叉树前序非递归…

MySQL——“order by”是如何工作的

假设目前有这么一个表 CREATE TABLE t (id int(11) NOT NULL,city varchar(16) NOT NULL,name varchar(16) NOT NULL,age int(11) NOT NULL,addr varchar(128) DEFAULT NULL,PRIMARY KEY (id),KEY city (city) ) ENGINEInnoDB; 业务要求是要查询城市是“杭州”的所有人名字&a…

1.2计算机系统的层次结构

文章目录&#xff08;1&#xff09;微指令&#xff08;2&#xff09;汇编语言&#xff08;3&#xff09;高级语言&#xff08;4&#xff09;操作系统&#xff08;5&#xff09;编译程序与解释程序&#xff08;6&#xff09;总结请先食用这一篇 计算机工作过程&#xff08;1&…

考研《数据结构》线性表—顺序表练习题

2.设计一个高效算法&#xff0c;将顺序表L的所有元素逆置&#xff0c;要求算法的空间复杂度为0&#xff08;1&#xff09;。 思路&#xff1a;扫描顺序表L的前半部分元素&#xff0c;对于元素L.data[i]与L.data[len-1-i]对换。 #include<iostream> using namespace std…

XCTF:NewsCenter

一道简单的常规注入题&#xff0c;就当练练手了 尝试’ 直接网页异常&#xff0c;尝试进行闭合# 网页恢复正常&#xff0c;证明SQL语句通过单引号进行闭合&#xff0c;则为字符型注入 直接判断字段数&#xff0c;order by n order by 3#回显正常&#xff0c;order by 4#网页异…

kaggle平台学习复习笔记 | 数据划分与模型集成

目录数据集划分与交叉验证模型集成方法Titanic为例的简单应用kaggle比赛相关tips数据集划分与交叉验证 数据集划分 通常有两种方法&#xff1a; 留出法(Hold-out) 适用于数据量大的情况K折交叉验证(K-fold CV) 适用于数据量一般情况 时间比较长自助采样(Bootstrap) 较少使用 …

Lua C接口编程(二)

引言 上篇文章我们学习了C如何调用Lua&#xff0c;今天我们就来聊聊Lua 如何调用C。 Lua版本&#xff1a;Lua 5.3.5 对于Lua提供的接口有不清楚的&#xff0c;可以参考Lua接口官方文档 一、Lua调用C步骤 需要将C文件编译成动态库在Lua文件中使用package.cpath配置C动态库路…

Linux学习笔记——分布式内存计算Spark安装部署

5.12、分布式内存计算Spark环境部署 5.12.1、简介 Spark是一款分布式内存计算引擎&#xff0c;可以支撑海量数据的分布式计算。 Spark在大数据体系是明星产品&#xff0c;作为最新一代的总和计算引擎&#xff0c;支持离线计算和实时计算。 在大数据领域广泛应用&#xff0c…

虚拟化技术考试重点总结

虚拟化技术考试重点总结 什么是虚拟化&#xff1f;其作用是什么 ​ 虚拟化&#xff0c;是指通过虚拟化技术将一台计算机虚拟为多台逻辑计算机。可以在一台计算机上同时运行多个逻辑计算机&#xff0c;每个逻辑计算机可运行不同的操作系统&#xff0c;并且应用程序都可以在相互…

Golang中http编程

http介绍 编写web语言&#xff1a; 1.java 2.php&#xff0c;现在都在尝试用go语言编写 3.python&#xff0c;豆瓣 4.go语言 》 beego&#xff0c;gin两个主流的web框架 https协议&#xff1a;我们使用浏览器访问的时候发送的就是http请求 http是应用层的协议&#xff0c;底…

论文投稿指南——中文核心期刊推荐(地质学)

【前言】 &#x1f680; 想发论文怎么办&#xff1f;手把手教你论文如何投稿&#xff01;那么&#xff0c;首先要搞懂投稿目标——论文期刊 &#x1f384; 在期刊论文的分布中&#xff0c;存在一种普遍现象&#xff1a;即对于某一特定的学科或专业来说&#xff0c;少数期刊所含…