C++ 引用的本质_c++中引用的作用是什么

C++ 引用的本质_c++中引用的作用是什么引用是C++引入的重要机制,它使原来在C中必须用指针实现的功能有了另一种实现的选择,在书写形式上更为简洁。那么引用的本质是什么,它与指针又有什么关系呢?

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全家桶1年46,售后保障稳定

代码运行环境:Windows7 32bits+VS2017+Win32。

引用是 C++ 引入的重要特性,它使原来在 C 中必须用指针实现的功能有了另一种实现的选择,在书写形式上更为简洁。那么引用的本质是什么,它与指针又有什么关系呢?

1.引用的底层实现方式

引用被称为变量的别名,它不能脱离被引用对象独立存在,这是在高级语言层面的概念和理解,并未揭示引用的实现方式。常见错误说法是“引用“自身不是一个变量,甚至编译器可以不为引用分配空间。

实际上,引用本身是一个变量,只不过这个变量的定义和使用与普通变量有显著的不同。为了解引用变量底层实现机制,考查如下代码:

int i = 5;
int &ri = i;
ri = 8;

Jetbrains全家桶1年46,售后保障稳定

在Visual Studio 2017环境的debug模式调试代码,反汇编查看源码对应的汇编代码的步骤是:调试->窗口->反汇编,即可得到如下原码对应的汇编代码:

int i=5;
00A013DE  mov        dword ptr [i],5    	//将文字常量5送入变量i
int &ri=i;
00A013E5  lea        eax,[i]  	 	    	//将变量i的地址送入寄存器eax
00A013E8  mov        dword ptr [ri],eax  	//将寄存器的内容(也就是变量i的地址)送入变量ri
ri=8;
00A013EB  mov        eax,dword ptr [ri]  	//将变量ri的值送入寄存器eax
00A013EE  mov        dword ptr [eax],8   	//将数值8送入以eax的内容为地址的单元中
return 0;
00A013F4  xor        eax,eax

在汇编代码中,ri 的数据类型为 dword。也就是说,ri 要在内存中占据 4 个字节的位置。所以,ri 的确是一个变量,它存放的是被引用对象的地址。

通常情况下,地址是由指针变量存放的,那么指针变量和引用变量有什么区别呢?

使用指针常量实现上面的代码功能。考查如下代码:

int i = 5;
int* const pi = &i;
*pi = 8;

按照相同的方式,在 VS2017 中得到如下汇编代码:

int i=5;
011F13DE  mov         dword ptr [i],5  
int * const pi=&i;
011F13E5  lea         eax,[i]  
011F13E8  mov         dword ptr [pi],eax  
*pi=8;
011F13EB  mov         eax,dword ptr [pi]  
011F13EE  mov         dword ptr [eax],8  

观察以上代码可以看出:
(1)只要将pi换成ri,所得汇编代码与第一段所对应的汇编代码完全一样。所以,引用变量在功能上等于一个指针常量,即一旦指向某一个单元就不能在指向别处。
(2)在底层,引用变量由指针按照指针常量的方式实现。

2.高级语言层面引用与指针常量的关系

(1)在内存中都是占用4个字节(32bits系统中)的存储空间,存放的都是被引用对象的地址,都必须在定义的同时进行初始化。

(2)指针常量本身(以p为例)允许寻址,即&p返回指针常量(常变量)本身的地址,被引用对象用*p表示;引用变量本身(以r为例)不允许寻址,&r返回的是被引用对象的地址,而不是变量r的地址(r的地址由编译器掌握,程序员无法直接对它进行存取),被引用对象直接用r表示。

(3)凡是使用了引用变量的代码,都可以转换成使用指针常量的对应形式的代码,只不过书写形式上要繁琐一些。反过来,由于对引用变量使用方式上的限制,使用指针常量能够实现的功能,却不一定能够用引用来实现。

例如,下面的代码是合法的:

int i=5, j=6;
int* const array[]={ 
   &i,&j};

而如下代码是非法的:

int i=5, j=6;
int& array[]={ 
   i,j};

也就是说,数组元素允许是指针常量,却不允许是引用。C++语言机制如此规定,原因是避免C++语法变得过于晦涩。假如定义一个“引用的数组”,那么array[0]=8;这条语句该如何理解?是将数组元素array[0]本身的值变成8呢,还是将array[0]所引用的对象的值变成8呢?对于程序员来说,这种解释上的二义性对正确编程是一种严重的威胁,毕竟程序员在编写程序的时候,不可能每次使用数组时都要回过头去检查数组的原始定义。

3.改变引用指别的对象

C++ 规定,引用变量在定义的时候就必须初始化,也即是将引用变量与被引用对象进行绑定。而这种引用关系一旦确定就不允许改变,直到引用变量结束其生命期。这种规定是在高级语言的层面上,由 C++ 编译器所做的检查来保障实施的。在特定的环境下,利用特殊的手段,还是可以在运行时动态地改变一个引用变量与被引用对象的对应关系,使引用变量指向一个别的对象。

#include <iostream>
using namespace std;

int main(int argc,char* argv[]) { 
   
	int i=5,j=6;
	int &r=i;
	void *pi,*pj;
	int* addr;
	int dis;

	pi=&i;    //取整型变量i的地址
	pj=&j;    //取整型变量j的地址
	dis=(int)pj-(int)pi;//计算连续两个整型变量的内存地址之间距离
	addr=(int*)((int)pj+dis);//计算引用变量r在内存中的地址

	cout<<"&i:"<<pi<<endl;
	cout<<"&j:"<<pj<<endl;
	cout<<"&pi:"<<&pi<<endl;
	cout<<"&pj:"<<&pj<<endl;
	cout<<"&addr:"<<&addr<<endl;
	cout<<"&dis:"<<&dis<<endl;
	cout<<"distance:"<<dis<<endl;
	
	(*addr)=(int)&j;    //将j的地址赋给引用r(此处把r看作指针)
	
	cout<<"addr:"<<addr<<endl;
	r=100;
	cout<<i<<" "<<j<<endl;
	return 0;
}

这个程序在 Debug 模式下输出结果如下:

&i:0038FC1C
&j:0038FC10
&pi:0038FBF8
&pj:0038FBEC
&addr:0038FBE0
&dis:0038FBD4
distance:-12
addr:0038FC04
5 100

仔细观察代码和输出结果可以得出如下结论:

(1)Win32(Windows 32bits)平台下,int 型变量和指针变量都占用 4 个字节,但是 &i-&j=-12 并非想象中的 4。
一是局部变量存储在栈空间,栈在主存中的生长方向是从高地址到低地址,因此i和j的地址差为负数;
二是 Debug 模式下,int 变量前后均添加 4 个字节的调试信息,故一个 int 占用了 12 字节。模式设为 Release,就会发现栈上连续定义的 int 变量,地址相差 4 个字节。

(2)指针变量 pi 与 int 变量 j 地址间相差了 24 字节,按照推理,如果引用r不占用内存空间,那么地址差应该为 12 字节,这也说明了引用变量在内存占用空间。

(3)将引用变量r理解成指针,间接的获取r的地址并修改 r 的值,使r指向变量 j。从引用的角度理解就是将引用 r 与 j 绑定。对 r 赋值,结果显示 j 的值被修改。

以上代码较为诡异,实际编程绝不提倡大家模仿。利用以上程序可以看出“引用“本身的确是一个变量,它存放被引用对象的地址。并且,利用特殊手段能够找到这个引用变量的地址并修改其自身在内存中的值,从而实现与其他对象的绑定。

这个程序在 VS 环境下的 Release 模式编译不通过,会出现内存访问冲突,无法通过引用变量 r 修改 j 的值,可能与 Release 模式下编译器对引用的优化有关。与此同时,该程序可移植性很差,在 64bits 平台上,由指针转换为 int 可能会发生截断从而丢失数据。其次,如果引用变量前的变量不是 int 型,考虑到内存对齐等因素,要准确计算引用变量的地址不是一件容易的事,很可能跟具体的编译器和运行环境相关。因此,研究此程序的目的是为了对引用变量的底层实现机制有所了解。在实际使用中,还是要遵循 C++ 语言对引用制定的规范。


参考文献

陈刚.C++高级进阶教程[M].武汉:武汉大学出版社,2008.
vs栈上连续定义的int变量,地址为什么相差12个字节

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

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/226870.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • dockerfile创建镜像的命令_什么是镜像

    dockerfile创建镜像的命令_什么是镜像Dockerfile创建完成后,可以使用dockerbuild命令根据Dockerfile构建一个镜像。1.首先准备好Dockerfile:2.执行构建命令:dockerbuild-tsecond:v1.0.注意最后有个点,代表使用当前路径的Dockerfile进行构建,-tsecond:v1.0取名并设定版本为v1.0。dockerbuild:用Dock…

  • 0-1多重背包(单调队列+多重背包)[通俗易懂]

    0-1多重背包(单调队列+多重背包)[通俗易懂]原题链接有 N 种物品和一个容量是 V 的背包。第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。输出最大价值。输入格式第一行两个整数,N,V (0<N≤1000, 0<V≤20000),用空格隔开,分别表示物品种数和背包容积。接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。输出格式输出一个整数,表示最大价值。数据范围0<N≤1

  • UML时序图知识

    UML时序图知识1.时序图(SequenceDiagrams)时序图描述对象之间消息的发送顺序,强调时间顺序。时序图是一个二维图,横轴表示对象,纵轴表示时间,消息在各对象之间横向传递,依照时间顺序纵向排列。用箭头表示消息、用竖虚线表示对象生命线。2.时序图的作用展示对象之间交互的顺序。将交互行为建模为消息传递,通过描述消息是如何在对象间发送和接收的来动态展示对象之间的交互;相对于其他UML图,时序图更强调交互的时间顺序;可以直观的描述并发进程。3.时序图组成元素角色(Actor)系统

  • pycahrm激活码【注册码】

    pycahrm激活码【注册码】,https://javaforall.cn/100143.html。详细ieda激活码不妨到全栈程序员必看教程网一起来了解一下吧!

  • 教授就是大学[通俗易懂]

    教授就是大学[通俗易懂]教授就是大学有一则广为流传的关于艾森豪威尔将军的故事。话说家喻户晓的二战英雄艾森豪威尔将军在1952年接受了哥伦比亚大学的聘请,担任这家著名常青藤大学的校长。上任伊始,将军在下属的陪同下巡视校园,会见校董会、行政人员和学生,最后参加了学校教授为他举行的欢迎大家。在一阵热烈的掌声之后,将军致辞。他首先谦恭地对有机会会见在场的全体哥伦比亚大学的“雇员”们表示万分的荣幸。这时,只见哥大德高望重的物理

  • Navicat_Premium激活码【2021最新】

    (Navicat_Premium激活码)2021最新分享一个能用的的激活码出来,希望能帮到需要激活的朋友。目前这个是能用的,但是用的人多了之后也会失效,会不定时更新的,大家持续关注此网站~https://javaforall.cn/100143.htmlIntelliJ2021最新激活注册码,破解教程可免费永久激活,亲测有效,上面是详细链接哦~1S…

发表回复

您的电子邮箱地址不会被公开。

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