解疑: 有关函数参数传递的问题

Stack

可能不少朋友刚开始学C的时候和我一样都曾想过这些问题,为此我做了一点简单的分析,供有疑问的同学参考。


问题1: v++与++v的区别

首先举两个栗子:

源码:

1
2
3
4
5
6
7
#include <stdio.h>
int main(void)
{
int v = 1;
printf("%d,%d,%d\n", v++, v++, v++);
return 0;
}

输出结果: 3,2,1

1
2
3
4
5
6
7
#include <stdio.h>
int main(void)
{
int v = 1;
printf("%d,%d,%d\n", ++v, ++v, ++v);
return 0;
}

输出结果: 4,4,4

为什么第一个输出3,2,1而不是1,2,3?而第二个结果都相等?请看下文

反汇编:

通过汇编代码可以很清晰的看到其硬件操作,对解决这个问题有很大帮助。注: 我用的是VC编译系统, 不同的编译器结果可能会不同,下一节有提到。

v++:

Assembly1

  1. 首先 mov dword ptr[v], 1 给 v 赋初值 1
  2. mov eax, dword ptr[v] 把 v 的值传给寄存器 eax
  3. mov dword ptr[ebp-0D0h], eax eax 的值再放入 ebp-0D0h 地址对应的4字节空间, 也就是 i 的初值1
  4. 看倒数第8行: mov eax, dword ptr[ebp-0D0h] eax 的值变回 ebp-0D0h 所对应的1
  5. 最后 push eax 第一个把eax压入栈内,最后弹出来,所以这是printf()的倒数第1个参数,也是第3个输出的值:1
  6. 同理,
    mov ecx, dword ptr[v]
    add ecx, 1
    mov dword ptr[v], ecx
    使 v 自增1
  7. 接下来
    mov edx,dword ptr[v]
    mov edx, dword ptr[ebp-0D4h]
    把v的值2传给ebp-0D4h对应的内存空间
  8. 倒数第6行 mov ecx, dword ptr[ebp-0D4h] 然后 push ecx 把ecx压入栈,printf()得到倒数第2个参数,所以第二个输出的值为 2
  9. 最后入栈的3第一个出栈
  10. printf()的后面三个参数分别是3,2,1,所以输出结果也是如此

++v:

Assembly2

  1. 第1行到第9行的一大堆指令就是让i自增3次变成4
  2. 这时i的值为4,分别mov传给eax,ecx,edx最后push,所以printf()得到的3个参数都为4。这是输出 4, 4, 4 的原因

得到结论: 函数的三个参数v++,v++,v++是分别压入v+2,v+1,v+0的值到栈内,而++v,++v,++v会先对v自加3次,最后压栈的是v+3的值。


问题2: 参数压栈的顺序问题

与上面的一样,这里自己定义一个函数,看上去更直观一点。

1
2
3
4
5
6
7
8
#include <stdio.h>
void function(int a, int b)
{ }
int main()
{
int v = 1;
function(v++, v++);
}

根据上一节的结论,v++压入堆栈的是不同的值,但是如果你认为形参a 与 b的值是12那就错了,正确答案是21

之所以是这样,是因为这里的入栈顺序是 从右往左 ,并且由于栈的特性,最先push进去的最后一个pop出来。故首先push v++也就是v的值,接着push 的是v+1,出栈时第一个pop 最后 push 的值,得到2,然后再pop 得到1。

有的人肯定会想,是不是所有的情况下函数参数传递都是从右往左的? 以下是我查了资料后的整理:

函数的参数传递有4种途径:

  1. 寄存器传递 (由于寄存器的个数是有限的,不常见)

  2. 约定存储单元传递

  3. 利用堆栈传递

  4. 利用CALL后续区传递

据说C/C++的函数参数压栈顺序是从右往左,而pascal是从左往右。

除此之外,不同的调用约定压栈顺序不同,C/C++的默认调用约定是__cdecl,系统API中__stdcall比较常见,顺序都是上面所说的右到左,另外还有个__fastcall,它的前两个参数通过寄存器传递(从左至右)。

希望对部分有着同样问题的同学有帮助。

Copyright © 雪峰 2015