函数调用过程中:实参将值拷贝给函数的形参,而函数的形参相当于一个生存周期位于函数
内部的局部变量,函数内部的内存操作也只是将拷贝到形参的值进行操作,形参在函数结束
后会被栈自动回收释放(形参在栈中分配),这就是为什么值传递后在形参中改变不了实参的值
了,因为实参只是把值拷贝给了形参,而实参是在另一个堆栈地址中。
int swap(int x, int y)//实参x和y的值并没有交换
{
int temp;
temp = x; x = y; y = temp;
return temp;
}
所以只有引用传递,即相当于
把一个值类型的变量的地址传给形参,在形参里操作这个地址(即指针地址)才能改变实参对应的地址中的
值,如下就是引用传递,形参为指针类型。
int swap(int *x, int *y)//这里形参x和y是在栈中分配的一块内存并拷贝了实参的指针值,即x和y中的内容分别=实参的指针变量的值。
{
int temp;
temp = *x; *x = *y; *y = temp;
return temp;
}
main()
{
int a=8;
int b=9;
int* aa=&a;
int* bb=&b
swap(aa,bb)//这里形参是在栈中分配的一块内存并拷贝了实参aa,bb的指针值,即形参中的内容分别=实参的指针变量的值。
}
另外,如果在函数内部声明定义一个变量并且是在栈中分配内存的,那么这个变量是局部变量将会在函数运行结束自动被栈回收,如果是在堆中分配内存的那么这个变量一般都是指针类型即指针变量,这个指针变量本身也是在栈中分配的函数结束会自动被栈回收,但是它指向的内存地址实在堆中分配需要手动释放才行。
func()
{
int* a=new int(0);//a是局部变量是在栈中分配的一个存储指针地址的变量(32位程序的话就是4个字节大小,会在函数结束后被栈回收释放),而a所指向的地址(new int(0)是在堆中分配内存的一种方式,其他的还有 *p = (char *)malloc(num)等方式)是在堆中分配需要手动释放(函数结束后系统不会自动处理堆中的内存)。
}
/***************************************************
一、 函数参数传递机制的基本理论
函数参数传递机制问题在本质上是调用函数(过程)和被调用函数(过程)在调用发生时进行通信的方法问题。基本的参数传递机制有两种:值传递和引用传递。以下讨论称调用其他函数的函数为主调函数,被调用的函数为被调函数。
值传递(passl-by-value)过程中,被调函数的形式参数作为被调函数的局部变量处理,即在堆栈中开辟了内存空间以存放由主调函数放进来的实参的值,从而成为了实参的一个副本。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。
引用传递(pass-by-reference)过程中,被调函数的形式参数虽然也作为局部变量在堆栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过堆栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。
二、 C语言中的函数参数传递机制
在C语言中,值传递是唯一可用的参数传递机制。但是据笔者所知,由于受指针变量作为函数参数的影响,有许多朋友还认为这种情况是引用传递。这是错误的。请看下面的代码:
int swap(int *x, int *y)
{
int temp;
temp = *x; *x = *y; *y = temp;
return temp;
}
void main()
{
int a = 1, b = 2;
int *p1 = &a;
int *p2 = &b;
swap(p1, p2)
}
函数swap以两个指针变量作为参数,当main()调用swap时,是以值传递的方式将指针变量p1、p2的值(也就是变量a、b的地址)放在了swap在堆栈中为形式参数x、y开辟的内存单元中。这一点从以下的汇编代码可以看出(注释是笔者加的):
22: void main()
23: {
……
……
13: int a = 1, b = 2;
00401088 mov dword ptr [ebp-4],1
0040108F mov dword ptr [ebp-8],2
14: int *p1 = &a;
00401096 lea eax,[ebp-4]
00401099 mov dword ptr [ebp-0Ch],eax
15: int *p2 = &b;
0040109C lea ecx,[ebp-8]
0040109F mov dword ptr [ebp-10h],ecx
16: swap(p1, p2);
004010A2 mov edx,dword ptr [ebp-10h] ;参数p2的值进栈
004010A5 push edx
004010A6 mov eax,dword ptr [ebp-0Ch] ;参数p1的值进栈
004010A9 push eax
004010AA call @ILT+15(swap) (00401014) ;调用swap函数
004010AF add esp,8 ;清理堆栈中的参数
17: }
阅读上述代码要注意,INTEL80x86系列的CPU对堆栈的处理是向下生成,即从高地址单元向低地址单元生成。从上面的汇编代码可知,main()在调用swap之前,先将实参的值按从右至左的顺序压栈,即先p2进栈,再p1进栈。调用结束之后,主调函数main()负责清理堆栈中的参数。Swap 将使用这些进入堆栈的变量值。下面是swap函数的汇编代码:
14: void swap(int *x, int *y)
15: {
00401030 push ebp
00401031 mov ebp,esp ;ebp指向栈顶
……
……
16: int temp;
17: temp = *x;
4: int temp;
5: temp = *x;
00401048 mov eax,dword ptr [ebp+8] ;操作已存放在堆栈中的p1,将p1置入eax
0040104B mov ecx,dword ptr [eax] ;通过寄存器间址将*p1置入ecx
0040104D mov dword ptr [ebp-4],ecx;经由ecx将*p1置入temp变量的内存单元。以下类似
6: *x = *y;
00401050 mov edx,dword ptr [ebp+8]
00401053 mov eax,dword ptr [ebp+0Ch]
00401056 mov ecx,dword ptr [eax]
00401058 mov dword ptr [edx],ecx
7: *y = temp;
0040105A mov edx,dword ptr [ebp+0Ch]
0040105D mov eax,dword ptr [ebp-4]
00401060 mov dword ptr [edx],eax
8: return temp;
00401062 mov eax,dword ptr [ebp-4]
9: }
由上述汇编代码基本上说明了C语言中值传递的原理,只不过传递的是指针的值而已。本文后面还要论述使用引用传递的swap函数。从这些汇编代码分析,这里我们可以得到以下几点:
1. 进程的堆栈存储区是主调函数和被调函数进行通信的主要区域。
2. C语言中参数是从右向左进栈的。
3. 被调函数使用的堆栈区域结构为:
局部变量(如temp)
返回地址
函数参数
低地址
高地址
4. 由主调函数在调用后清理堆栈。
5. 函数的返回值一般是放在寄存器中的。
这里尚需补充说明几点:一是参数进栈的方式。对于内部类型,由于编译器知道各类型变量使用的内存大小故直接使用push指令;对于自定义的类型(如structure),采用从源地址向目的(堆栈区)地址进行字节传送的方式入栈。二是函数返回值为什么一般放在寄存器中,这主要是为了支持中断;如果放在堆栈中有可能因为中断而被覆盖。三是函数的返回值如果很大,则从堆栈向存放返回值的地址单元(由主调函数在调用前将此地址压栈提供给被调函数)进行字节传送,以达到返回的目的。对于第二和第三点,《Thinking in C++》一书在第10章有比较好的阐述。四是一个显而易见的结论,如果在被调函数中返回局部变量的地址是毫无意义的;因为局部变量存于堆栈中,调用结束后堆栈将被清理,这些地址就变得无效了。
三、 C++语言中的函数参数传递机制
C++既有C的值传递又有引用传递。在值传递上与C一致,这里着重说明引用传递。如本文前面所述,引用传递就是传递变量的地址到被调函数使用的堆栈中。在C++中声明引用传递要使用"&"符号,而调用时则不用。下面的代码是使用引用传递的swap2函数和main函数:
int& swap2(int& x, int& y)
{
int temp;
temp = x;
x = y;
y = temp;
return x;
}
void main()
{
int a = 1, b = 2;
swap2(a, b);
}
此时函数swap2将接受两个整型变量的地址,同时返回一个其中的一个。而从main函数中对swap2的调用swap2(a, b)则看不出是否使用引用传递,是否使用引用传递,是由swap2函数的定义决定的。以下是main函数的汇编代码:
11: void main()
12: {
……
……
13: int a = 1, b = 2;
00401088 mov dword ptr [ebp-4],1 ;变量a
0040108F mov dword ptr [ebp-8],2 ;变量b
14: swap2(a, b);
00401096 lea eax,[ebp-8] ;将b的偏移地址送入eax
00401099 push eax ;b的偏移地址压栈
0040109A lea ecx,[ebp-4] ;将a的偏移地址送入ecx
0040109D push ecx ;将a的偏移地址压栈
0040109E call @ILT+20(swap2) (00401019) ;调用swap函数
004010A3 add esp,8 ;清理堆栈中的参数
15: }
可以看出,main函数在调用swap2之前,按照从右至左的顺序将b和a的偏移地
址压栈,这就是在传递变量的地址。此时swap2函数的汇编代码是:
2: int& swap2(int& x, int& y)
3: {
00401030 push ebp
00401031 mov ebp,esp
……
……
4: int temp;
5: temp = x;
00401048 mov eax,dword ptr [ebp+8]
0040104B mov ecx,dword ptr [eax]
0040104D mov dword ptr [ebp-4],ecx
6: x = y;
00401050 mov edx,dword ptr [ebp+8]
00401053 mov eax,dword ptr [ebp+0Ch]
00401056 mov ecx,dword ptr [eax]
00401058 mov dword ptr [edx],ecx
7: y = temp;
0040105A mov edx,dword ptr [ebp+0Ch]
0040105D mov eax,dword ptr [ebp-4]
00401060 mov dword ptr [edx],eax
8: return x;
00401062 mov eax,dword ptr [ebp+8] ;返回x,由于x是外部变量的偏移地
;址,故返回是合法的
9: }
可以看出,swap2与前面的swap函数的汇编代码是一样的。这是因为前面的swap函数接受指针变量,而指针变量的值正是地址。所以,对于这里的swap2和前面的swap来讲,堆栈中的函数参数存放的都是地址,在函数中操作的方式是一致的。但是,对swap2来说这个地址是主调函数通过将实参变量的偏移地址压栈而传递进来的--这是引用传递;而对swap来说,这个地址是主调函数通过将实参变量的值压栈而传递进来的--这是值传递,只不过由于这个实参变量是指针变量所以其值是地址而已。
这里的关键点在于,同样是地址,一个是引用传递中的变量地址,一个是值传递中的指针变量的值。我想若能明确这一点,就不至于将C语言中的以指针变量作为函数参数的值传递情况混淆为引用传递了。
虽然x是一个局部变量,但是由于其值是主调函数中的实参变量的地址,故在swap2中返回这个地址是合法的。
c++ 中经常使用的是常量引用,如将swap2改为:
Swap2(const int& x; const int& y)
这时将不能在函数中修改引用地址所指向的内容,具体来说,x和y将不能出现在"="的左边。
四、 结束语
本文论述了在 C 和 c++ 中函数调用的参数传递机制;同时附带说明了函数返回值的一些问题。本文示例使用的是VC++6.0。
可见值传递是传输了要传递的变量的一个副本,所以改变这个副本不会对调用函数造成影响,但是这个被调用函数一般有一个有用的返回值,也就是你用某个东西,在使用过程中,也许改变了它,但是时候后,你又保持原样给了人家。比如给你一个打好节的丝巾,你使用时换了另一种样式,照了像,还别人的时候,又按照人家的借你的样子还给人家,而这个照片就是需要得到的东西(类似返回值)。
而引用,就是将要传递的变量的地址传到了被调用函数中,如果在被调用函数中改变,那么就会在调用函数中改变。比如你借了人家布,如果你剪裁了不同的样式,那么还人家的样子就是你剪裁后的样子。一般c++可以使用值传递和引用传递,后者更多。因为这样不用另外在堆栈中开辟空间,而值传递就需要另外的开辟空间,对内存有一定的浪费。一般c中只使用值传递。
另外关于存储数据方面,一般是将局部变量,函数返回地址,函数参数放到堆栈中,而函数返回值一般放到寄存器中,为的是方便中断,如果有零时中断就可以直接从寄存器中处理,不用再进行压栈出栈操作。
/*******************************
指针做形参做局部变量以及内存分配
一级指针做形参:首先一定要明白形参和你传递参数的那个实参是两个不同的变量,即使同名也还依然不同。指针传递的是一个变量或者一个值的地址,但是它本身还是采用值传递的方式。即你不能使它指向另外一块地址,但是你可以改变它指向的空间里存的值。
二级指针做形参:二级指针也是传值,但是他指向的地址是个一维指针,所以可以改变二维指针指向的地址空间里的内容也就是要申请空间的一维指针,不能改变二维指针本身的值,即不能让他指向一个新的一维指针。所以二维指针传递的是一个一维指针。
具体看下面这个程序以及输出:
#include<stdio.h> #include <malloc.h> #include <string.h> void GetMemory1(char *p)// { //该函数的形参为字符指针,在函数内部修改形参并不能真正改变传入形参的实参值。 //因此在主函数中执行完下面两句之后 //char *str1 = NULL; //GetMemory1(str1); //str1仍然为NULL,因此会报内存错误,没有输出结果。 p = (char *)malloc(100); //要记得使用指针变量时,每次分配空间后要判断是否分配成功。而且在主函数中使用之后记得释放内存,并置空 if (p == NULL) { printf("error memory"); } } /*但是上面的函数参数变为*char *&p就可以在主函数中正常输出了。 指针做形参也是采用值传递的方式,也就是会把指针的值-地址传过去,所以可以改变这个地址里的内容, 但是你不能改变指针的值,也就是指向的地址。但是引用就是变量的别名,所以可以改变指针的值, 所以就可以在函数里申请空间*/ char *GetMemory2(void) { char p[] = "hello world"; return p; //p[]数组为函数内部局部变量,在函数返回后,内存已经被释放了, //所以在主函数中调用该函数str2 = GetMemory2();输出的可能为乱码 } void GetMemory3(char **p,int num) { *p = (char *)malloc(num); //要记得使用指针变量时,每次分配空间后要判断是否分配成功。而且在主函数中使用之后记得释放内存,并置空 if (*p == NULL) { printf("error memory"); } } void main() { int n=0; char *str1 = NULL; char *str2 = NULL; char *str3 = NULL; //GetMemory1(str1); //strcpy(str1,"Hello world"); //printf("%s\n",str1); str2 = GetMemory2(); printf("%s\n",str2);//输出乱码 GetMemory3(&str3,100); strcpy(str3,"hello world"); printf("%s\n",str3);//输出hello world char *str4 = (char *)malloc(100); if (str4 == NULL) { printf("error memory"); } else { strcpy(str4,"Hello"); free(str4); str4 == NULL;//free后要置空,否则str可能变成“野指针” } if (str4 != NULL) { strcpy(str4,"world"); printf("%s\n",str4);//输出world } scanf("%d",&n); }
指针做局部变量:如果你申请了空间(用new等,赋值不算)又没有delete,那么这个空间在你程序运行结束之前不会释放,只要你知道这个空间的地址,就可以访问。这里的赋值不算是指,比如,你先定义一个数组,然后把数组名赋值指针。但是char *d = "ZET";这种形式相当于new了4个空间。
下面是中兴通讯2012校招笔试题目,问输出什么?
当时答错(狂汗),现在搞明白,在函数里写了注释:
#include <stdio.h> //此函数中d也是个局部变量,函数执行完自动销毁,但是指针分配的空间不会被自动回收,除非程序员delete掉。 //所以这个可以正常输出。 char *a() { char *d = "ZET";//这个初始化的一种形式,相当于分配了四个空间 return d; } //但是第二个数组空间是系统维护的,函数执行完自动销毁 char *b() { char p[10] = {"3G平台"}; return p; } //参数是值传递方式,改变形参的地址,传递的实参的地址确不会因此改变 void c(int n,char *pName) { char *a[4] = {"aaa","bbb","ccc","ddd"}; pName = a[n]; } void main() { int n=0; char *pName = "DB"; printf("%s\n",a());//输出ZET printf("%s\n",b());//随机输出乱码 c(2,pName); printf("%s\n",pName); //输出DB,因为char *pName = "DB";已经使得pName指向了DB,但c(2,pName);并不能改变pName指向的地址。 //形象点说就是:我有一个箱子给你用,你可以在里面装东西,但是你不能把我的箱子换成另外一个给我。 //在这里指的是不能函数调用不能使pName变成函数中的二维数组a。 scanf("%d",&n); }
指针做形参,指针做局部变量,数组做形参,数组做局部变量之类的容易迷糊,得不断学习...
/***************************************************
如果函数的参数是一个指针,不要指望用该指针去申请动态内存。(详见《高质量C++编程7.4节》)
在C编译器原理上:编译器总是要为函数的每个参数制作临时副本,指针参数str的副本是 _str,编译器使 _str = str。如果函数体内的程序修改了_str的内容,就导致参数str的内容作相应的修改。这就是指针可以用作输出参数的原因。
即上面的函数代码经过编译后成为:
char * fun(char *str)
{
char *_str;
_str = str;
_str = (char *) malloc(100);
}
系统分配内存给_str指针,_str指针指向了系统分配的新地址,函数体内修改的只是_str的内容,对原str所指的地址的内容没有任何影响。因此,函数的参数是一个指针时,不要在函数体内部改变指针所指的地址,那样毫无作用,需要修改的只能是指针所指向的内容。即应当把指针当作常量。
如果非要使用函数指针来申请内存空间,那么需要使用指向指针的指针
char * fun(char **str)
{
*str = (char *) malloc(100);
}
还有另外的方案,通过函数返回值传递动态内存:
char * fun()
{
char *str;
str= (char *) malloc(100);
return str;
}
这个倒还说得过去,因为函数返回的是一个地址的值,该地址就是申请的内存块首地址。
此时,7.4节结束前还有一句话:不要用return语句返回指向“栈内存”的指针,因为该内存在函数结束时自动消亡。
这里区分一下静态内存,栈内存和动态分配的内存(堆内存)的区别:
(1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
(2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
(3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。
在此,辨析一下 char p[] = "abcdefg";
char *p = "abcdefg";
用来初始化字符数组的字符串常量"abcdefg",编译器会在栈中为字符数组分配空间,然后把字符串中的所有字符复制到数组中;
而用来初始化字符指针的字符串常量"abcdefg"会被编译器安排到只读数据存储区,但也是按字符数组的形式来存储的,我们可以通过一个字符指针读取字符串常量但不能修改它,否则会发生运行时错误。