再谈c++中的引用
在《从汇编看c++的引用和指针》一文中,虽然谈到了引用,但是只是为了将两者进行比较。这里将对引用做进一步的分析。
1 引用的实现方式
在介绍有关引用的c++书中,很多都说引用只是其引用变量的一个别名。我自己不是很喜欢这种解释,因为觉得这种解释会给人误解,好像引用和变量就是一回事,而且,书中也没有给出,为什么引用就是一个别名的原因,大都只是给出一个例子,证明引用可以改变传递给函数的实际参数的值,从而说明,用引用传参,和传值不是一回事。这里,我将介绍一下自己分析的引用实现方式。下面先来看c++源码:
int main() {
int i = ;
int j = ;
int& ri = i; //指向i的引用
int* pj = &j; //指向j的指针 //用指针和引用的方式分别对i和j加1
ri++;
(*pj)++;
}
下面来看一下main函数的汇编码:
int main() { push ebp
mov ebp, esp
sub esp, ; 00000010H ; 2 : int i = 1; mov DWORD PTR _i$[ebp], ;将1写入_i$[ebp]代表的内存,即给变量i赋值1 ; 3 : int j = 2; mov DWORD PTR _j$[ebp], ;将2写入_j$[ebp]代表的内存,即给变量j赋值2 ; 4 : int& ri = i; lea eax, DWORD PTR _i$[ebp];获得_i$[ebp]所带表的内存地址,写入寄存器eax
mov DWORD PTR _ri$[ebp], eax;将eax写入_ri$[ebp]所带表的内存,这里,完成了引用的初始化
;可以看到,虽然我们使用变量i来初始化引用,但是,编译器仍然保存的是变量i的地址 ; 5 : int* pj = &j; lea ecx, DWORD PTR _j$[ebp]; 将_j$[ebp]所代表的内存地址给寄存器ecx
mov DWORD PTR _pj$[ebp], ecx;将ecx里面的值写入_pj$[ebp]所代表的的内存,这里完成了指针变量的初试化,可以看到
;指针变量pj确实保存的是变量j的地址 ; 6 :
; 7 : ri++; mov edx, DWORD PTR _ri$[ebp];将_ri$[ebp]所代表的内存的内容(即变量i的地址)写入寄存器edx
mov eax, DWORD PTR [edx];将edx的值指向的内存(即存储i的内存)的内容(即i的值1)写入寄存器eax
add eax, ;将eax里面的值加1,结果存入eax寄存器
mov ecx, DWORD PTR _ri$[ebp];将_ri$[ebp]所代表的内存的内容(即变量i的地址)写入寄存器ecx
mov DWORD PTR [ecx], eax;将eax的值写入ecx的值指向的内存(即变量i所在的内存) ; 8 ; 8 : (*pj)++; mov edx, DWORD PTR _pj$[ebp];将_pj$[ebp]所代表的内存的内容(即变量j所在内存的地址)写入寄存器edx
mov eax, DWORD PTR [edx];将edx的值指向的内存(即变量j所在的内存)的内容(即变量j的值2)写入寄存器eax
add eax, ;将eax里面的值加1,结果存到寄存器eax里面
mov ecx, DWORD PTR _pj$[ebp];将_pj$[ebp]所代表的内存内容(即变量j所在的内存的地址)写入寄存器ecx
mov DWORD PTR [ecx], eax;将eax的值写入ecx的值指向的内存(即变量j所在的内存) ; 9 : }
从上边初始化指针和引用的汇编码可以看出:两者都是保存的变量的地址,虽然初始化指针的时候明确使用了&运算符,但是初始化引用的时候,编译器仍然保存的是变量i的地址。
而在用指针和引用操作各自所指的变量的时候,对于引用,虽然没有使用*运算符,通过保存的地址获取变量的对应值,但是,编译器已经帮我们做了,可以把对引用和指针的操作的汇编码进行比较,可以发现,二者是一样的。
因此,对于指针,pj它保存的是变量j的地址,但是如果我们要通过指针变量pj获取j的指针,必须自己用*运算符做这种寻址操作;但是,但我们是用引用的时候,ri虽然也保存的是变量i,但是获取变量i的值的时候不必使用*运算符来寻址,因为编译器已经帮我们做了,正因为省略了用*运算符寻址的过程,使得引用使用起来更像普通变量一样,也就是说使用引用ri和使用变量i的效果是一样的。我觉得,这就是把引用称为变量的别名的原因。
作为一种特殊的类型,引用也有它自己的特点:
1 必须初始化
2 不能指向NULL
3 一旦初始化了引用,这个引用就一直引用某一变量,无法改变。比如,ri只能是i的引用,不能通过ri=j使其引用变量j,这条指令只是将ri引用的变量i赋值为j的值,ri仍然引用的是i,也就是说保存的仍然是变量i的地址,即给引用赋值,只能在初试化的时候。
4 永远也无法获取引用的地址,即无法通过&ri获取ri的地址,这样获取的都是变量i的地址
5 没有引用类型的数组,即,不能声明int& a[3];
2 传引用与返回引用
下面来看引用作为参数和返回值的情况:
c++源码如下:
//该函数传递引用也返回引用
int& f(int& i) {
i++;
return i;
} int main() {
int i = ;
int j = f(i);
}
下面是main函数对应的汇编码:
: int main() { push ebp
mov ebp, esp
sub esp, ; 8 : int i = 1; mov DWORD PTR _i$[ebp], ;将1写入_i$[ebp]所代表的内存,即为变量i赋值1 ; 9 : int j = f(i); lea eax, DWORD PTR _i$[ebp];将_i$[ebp]所代表的内存(即变量i所在内存)给寄存器eax
push eax;将eax的值压栈,作为参数传递给函数f
call ?f@@YAAAHAAH@Z ; 调用函数f
add esp, ;这条指令的作用是将栈顶指针下移4byte,目的是为了释放传递参数所分配的栈空间
mov ecx, DWORD PTR [eax];将eax的值所指向的内存(即变量i所在的内存)的内容(即变量i的值)给寄存器ecx
mov DWORD PTR _j$[ebp], ecx;将ecx的值写入_j$[ebp]所代表的的内存(即变量j所在内存),初始化变量j ; 10 : }
可以看到,传递给函数f的参数时实际参数变量i的地址,这和上面讲的ri=i一样,只不过这里使用实参初始化形参引用。
下面是函数f的汇编码:
; 2 : int& f(int& i) { push ebp
mov ebp, esp ; 3 : i++; mov eax, DWORD PTR _i$[ebp]; 将_i$[ebp]所代表的的内存内容(即传进来的参数,也就是变量i的地址)给寄存器eax
mov ecx, DWORD PTR [eax];将eax的值所指向的内存(即变量i所在内存)的内容(即变量i的值)给寄存器ecx
add ecx, ;将ecx的值加1
mov edx, DWORD PTR _i$[ebp];将_i$[ebp]所代表的内存内容(即传进来的参数,也就是变量i的地址)给寄存器edx
mov DWORD PTR [edx], ecx;将ecx的值给edx的值所指向的内存(即变量i所在的内存) ; 4 : return i; mov eax, DWORD PTR _i$[ebp];将_i$[ebp]所代表的内存内容(即传进来的参数,也就是变量i的地址)给寄存器eax,作为返回值 ; 5 : }
可以看到,函数f返回的值也是一个地址,即实参i的地址。
从上面可以总结,当引用作为参数和返回值的时候,实际上也是传递和返回的地址
3 临时变量和引用
当用引用作为参数时,可能产生临时变量,然后用临时变量的地址初始化引用参数,但是产生临时变量有条件:
c++源码如下:
void f(int& i) {} int main() {
int i;
f(i + );
}
编译之后,就会产生如下错误:
出现这种错误,是因为要调用函数f,必须为其传递一个地址,但是(i + 1)并不是一个变量,也就是说,并没有为(i + 1)生成临时变量,无法获取地址,它只是一个非左值。下面就来看一下生成临时变量的条件:
在引用参数为const的情况下:
1 实参的类型正确,但是不是左值(上面的类型就是这种情况)
2 实参的类型不正确,但是可以转换成正确的类型
所谓左值,就是可以被引用的数据对象,常规变量(可以修改的左值)和const变量(不可以修改的左值)都可以看成左值,而非左值,包括字面常量(比如单纯的数字1,但用引号括起来的字符串除外,他们有地址表示)和包含多项的表达式,他们不能被取地址,也不允许赋值。
因此,如果把上述函数f的参数类型改成const int&就可以通过编译,因为这时候会为(i + 1)生成临时变量。
为什么作为函数参数的引用类型要是const才能生成临时变量?因为使用引用作为函数参数的目的,就是可以修改做为参数传递的变量,但是,创建临时变量会阻止这种意图,也就是说,如果作为参数的引用没有const关键字,而临时变量依然可以产生,那么,引用操作的将是这些临时变量,而不是传递进来的参数本身(这和传值差不多)。而有const关键字的时候,表示函数不能改变这些传进来的参数,因此,用生成的临时变量来初始化这些参数也就无所谓了。
但是,上面的规则,只是对于内建类型成立,而对于复合类型(结构体和类,不包括数组,因为没有引用类型的数组,数组只能通过指针传递),总是会生成临时变量:
class X {
private:
int i;
int j;
};
void f(X& x) {}
int main() {
int i;
f(X());
}
上面的c++代码可以通过编译。
再谈c++中的引用的更多相关文章
- Unity教程之再谈Unity中的优化技术
这是从 Unity教程之再谈Unity中的优化技术 这篇文章里提取出来的一部分,这篇文章让我学到了挺多可能我应该知道却还没知道的知识,写的挺好的 优化几何体 这一步主要是为了针对性能瓶颈中的”顶点 ...
- 浅谈Java中的引用
在Java语言中,引用是指,某一个数据,代表的是另外一块内存的的起始地址,那么我们就称这个数据为引用. 在JVM中,GC回收的大致准则,是认定如果不能从根节点,根据引用的不断传递,最终指向到一块内存区 ...
- 再谈AR中的图像识别算法
之前在<浅谈移动平台创新玩法>简单的猜测了easyar中使用的图像识别算法,基于图片指纹的哈希算法的图片检索 .后再阿里引商大神的指点下,意识到图片检测只适用于静态图片的识别,只能做AR脱 ...
- 再谈vim中多窗口的编辑
参考:http://blog.csdn.net/shuangde800/article/details/11430659 很好 鼠标在各个窗口间循环移动: ctrl+w+(小写的 hjkl), &qu ...
- 再谈机器学习中的归一化方法(Normalization Method)
机器学习.数据挖掘工作中,数据前期准备.数据预处理过程.特征提取等几个步骤几乎要花费数据工程师一半的工作时间.同时,数据预处理的效果也直接影响了后续模型能否有效的工作.然而,目前的大部分学术研究主要集 ...
- NET Core微服务之路:再谈分布式系统中一致性问题分析
前言 一致性:很多时候表现在IT系统中,通常在分布式系统中,必须(或最终)为多个节点的数据保持一致.世间万物,也有存在相同的特征或相似,比如儿时的双胞胎,一批工厂流水线的产品,当然,我们不去讨论非IT ...
- 再谈vim中多窗口的编辑 ctrl+w+H窗口位置最大化和互换等操作
参考:http://blog.csdn.net/shuangde800/article/details/11430659 很好 鼠标在各个窗口间循环移动: ctrl+w+(小写的 hjkl), &qu ...
- 再谈Contacts中姓氏多音字排序错误问题
说到中国人的名字,那就不得不考虑多音字的问题,比如'单',在作为姓氏时应该读作'shan'而不是'dan'.但是在Contacts程序中却使用的是'D'来作为bucket label!这是为什么?如何 ...
- 再谈JVM中类加载
前言 由于本人参加面试,但是JVM这块回答的十分不好,问了面试官,面试官说我基础不行!我真的不行,所以看过的不一定能理解,感觉之前就是糊弄任务,然后这次等实训结束,啥都干完了,我就记录下,深入了解下面 ...
随机推荐
- (转+原)python中的浅拷贝和深拷贝
转载请注明出处: http://www.cnblogs.com/darkknightzh/p/6069722.html 原网址: http://blog.csdn.net/sunshine_in_mo ...
- [Mugeda HTML5技术教程之12]制作跨屏互动应用
mugeda动画平台还可以用来制作跨屏互动的动画应用,比如在PC端的大屏幕上显示动画的主界面,同时会显示出供手机扫描的二维码,手机扫描后会在手机上显示手机端动画界面.通过手机就可以和PC端的显示界面跨 ...
- 【Android】使用Gson和Post请求和服务器通信
一.需求文档如下: URL:http://108.188.129.56:8080/example/cal 请求格式: {"para1":10,"para2":2 ...
- 使用 Mockito 单元测试 – 教程
tanyuanji@126.com 版本历史 - - - - 使用 Mockito 进行测试 该教程主要讲解 Mockito 框架在Eclipse IDE 中的使用 目录 tanyuanji@12 ...
- scaletype
http://www.myexception.cn/image/726203.html 图片说明Andorid中ImageView的不同属性ScaleType的区别 ImageView是Android ...
- input元素有padding间距,所以使用box-sizing来保持宽度不超出父元素
http://vicbeta.com/code/2013/04/24/phone-over-width.html 手机web开发资料少,原创解决方案Mark. 手机页面遇到一个横竖屏切换时出现的问题. ...
- 三元运算符和GridView数据显示
三元运算符嵌套使用:<%# Eval("InsertType").ToString() == "0" ? "数据库" : Eval(& ...
- AnimateWindow
WINDOWS提供了一个很有意思的函数:AnimateWindow.之前我想实现像MSN,QQ这些收到邮件的时候动画方式,从地下升上来的显示一个窗口,感觉很麻烦,自己去写代码,效果很不理想,今天无意中 ...
- ANDROID使用MULTIPARTENTITYBUILDER实现类似FORM表单提交方式的文件上传
最近在做 Android 端文件上传,要求采用 form 表单的方式提交,项目使用的 afinal 框架有文件上传功能,但是始终无法与php写的服务端对接上,无法上传成功.读源码发现:afinal 使 ...
- Lucene.net常见功能实现知识汇总
在开发SearchEasy Site SearchEngine(搜易站内搜索引擎)的时候,经常会遇到一些搜索引擎的常见功能如何实现的问题,比如实现相关度百分比显示?如何实现在结果中搜索等等诸如此类常见 ...