基本理论:
函数参数传递机制问题在本质上是调用函数过程和被调用函数在调用发生时进行同的方法问题。基本的参数传递机制有两种,值传递、引用传递。
值传递: 在值传递过程中,被调函数的形参作为被调函数的局部变量,即在该函数栈中开辟内存空间以存放由主函数传递进来的实参的值,从而成为实参的一个副本。值传递的特点是被调函数对形参的任何操作都是作为局部变量进行,不会影响主函数实参的值。
引用传递:引用传递过程中,被调函数的形参虽然也作为局部变量在该函数栈中开辟了内存空间,但这时存放的是由主函数传递进来的实参的地址。被调函数的形对形参的任何操作都被处理成间接寻址,即通过在栈中存放的地址访问主函数中的实参变量。因此,被调函数对形参做的任何操作都会影响主函数中的实参变量。
在C语言中,值传递是唯一可用的参数传递机制。但这时我们就要问,不是还有指针的地址传递吗?由于受指针变量作为函数参数的影响,有许多朋友认为指针作参数传递和引用传递一样。这是错误的。请看下面的代码:
int swap(int *x, int *y)
{
int temp;
temp = *x;
*x = *y;
*y = temp;
return temp;
}
int main()
{
int a = 1, b = 2;
int *p1 = &a;
int *p2 = &b;
swap(p1, p2);
printf("a=%d b=%d\n", a, b);
return 0;
}
- 函数swap以两个指针变量作为参数,当main()调用swap时,是以值传递的方式将指针变量p1、p2的值(也就是变量a、b的地址)放在了swap在堆栈中为形式参数x、y开辟的内存单元中。这一点从以下的汇编代码可以看出:
//main函数汇编
int main()
{
00271420 push ebp
00AA1421 mov ebp,esp
00AA1423 sub esp,0F4h //主函数栈桢空间总大小f4 --->244
00AA1429 push ebx
00AA142A push esi
00AA142B push edi
00AA142C lea edi,[ebp-0F4h]
00AA1432 mov ecx,3Dh
00AA1437 mov eax,0CCCCCCCCh //将内存空间循环赋予初始值cccccccc
00AA143C rep stos dword ptr es:[edi]
00AA143E mov eax,dword ptr ds:[00AA8000h]
00AA1443 xor eax,ebp
00AA1445 mov dword ptr [ebp-4],eax
int a = 10, b = 20;
00AA1448 mov dword ptr [a],0Ah //a内存空间放入10
00AA144F mov dword ptr [b],14h //b内存空间放入20
int *p1 = &a;
00AA1456 lea eax,[a] //将a的地址放入eax
00AA1459 mov dword ptr [p1],eax //将eax里面的值放入p1内存空间中,下面类似
int *p2 = &b;
00AA145C lea eax,[b]
00AA145F mov dword ptr [p2],eax
swap(p1, p2);
00AA1462 mov eax,dword ptr [p2] //参数p2的值进栈
00AA1465 push eax
00AA1466 mov ecx,dword ptr [p1] //参数p1的值进栈
00AA1469 push ecx
00AA146A call swap (0AA100Fh) //调用swap函数
00AA146F add esp,8 //清理栈中的参数
printf("a=%d b=%d\n", a, b);//下面是printf函数的汇编
00AA1472 mov esi,esp
00AA1474 mov eax,dword ptr [b]
00AA1477 push eax
00AA1478 mov ecx,dword ptr [a]
00AA147B push ecx
00AA147C push 0AA5858h
00AA1481 call dword ptr ds:[0AA9114h]
00AA1487 add esp,0Ch
00AA148A cmp esi,esp
00AA148C call __RTC_CheckEsp (0AA113Bh)
return 0;
00AA1491 xor eax,eax
}
- 阅读上述代码要注意,INTEL80x86系列的CPU对堆栈的处理是向下生成,即从高地址单元向低地址单元生成。从上面的汇编代码可知,main()在调用swap之前,先将实参的值按从右至左的顺序压栈,即先p2进栈,再p1进栈。调用结束之后,主调函数main()负责清理堆栈中的参数。Swap 将使用这些进入堆栈的变量值。
//swap函数的汇编代码:
int swap(int *x, int *y)
{
002713C0 push ebp
002713C1 mov ebp,esp
002713C3 sub esp,0CCh//swap函数栈桢空间大小cc ---> 204
002713C9 push ebx
002713CA push esi
002713CB push edi
002713CC lea edi,[ebp-0CCh]
00AA13D2 mov ecx,33h
00AA13D7 mov eax,0CCCCCCCCh//将内存空间循环赋予初始值cccccccc
00AA13DC rep stos dword ptr es:[edi]
int temp;
temp = *x;
00AA13DE mov eax,dword ptr [x]//操作已存放在堆栈中的p1,将p1置入eax
00AA13E1 mov ecx,dword ptr [eax]//通过寄存器间址将*p1置入ecx
00AA13E3 mov dword ptr [temp],ecx//经由ecx将*p1置入temp变量的内存单元。以下类似
*x = *y;
00AA13E6 mov eax,dword ptr [x]
00AA13E9 mov ecx,dword ptr [y]
00AA13EC mov edx,dword ptr [ecx]
00AA13EE mov dword ptr [eax],edx
*y = temp;
00AA13F0 mov eax,dword ptr [y]
00AA13F3 mov ecx,dword ptr [temp]
00AA13F6 mov dword ptr [eax],ecx
}
由上述汇编代码基本上说明了C语言中值传递的原理,只不过传递的是指针的值。本文后面还要论述使用引用传递的swap函数。从这些汇编代码分析,这里我们可以得到以下几点:
- 进程的堆栈存储区是主调函数和被调函数进行通信的主要区域。
- C语言中参数是从右向左进栈的。
- 被调函数使用的堆栈区域结构为:局部变量(如temp)、返回地址、函数参数、低地址 、高地址。
- 由主调函数在调用后清理堆栈。
函数的返回值一般是放在寄存器中的。
这里尚需补充说明几点:一是参数进栈的方式。对于内部类型,由于编译器知道各类型变量使用的内存大小故直接使用push指令;对于自定义的类型(如structure),采用从源地址向目的(堆栈区)地址进行字节传送的方式入栈。二是函数返回值为什么一般放在寄存器中,这主要是为了支持中断;如果放在堆栈中有可能因为中断而被覆盖。三是函数的返回值如果很大,则从栈向存放返回值的地址单元(由主调函数在调用前将此地址压栈提供给被调函数)进行字节传送,以达到返回的目的。如果在被调函数中返回局部变量的地址是毫无意义的;因为局部变量存于栈中,调用结束后栈将被清理,这些地址就变得无效了。
C++既有C的值传递又有引用传递。在值传递上与C一致,这里着重说明引用传递。如本文前面所述,引用传递就是传递变量的地址到被调函数使用的堆栈中。在C++中声明引用传递要使用”&”符号,而调用时则不用。
代码:
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);
}
main函数汇编
void main()
{
……
……
int a = 1, b = 2;
00401088 mov dword ptr [ebp-4],1 ;变量a
0040108F mov dword ptr [ebp-8],2 ;变量b
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 //清理堆栈中的参数
……
……
}
- 可以看出,main函数在调用swap2之前,按照从右至左的顺序将b和a的偏移地址压栈,这就是在传递变量的地址swap(int &x, int &y)
//swap函数汇编
int& swap2(int& x, int& y)
{
00401030 push ebp
00401031 mov ebp,esp
……
……
int temp;
temp = x;
00401048 mov eax,dword ptr [ebp+8]
0040104B mov ecx,dword ptr [eax]
0040104D mov dword ptr [ebp-4],ecx
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
y = temp;
0040105A mov edx,dword ptr [ebp+0Ch]
0040105D mov eax,dword ptr [ebp-4]
00401060 mov dword ptr [edx],eax
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将不能出现在”=”的左边。