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函数,这样就节省了空间和时间。