c中函数参数传递

1 输入参数传递数组或地址

测试代码:

 1 #include <stdio.h>
 2 
 3 void foo(char *a)
 4 {
 5     fprintf(stdout, "%x %x %x\n", &a, a, a[0]);
 6 }
 7 
 8 int main ( int argc, char *argv[] )
 9 {
10     char a[20] = {'a', 'b', 'c'};
11 
12     fprintf(stdout, "%x %x %x\n", &a, a, a[0]);
13     foo(a);
14     return 0;
15 }               /* ----------  end of function main  ---------- */

运行结果:

bfc5f31c bfc5f31c 61
bfc5f2f0 bfc5f31c 61

  运行结果的第二列和第三列都比较好理解,a仅仅是个地址,在main中并没有专门为a开辟一个空间来存放a这个地址,也可以说a就是一个地址的别名,它和指针不一样,指针是一个变量,只是这个变量里面存放的是地址而已,所以在main函数中对a取地址其实是没什么意义的,因为它本来就是个地址,所以编译器仅仅是把a输出。但当以a为参数调用foo函数时,一边我们都认为把一个地址传给了foo,foo里的a和main中的a是一样的。其实不然,foo里的a是一个指针变量,main中调用foo时,其实就是把main中a的值赋值给了foo中的a变量。所以本质上来讲,传地址和传值是没区别的,其实都是传值,区别在于这个值是不是个地址而已。不过c++的引用就不一样了,它是真正的对传递的参数取个别名,所以和传地址是不一样的。

  我们可以反汇编来看看上面的程序实际运行是怎么样的,先看main函数:

08048422 <main>:
 8048422:       55                      push   %ebp
 8048423:       89 e5                   mov    %esp,%ebp
 8048425:       83 e4 f0                and    $0xfffffff0,%esp
 8048428:       83 ec 40                sub    $0x40,%esp
 804842b:       c7 44 24 2c 00 00 00    movl   $0x0,0x2c(%esp)
 8048432:       00
 8048433:       c7 44 24 30 00 00 00    movl   $0x0,0x30(%esp)
 804843a:       00
 804843b:       c7 44 24 34 00 00 00    movl   $0x0,0x34(%esp)
 8048442:       00
 8048443:       c7 44 24 38 00 00 00    movl   $0x0,0x38(%esp)
 804844a:       00
 804844b:       c7 44 24 3c 00 00 00    movl   $0x0,0x3c(%esp)
 8048452:       00
 8048453:       c6 44 24 2c 61          movb   $0x61,0x2c(%esp)
 8048458:       c6 44 24 2d 62          movb   $0x62,0x2d(%esp)
 804845d:       c6 44 24 2e 63          movb   $0x63,0x2e(%esp)
 8048462:       0f b6 44 24 2c          movzbl 0x2c(%esp),%eax
 8048467:       0f be c8                movsbl %al,%ecx
 804846a:       ba 84 85 04 08          mov    $0x8048584,%edx
 804846f:       a1 c0 97 04 08          mov    0x80497c0,%eax
 8048474:       89 4c 24 10             mov    %ecx,0x10(%esp)
 8048478:       8d 4c 24 2c             lea    0x2c(%esp),%ecx
 804847c:       89 4c 24 0c             mov    %ecx,0xc(%esp)
 8048480:       8d 4c 24 2c             lea    0x2c(%esp),%ecx
 8048484:       89 4c 24 08             mov    %ecx,0x8(%esp)
 8048488:       89 54 24 04             mov    %edx,0x4(%esp)
 804848c:       89 04 24                mov    %eax,(%esp)
 804848f:       e8 8c fe ff ff          call   8048320 <fprintf@plt>
 8048494:       8d 44 24 2c             lea    0x2c(%esp),%eax
 8048498:       89 04 24                mov    %eax,(%esp)
 804849b:       e8 44 ff ff ff          call   80483e4 <foo>
 80484a0:       b8 00 00 00 00          mov    $0x0,%eax
 80484a5:       c9                      leave
 80484a6:       c3                      ret

  函数中一些惯例语句我们直接跳过,我在http://www.cnblogs.com/chengxuyuancc/archive/2013/05/28/3104769.html中已经分析过了,我们直接看其它部分。首先编译器为数组char  a[20]在栈中分配空间,从804842b到804844b可以看出编译器为数组分配的空间是0x2c(%esp)~0x3c(%esp),这段代码主要是对数组清零。8048453~804848c主要是为调用函数fprintf,将参数压入栈中,从代码可以看出参数是从右向左依次压入栈的。8048494~8048498获取a的值并将a的值放入栈顶。

foo函数的反汇编代码:

080483e4 <foo>:
 80483e4:       55                      push   %ebp
 80483e5:       89 e5                   mov    %esp,%ebp
 80483e7:       53                      push   %ebx
 80483e8:       83 ec 24                sub    $0x24,%esp
 80483eb:       8b 45 08                mov    0x8(%ebp),%eax
 80483ee:       0f b6 00                movzbl (%eax),%eax
 80483f1:       0f be d8                movsbl %al,%ebx
 80483f4:       8b 4d 08                mov    0x8(%ebp),%ecx
 80483f7:       ba 84 85 04 08          mov    $0x8048584,%edx
 80483fc:       a1 c0 97 04 08          mov    0x80497c0,%eax
 8048401:       89 5c 24 10             mov    %ebx,0x10(%esp)
 8048405:       89 4c 24 0c             mov    %ecx,0xc(%esp)
 8048409:       8d 4d 08                lea    0x8(%ebp),%ecx
 804840c:       89 4c 24 08             mov    %ecx,0x8(%esp)
 8048410:       89 54 24 04             mov    %edx,0x4(%esp)
 8048414:       89 04 24                mov    %eax,(%esp)
 8048417:       e8 04 ff ff ff          call   8048320 <fprintf@plt>
 804841c:       83 c4 24                add    $0x24,%esp
 804841f:       5b                      pop    %ebx
 8048420:       5d                      pop    %ebp
 8048421:       c3                      ret

  在80483f4行中0x8(%ebp)指向的就是函数foo中的参数a的存储空间,正如前面所说的,foo中的a是一个指针变量,里面存放的是main中传过来的数组的地址。8048409则是获得a的地址值。

  从汇编代码中我们可以直观的看到main中的a实际是一个地址的别名,它不占用存储空间,而它以参数传递给foo时,foo的接收参数a是有存储空间的

 2 输入参数传递结构体

测试代码:

 1 #include <stdio.h>
 2 
 3 typedef struct test_p
 4 {
 5     char a[20];
 6 }test_p;
 7 
 8 void foo(test_p a)
 9 {
10     a.a[0] = 2;
11     printf("%d\n", a.a[0]);
12 }
13 
14 int main ( int argc, char *argv[] )
15 {
16     test_p a;
17 
18     a.a[0] = 1;
19     foo(a);
20     printf("%d\n", a.a[0]);
21     return 0;
22 }               /* ----------  end of function main

运行结果:

2
1

  这个结果是很容易理解的,由于上面属于传值调用,也就是直接把main中的test_p结构体a拷贝一份赋值给foo中的test_p结构体a,从反汇编的代码中也可以看出实际也是这样的。

 3 输出参数传递结构体

  如果函数有返回值,一般传出参数放在寄存器eax中,从第一个例子中main函数的反汇编代码可以看出,main在返回之前将0赋值给了eax寄存器作为函数返回值。但如果返回值比较大,eax装不了怎么办?我们可以看看下面输出参数为结构体的情况。

测试代码:

 1 #include <stdio.h>
 2 
 3 typedef struct test_p
 4 {
 5     char a[20];
 6 }test_p;
 7 
 8 test_p foo()
 9 {
10     test_p a;
11     a.a[0] = 1;
12     return a;
13 }
14 
15 int main ( int argc, char *argv[] )
16 {
17     test_p a;
18 
19     a = foo();
20     return 0;
21 }               /* ----------  end of function main  ---------- */

反汇编代码:

08048394 <foo>:
 8048394:       55                      push   %ebp
 8048395:       89 e5                   mov    %esp,%ebp
 8048397:       83 ec 20                sub    $0x20,%esp
 804839a:       c6 45 ec 01             movb   $0x1,-0x14(%ebp)
 804839e:       8b 45 08                mov    0x8(%ebp),%eax
 80483a1:       8b 55 ec                mov    -0x14(%ebp),%edx
 80483a4:       89 10                   mov    %edx,(%eax)
 80483a6:       8b 55 f0                mov    -0x10(%ebp),%edx
 80483a9:       89 50 04                mov    %edx,0x4(%eax)
 80483ac:       8b 55 f4                mov    -0xc(%ebp),%edx
 80483af:       89 50 08                mov    %edx,0x8(%eax)
 80483b2:       8b 55 f8                mov    -0x8(%ebp),%edx
 80483b5:       89 50 0c                mov    %edx,0xc(%eax)
 80483b8:       8b 55 fc                mov    -0x4(%ebp),%edx
 80483bb:       89 50 10                mov    %edx,0x10(%eax)
 80483be:       8b 45 08                mov    0x8(%ebp),%eax
 80483c1:       c9                      leave
 80483c2:       c2 04 00                ret    $0x4

080483c5 <main>:
 80483c5:       55                      push   %ebp
 80483c6:       89 e5                   mov    %esp,%ebp
 80483c8:       83 ec 24                sub    $0x24,%esp
 80483cb:       8d 45 ec                lea    -0x14(%ebp),%eax
 80483ce:       89 04 24                mov    %eax,(%esp)
 80483d1:       e8 be ff ff ff          call   8048394 <foo>
 80483d6:       83 ec 04                sub    $0x4,%esp
 80483d9:       b8 00 00 00 00          mov    $0x0,%eax
 80483de:       c9                      leave
 80483df:       c3                      ret

  从反汇编的main函数中可以看出,在调用foo之前给foo传递了结构体变量a的地址。在foo函数在804839e~80483bb代码中将自己本地结构体变量的值赋给foo传递过来的变量,并将foo传递过来的变量的地址赋值给寄存器eax。也就是说main中的语句a = foo()其实就相当于语句foo(&a)。这样就很好的解决了返回参数过大的问题。

  上面讲的三种参数传递的情况算是c中较复杂的情况了,其它的情况都是同样的道理。从反汇编的代码中可以看出编译器为我们写的c代码做了很多优化的工作,就如上面的第三种情况,开始我认为foo函数在返回的时候应该另外开辟一段临时空间用以存放变量a的值,由于foo函数返回后变量a的空间就被释放了,在回到main函数后再将临时空间中存放的值赋值给a,不过编译器却巧妙的将a变量的地址传给foo函数,这样就节省了空间和时间。

原文地址:https://www.cnblogs.com/chengxuyuancc/p/3107054.html