C++引用的本质与修改引用的方法 | 您所在的位置:网站首页 › c编译器的本质目的和特点 › C++引用的本质与修改引用的方法 |
比较新的帖子在这。 http://blog.csdn.net/huqinweI987/article/details/50769096 包含引用在函数传参时的流程分析等。
============================================================================================================= 本文不探讨罗列引用的概念,什么函数传参之类的,这些基础概念和用法很容易搜到~! 本文主要探讨引用和指针在C语言的下一层——即汇编或者确切的说是伪汇编(AT&T伪汇编都一样的代码,你指望下层x86汇编还能不一样么~)——的实现过程,来摸索一下他们的特点与本质。
首先,引用(Reference)在C中没有,是C++ 才有的概念~! 要用g++编译器。 定义:引用就是某个目标变量的“别名”(alias)
在我看来,这个“目标变量”也要加上引号,看“目标变量”到底是怎么定义的了。如果“目标变量”由变量名和值组成,那引用应该是不包含“变量名”这部分的,说白了,觉得他就是一个“新变量”,只是他和原变量的“值”(即,目标地址,存储内容)是共用的。
实例测试:
用g++编译,gdb调试:
可以看到,让refa引用a的过程,其实就是提取地址(lea),并且占用了栈空间。和指针的实现是一模一样的。不管你“理论上”怎么说,至少在实现上(至少在linux的实现上),他就是指针。
可以看到,操作都是直接或者间接的对a的原地址0x10(%esp)进行操作,这个没什么问题。但是说引用不占地址是错误的,作为一个“指针”他至少占用4字节吧~! 这是代码后续的赋值操作: Breakpoint 2, main () at ref2.cpp:13 13 a= 2; 1: x/i $pc => 0x804868d : movl $0x2,0x10(%esp)
Breakpoint 3, main () at ref2.cpp:18 18 refa= 3; 1: x/i $pc => 0x8048705 : mov 0x14(%esp),%eax (gdb) si 0x08048709 18 refa = 3; 1: x/i $pc => 0x8048709 : movl $0x3,(%eax)
22 *ptra= 3; 1: x/i $pc => 0x804877f : mov 0x18(%esp),%eax (gdb) si 0x08048783 22 *ptra = 3; 1: x/i $pc => 0x8048783 : movl $0x3,(%eax)
可以看到引用和指针,从定义到赋值,实现都是一样的。
虽然引用和 指针的意义,认为差不多,但是使用方法还是有差别的,想获得右值,引用直接用变量名, 指针要加*操作符。而对引用使用*操作是不允许的。 另外,不同于指针,引用在声明的时候必须初始化, 但引用可能只能一次初始化而不能改变引用“目标”吗? 至少通过如下方法是不能的: int a = 1; int b = 2; int &refa = a; refa = b; 这相当于赋值为b,即赋值为2,连a的值都会变成2. &refa = &b; 也是不可能的,因为&refa不是左值。 refa = &b; 更不对了,因为这也相当于赋值,不过不是2了,是b的地址(打印成10进制,类似于-1075934160这种),并且,需要强制转换: refa = (int)&b;
说再多都是YY,实践出真知~! 围绕我的”引用即指针“的理念,再做一个摸索,既然认为引用是指针了,那么sizeof任何指针,包括double的,肯定都是4(我的32位机)。我定义一个double的引用,看看sizeof结果如何(右侧为输出结果):
这个结果倒是没夸张到直接让ref变成pointer。sizeof(refd)还是按普通的double来算大小,而不是直接按指针来算的。但是也情有可原吧,都说了,虽然他的底层实现和指针一样,但是sizeof()需要的是返回类型,它的返回类型——即”操作级别“,还是比指针要低的。 其实我更倾向于这是编译器的优化,碰到sizeof()使用引用当操作数的,直接转换成原类型来传递了。(sizeof是操作符不是函数)
最后:到底怎样理解引用更好? 首先,不太同意“引用就是一个别名,不占用内存空间“的说法,至少这句话可以再严谨点——”引用不为值再去开辟一个地址空间,但是其本身要占用空间~!“
奇了怪了,引用确实占用栈空间,也确实是存储了目标变量的地址~~~那既然有空间,就应该和指针一样,我改变你的值不就等于改变你的指向了么? 但是,因为它和指针不在同一个“操作级别”上,它的”值“又不是地址,也不能像指针那样改变他的指向。 (“操作级别”是通过存储内容来判定的,比如普通变量的存储内容是“值”,而指针的存储内容是“地址”,可以通过指针独特的“*”操作来判断这个“级别”)
个人倾向于认为引用本身就是一种指针,至于他又不能像指针一样进行重定向等操作,觉得这些完全是语言级别或者说编译器的刻意限制,只是一种规则,没有其他原因。 再次怀疑人生——编译器的本质如何?到底什么叫做编程语言?各层语言界限如何?从这么多的实践操作经验来总结,似乎也逐渐理解了些,如果再去看看《编译原理》,或许会有所收获。
本文不探讨罗列引用的概念,什么函数传参之类的,这些基础概念和用法很容易搜到~! 本文主要探讨引用和指针在C语言的下一层——即汇编或者确切的说是伪汇编(AT&T伪汇编都一样的代码,你指望下层x86汇编还能不一样么~)——的实现过程,来摸索一下他们的特点与本质。
首先,引用(Reference)在C中没有,是C++ 才有的概念~! 要用g++编译器。 定义:引用就是某个目标变量的“别名”(alias)
在我看来,这个“目标变量”也要加上引号,看“目标变量”到底是怎么定义的了。如果“目标变量”由变量名和值组成,那引用应该是不包含“变量名”这部分的,说白了,觉得他就是一个“新变量”,只是他和原变量的“值”(即,目标地址,存储内容)是共用的。
实例测试:
用g++编译,gdb调试:
可以看到,让refa引用a的过程,其实就是提取地址(lea),并且占用了栈空间。和指针的实现是一模一样的。不管你“理论上”怎么说,至少在实现上(至少在linux的实现上),他就是指针。
可以看到,操作都是直接或者间接的对a的原地址0x10(%esp)进行操作,这个没什么问题。但是说引用不占地址是错误的,作为一个“指针”他至少占用4字节吧~! 这是代码后续的赋值操作: Breakpoint 2, main () at ref2.cpp:13 13 a= 2; 1: x/i $pc => 0x804868d : movl $0x2,0x10(%esp)
Breakpoint 3, main () at ref2.cpp:18 18 refa= 3; 1: x/i $pc => 0x8048705 : mov 0x14(%esp),%eax (gdb) si 0x08048709 18 refa = 3; 1: x/i $pc => 0x8048709 : movl $0x3,(%eax)
22 *ptra= 3; 1: x/i $pc => 0x804877f : mov 0x18(%esp),%eax (gdb) si 0x08048783 22 *ptra = 3; 1: x/i $pc => 0x8048783 : movl $0x3,(%eax)
可以看到引用和指针,从定义到赋值,实现都是一样的。
虽然引用和 指针的意义,认为差不多,但是使用方法还是有差别的,想获得右值,引用直接用变量名, 指针要加*操作符。而对引用使用*操作是不允许的。 另外,不同于指针,引用在声明的时候必须初始化, 但引用可能只能一次初始化而不能改变引用“目标”吗? 至少通过如下方法是不能的: int a = 1; int b = 2; int &refa = a; refa = b; 这相当于赋值为b,即赋值为2,连a的值都会变成2. &refa = &b; 也是不可能的,因为&refa不是左值。 refa = &b; 更不对了,因为这也相当于赋值,不过不是2了,是b的地址(打印成10进制,类似于-1075934160这种),并且,需要强制转换: refa = (int)&b;
围绕我的”引用即指针“的理念,再做一个摸索。既然认为引用是指针了,那么sizeof任何指针,包括double的,肯定都是4(我的32位机)。我定义一个double的引用,看看sizeof结果如何(右侧为输出结果):
这个结果倒是没夸张到直接让ref变成pointer。sizeof(refd)还是按普通的double来算大小,而不是直接按指针来算的。但是也情有可原吧,都说了,虽然他的底层实现和指针一样,但是sizeof()需要的是返回类型,它的返回类型——即”操作级别“,还是比指针要低的,和普通的变量相仿。
最后:到底怎样理解引用更好? 首先,不太同意“引用就是一个别名,不占用内存空间“的说法,至少这句话可以再严谨点——”引用不为值再去开辟一个地址空间,但是其本身要占用空间~!“
奇了怪了,引用确实占用栈空间,也确实是存储了目标变量的地址~~~那既然有空间,就应该和指针一样,我改变你的值不就等于改变你的指向了么? 但是,因为它和指针不在同一个“操作级别”上,它的”值“又不是地址,也不能像指针那样改变他的指向。 (“操作级别”是通过存储内容来判定的,比如普通变量的存储内容是“值”,而指针的存储内容是“地址”,可以通过指针独特的“*”操作来判断这个“级别”)
个人倾向于认为引用本身就是一种指针,至于他又不能像指针一样进行重定向等操作,觉得这些完全是语言级别或者说编译器的刻意限制,只是一种规则,没有其他原因。 反正翻译成下层的东西,都是那点破事,转换成最后就是一些地址一些寄存器,你能找到地址你就能改(不能改的话,又是哪层编译器或者汇编器限制你的呢?)~! (那么,编译器的本质如何?到底什么叫做编程语言?各层语言界限如何?从这么多的实践操作经验来总结,似乎也逐渐理解了些,如果此时再去看看《编译原理》,或许会有所收获。)
完~!
------------------------ ------------------------ ------------------------ ------------------------ ------------------------ ------------------------ ------------------------ ------------------------ ------------------------ ------------------------ ------------------------ ------------------------ ------------------------ ------------------------ ------------------------ ------------------------ ------------------------ ------------------------ --------------- OTHER: ------------------------ ------------------------ ------------------------ ------------------------ ------------------------ ------------------------ ------------------------ ------------------------ ------------------------ ------------------------ ------------------------ ------------------------ ------------------------ ------------------------ ------------------------ ------------------------ ------------------------ ------------------------ --------------- 写的是关于引用的,但是通过用gdb调试,个人还是有其他方面的收获: 例如,AT&T汇编中括号的含义,目测带括号是取地址,不带括号是原寄存器,好像和之前《计算机组成原理》的伪指令规则差不多。 对比: 存入数值到eax寄存器——用%eax。 存入数值到eax寄存器所储存的内存地址处——用(%eax)。 esp的操作同样如此:比如0x18(%esp)应该是从esp取出内存地址,再加上0x18偏移量。
还有,之前看linux的伪汇编,esp一般都是不变的(新补充的内容,esp和ebp其实都是跟着栈帧(stack frame)走的,可以变,不变是因为在主函数,没发生函数调用),变的是偏移值,使用类似于0x1c(%esp)的形式进行操作。 每次编译运行,esp起始都是230结尾的(系统决定,具体:0xbffff230),但是通过本例观察,说esp不变是不准确的,执行系统调用,涉及各种库的时候,一直在变:从230到22c、228、224。。。等于栈下移了?在同一函数内不移,切换了才移?
也许试试嵌套个函数什么的也会有发现~ 关于栈指针怎么跳转,甚至发生函数跳转时十几个寄存器到底保存上下文需要几个,而这几个压栈又是怎么压的,有一个规则,按顺序压,按倒序取?这又是另外一篇日志要探索的事情了。已经搞定! http://blog.csdn.net/huqinweI987/article/details/50790778
其他未完成作业: 看看const的实现又是怎样的,是否有什么特殊的方法规定”只读“,比如转存寄存器之类的。 使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名"的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。
========================================================================================================================= 补充完善(20160316): 原变量与引用的实际使用区别——传参: int a = 1; int &refA; 引用是指向变量的指针,引用存的是变量的地址,变量存的是值。 函数传参传递变量地址: 传递地址&a汇编操作是lea 取a的地址到eax,再将eax所指向地址a的值取出,传参。 传递地址&refA的汇编操作是move 取refA的内容到eax,再将eax所指向地址a的值取出,传参。 这样一来想获取refA的地址也就是获取a的地址了,所以从操作层面上,refA等价于a了。
而sizeof()操作符(非函数),直接就返回int类型大小4了,被编译器优化过,转换过程隐藏了。所以无法通过sizeof()发觉其指针变种的本质。 唯一能证明refA是“指针”的证据,就是栈里边实实在在的空间。其他操作都被隐藏了。
而要说引用能改吗?其实也能改,在内存中存在的东西,都能改!!手段非常罢了!
下边是一个利用指针强改引用的例子: #include using namespace std; int main() { int a = 1; int b = 2; int &refA = a; int *ptrA = NULL; ptrA = &a; ptrA++; *ptrA = (int)&b; int size = 0; size = sizeof(a); size = sizeof(refA); cout |
CopyRight 2018-2019 实验室设备网 版权所有 |