再谈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这块回答的十分不好,问了面试官,面试官说我基础不行!我真的不行,所以看过的不一定能理解,感觉之前就是糊弄任务,然后这次等实训结束,啥都干完了,我就记录下,深入了解下面 ...
随机推荐
- DOM 节点实例操作
涉及知识点包括节点的所有知识 目的: 自动为文档创建一个目录表 自动创建目录
- FTP两种工作模式:主动模式(Active FTP)和被动模式(Passive FTP)
在主动模式下,FTP客户端随机开启一个大于1024的端口N向服务器的21号端口发起连接,然后开放N+1号端口进行监听,并向服务器发出PORT N+1命令.服务器接收到命令后,会用其本地的FTP数据端口 ...
- NAS4Free 安装配置(六)配置transmission实现BT(PT)下载
配置transmission transmission是一个跨平台的BT客户端 首先我们建立一个存放transmission配置文件的目录 可以通过SSH,也可以通过网页来完成 注意:最好是通过SSH ...
- C 中va_list,va_arg,va_start,va_end usage
1.在学习c语言,难免会遇到多参函数,刚好c中也提供了一些机制:宏函数 #ifdef _M_ALPHA typedef struct { char *a0; /* pointer to first h ...
- CoreData (四)备
监听NSFetchedResultsController 之前说过, NSFetchedResultsController是有两个重要的功能. 第一:NSFetchedResultsControlle ...
- [UVA] 11991 - Easy Problem from Rujia Liu? [STL应用]
11991 - Easy Problem from Rujia Liu? Time limit: 1.000 seconds Problem E Easy Problem from Rujia Liu ...
- Tuning Radio Resource in an Overlay Cognitive Radio Network for TCP: Greed Isn’t Good
好吧,这是09年七月发布在IEEE Communications Magazine的一篇文章. 核心二个词:overlay cognitive radio network,tcp 讲的是,在认知无线网 ...
- Keil "RECURSIVE CALL TO SEGMENT"彻底解决
我们在做菜单程序或通过函数指针调用函数时,如果被调用的函数中有包含了常量字符串,那么经常会出现这样的的错误提示:"RECURSIVE CALL TO SEGMENT"意思是&quo ...
- ImageMaigck不支持中文路径的问题
不知道咋回事. 回顾下: char* pTest1 = "测试.txt"; wchar_t* pTest2 = L"测试.txt"; 以上是pTest1指向的内 ...
- 安卓开发中ScrollView不能用RelativeLayout的解决方案
RelativeLayout的意义: 布局各个部件的相对布局.使得界面空间合理利用. 一.ScrollView局限: 滑动的只能是linearlayout,甚至整个布局都不能有RelativeLayo ...