您的位置:首页 > 其它

从汇编和高级语言的角度理解传值方式,传值,传引用,传指针的本质机制与区别。白话通俗易懂。

2015-05-23 21:22 369 查看
函数的传参与返回值的方式有传值和传递引用,c语言中就是传值,而c++扩展传引用。

而传值分为传递值(实参的值,此时形参是实参在内存中的一份拷贝,形参在使用时分配内存,结束时释放,实参和形参在内存中的地址不同,因此对形参的改变不会改变实参)

传值的另外一种是传指针(传递的值是实参的地址,此时形参的值为实参的地址,所以对形参的修改就会修改实参的值)

而传引用,从高级语言的层面看,引用是实参的别名,所以对形参的修改就是直接对实参的改变。引用是变量的地址,可以理解为一个常指针,也就是指向固定的指针,但是与指针还是有区别的,具体使用的时候不同,并且引用于指针有很多不同,引用必须初始化,但是指针可以定义的时候不初始化,另外指针可以指向空NULL,这一方面引用比指针更安全,但是引用还是存在风险的只是风险小一点而已。比如,引用一个不合法的地址。举例:

int *e=newint(10);

int &f=*e;

delete e;

f=30;

从汇编底层代码的层面看,如果将函数传参的传指针和传引用的方式的函数分别生成汇编代码发现,是一模一样的,主调函数的传参的汇编代码也是一模一样的,所以从底层汇编代码看,传引用就是通过传指针实现的。汇编代码都是通过lea取地址和mov把取到的地址放到一块内存中。



还有一个问题是,引用的本质是什么,引用到底占不占内存?

主要是针对很多关于引用的说法上,比如引用是别名,不是值不占内存,只有声明没有定义。

其实这些说法是不准确的,没有从深层上去分析,只是表面上看到的现象说法。确切说是编译器给你做的优化的表象。因为从底层汇编实现代码上看,引用和指针完全一样,根本的区别就在编译器处理的环节,很多限定都是在编译时限制的,比如引用是指针指向固定的指针,这个定向就是在编译环节限定的,像const和private等一样,在汇编代码中并没有相应的代码,都是在编译器处理的时候做相关的限定的。

int a =10;

int *p=&a;

int &q=a;

指针p,每次都要先到指针p自身的地址把p的内容(p的内容是p指向的内容的地址&a)取到,然后再到取到的地址&a中把内容10取出来,也就是先寻址后取值。而编译器对引用q做了优化,每次碰到引用q就相当于*p,不用先去p里面取地址,直接就是相当于a,对所有q的操作都相于是对*p的操作也就是对a的操作,所以对q取地址,就是对a取地址,也就是&q=&a。(所以当利用sizeof对指针p,是指针本身的所占字节数,也就是地址&a的存放的字节数,而sizeofq的时候,因为q相当于a或者相当于*p,就是10所占的字节数。也就是说所有对实际引用q的操作都会被编译器转化为a的操作,不会得到实际的q的操作;另外指针和引用的自增++操作得到的结果也不同,指针自增p++,指向变化了,而引用的++相当于(*p)++,相当于a++。这个是是说明了指针本身的值(所指向的地址)是以传值的方式传递的,改变本身的值(地址值)只会改变指针本身的指向,不会改变指针所指向的变量的值,而传引用,引用本身是通过传递引用的方式传递的,改变引用本身的值就是改变所对应的变量的值。)eg:

void func(int* p, int&r);

int a = 1;

int b = 1;

func(&a,b);

指针本身的值(地址值)是以pass by value进行的,你能改变地址值,但这并不会改变指针所指向的变量的值,

p = someotherpointer; //a is still 1

但能用指针来改变指针所指向的变量的值,

*p = 123131; // a now is 123131

但引用本身是以pass byreference进行的,改变其值即改变引用所对应的变量的值

r = 1231; // b now is 1231

所以也就是看上去引用q就是a的一个别名(绰号),比如一个人有一个大名,一个小名,小名就是这个人的一个别名,你叫他的小名说他怎么样怎么样,其实就是在说这个人怎么样怎么样一个意思。还有取地址的时候发现得到的也是a的地址,所以感觉应用是不占用内存的。这里10是放在内存中开辟的一个int型大小的内存空间中,而变量名a实际是不存在的,对应于底层实现,编译器生成的目标文件中它会用存放10的内存地址代替所有的变量名a,所以变量名a不占用内存。可以看出变量的实质是编译器的一种抽象,一种底层实现机制的透明。变量名实质是内存一块区域的地址,改变这个变量就是改变了这块内存地址里面的内容,编译器抽象后让我们可以不必了解这些底层实现细节,只是简单的看到有一个变量a,它的值是10,我们可以改变a的值这样。在此处引用的变量名q相当于a的一个别名,同理在编译器优化下,生成目标文件中也会用存放10的内存地址来代替所有的引用的变量名q,可以认为引用的变量名q不占用内存。但是从实际的汇编代码的底层实现看,引用还是会占内存的,用来存放10也就是a这个地址的。一般是4字节大小,只是每次编译器都优化导致我们取不到引用的地址而已。Eg:

#include<stdio.h>

int main()

{

int x=1;

int y=2;

int &b=x;

printf("&x=%x,&y=%x,&b=%x,b=%x\n",&x,&y,&y-1,*(&y-1));

getchar();

return 0;

}



上面就证明了本身引用是占内存的,下面来看编译的优化,&b=&(*b)=&a

#include<stdio.h>

int main()

{

int x=1;

int &b=x;

printf("&x=%x,,&b=%x\n",&x,&b);

getchar();

return 0;

}





所以引用在底层汇编代码与指针相同,底层代码也是通过指针实现的,可以看成是一个定向的常指针,但是编译器阶段使用是不同的,他相当于*p,相当于直接对它引用的变量的操作,但是当作参数传递的时候,它又区别于变量a,因为传引用,形参的改变可以改变实参,而普通的传值,传变量a的值时,形参的改变是不可以改变实参的值的,这是因为传引用时,传的是地址,是变量名a,形参和实参都具有相同的地址。而普通传值时传递的是a里面的内容10这个值,传进去以后形参单独放在一个地址来保存这个10,也就是形参和实参不具有相同的地址。另外,引用在定义时必须进行初始化,不可以引用NULL,一旦被初始化,就不可以变化了,从一而终,而指针可以变化,可以指向NULL,指针可以指向别的地方等。

什么时候在指针和引用之间使用引用的情况?

根据more effective c++之中介绍的总结一下就是:

在必须要指向一个指向并且不想改变这个指向时,以及在重载操作符并且为了防止不必要的语义误解时,选用引用,其他时候选用指针。

在可能存在不指向任何指向的情况或者会改变指向的情况的时候,不应该是使用引用。

一般默认程序员不会犯不初始化,或者对不合法区域的引用时,默认使用引用比指针更加安全高效,因为默认都已经初始化而且合法的,而指针可以先定义以后在初始化,所以指针使用前需要检查,防止出现野指针的情况。

string&rs; // 错误,引用必须被初始化

string s("xyzzy");

string& rs = s; // 正确,rs指向s

指针没有这样的限制。

string *ps; // 未初始化的指针

// 合法但危险

另外还是在另外一篇博主(http://blog.csdn.net/whzhaochao/article/details/12891329)只是从高级语言层面分析了几种传值方式的比较,比较清晰明了,如果结合上面我们的这些知识分析来看下面我转过来的(只是转帖的图)(由于原博主画的图比较好,我就没有再单独画了,而且根据上面底层的理解再来从高级语言看,非常简单易懂)

我把博主里面的内容用更加通俗的白话用自己的话来分析了一下,只是单独运行一下程序的话,内存值变化,博主那几个图画的很好,我就直接拿过来了,不然画箭头太麻烦了。

1传值

这个在形参和实参那个文章中已经分析过了,从汇编底层来说,它们存在各自的函数栈之中,存在不同的内存之中,地址不同,所以改变形参不会改变实参。这个时候的值的形参就是是实参的一份拷贝。



从右边的运行结果,我们可以看出来a和p的地址不同,改变形参p没有改变实参a。



可以看出形参p是对实参a在内存中另外一个地方的一个拷贝。其实实参a在主调函数的栈空间中分配的内存中,而形参p在被调函数中的栈空间中分配的内存中,当被调函数结束时,p会被释放掉。

2传引用:



右边的结果可以看出来,a与p的地址是相同的,改变形参p就是对实参a的改变,大家可以结合上面我们的分析具体来理解一下。



对p的操作就是直接等价于对a的操作,p编译器用*p操作代替,也就是变量名a,0x10的地址。前面已经有非常详细的分析了,不再赘述。不同于指针还要先寻址在取值,可以参照对比接下来的传指针的图来对比理解。

3传指针



这个时候a和p的地址是不同,因为p有自己的本身的地址,(其实引用也有自己的地址,我们在前面讲解的时候已经用程序分析过了,由于编译器的原因我们用&p是取不到引用自身的地址,而是&a),传指针,也是可以达到改变形参来改变实参,只是要先寻址再取值的操作。



4指针的引用传递,这个大家可以简单理解一下,不过通过前面的,对这个理解也是非常容易的。



在这里是新定义了一个指针b,然后把b这个指针的引用传递,指针是有自己的内存地址的,所以&b不等于&a,同时b传递的是引用所以&b=&p,p是b的一个别名,b是指向a的,所以里面的内容都是&a,而且这种方式同理也是可以改变形参也是会相应的改变实参的值的。



再看不用指针的引用来传递的情况



这个时候,因为b是传递的指针,所以p有自己的本身的地址了,所以&p不等于&b了,但是&p和&b都是指向a的。其实这个时候p相当于一个指向指针的指针。



一个由传指针带来的错误案例:



这个是由于allocate函数传递的是指针,所以p本身有自己的地址,&p不等于&str,p指向str,里面的内容是str的内容为0,但是p调用malloc分配内存单元的时候,是给p分配的,由于p和str不是直接等同的,内存地址不同,所以没有达到通过形参分配内存达到给实参分配内存的目的,导致strcpy的出错。

正确的,也就是是p和str直接等同为同一块内存单元,那么就是引用,通过别名的形式。



是不是通过我们前面底层的分析,在来从高级语言的角度能够更加抓住本质。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: