C函数参数传递与返回值传递

本文详细解析了C语言中__stdcall和__cdecl两种函数调用约定的参数传递方式,通过实例展示了参数压栈及堆栈清理的过程。同时,文章探讨了函数返回值传递,指出对于大尺寸对象,返回时可能会产生额外的拷贝,导致性能下降,并提到了返回值优化(RVO)技术来减少拷贝开销。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

(1)参数传递

       __stdcall和__cdecl都是函数调用约定关键字,先给出这两者的区别,然后举实例分析:

  __stdcall:参数由右向左压入堆栈;堆栈由函数本身清理。

    __cdecl:参数也是由右向左压入堆栈;但堆栈由调用者清理。

  另外,这两者在同一名字修饰约定下,编译过后变量和函数的名字也不一样,具体见另一博文:名字修饰约定extern "C"与extern "C++"浅析


  下面给出实例分析:

[cpp]  view plain  copy
  1. #include "stdio.h"  
  2. #include <iostream>  
  3. #include <Windows.h>  
  4. #include <conio.h>  
  5.   
  6. using namespace std;  
  7.   
  8. int __stdcall Func_stdcall(int nParam1, int nParam2)  
  9. {  
  10.     return 1;  
  11. }  
  12.   
  13. int __cdecl Func_cdecl(int nParam1, int nParam2)  
  14. {  
  15.     return 1;  
  16. }  
  17.   
  18. int main()    
  19. {  
  20.     int a = Func_stdcall(1, 2);  
  21.   
  22.     a = Func_cdecl(1, 2);  
  23.   
  24.     return 0;    
  25. }   

  以上代码在XP + VC++6.0 SP6环境下编译,编译后的汇编代码如下:


  首先要明确上图汇编代码中几个指令的作用:

  1.call:将call下一条指令的EIP压入堆栈,然后跳到@后标号地址处执行;EIP相当于保存着当前程序的计数器值。

  2.ret:将堆栈的当前数据弹出给EIP,然后继续执行;

  3.ret n:n表示一个整数,将堆栈的当前数据弹出给EIP,再将ESP的值加上n,然后继续执行。

  我们再看汇编代码,调用Func_stdcall和Func_cdecl时,都是由调用者(main函数)将参数压入堆栈,注意地址0x00401127、0x00401129和0x00401133、0x00401135都是先压入2,再压入1,这个顺序就是函数参数由右向左的顺序。

  再注意地址0x0040110F,这是调用Func_stdcall时的出口指令,"ret 8"先把EIP的值弹出,然后再将ESP的值加8,相当于执行两次出栈的操作。因为编译环境是32位的,调用Func_stdcall时压入的2和1,其实是压入的两个32位整数值,刚好占8个字节。然后再继续执行EIP处的指令,此时EIP的值应为0x00401130,为call指令的下一条指令,这条指令是将返回的值赋给变量a。可见,堆栈的清理是由Func_stdcall内部处理的,外部调用者并不处理。

  然后再来看看__cdecl修饰的Func_cdecl,注意地址0x0040111B,只有一个指令“ret”,只将堆栈当前的值弹出给EIP,然后继续执行。但是在调用前已经压入了两个32位的整数值,堆栈还没有被清理。我们再来看看继续执行的指令,地址0x0040113C处的指令为继续执行的指令,指令为“add esp,8“,这个很好理解了,直接将esp的值加上8,也相当于执行两次出栈操作。但这是由调用者(main参数)进行的,因此堆栈是由调用者进行清理的。

      __stdcall通常用于Windows API中,可见如下代码:

[cpp]  view plain  copy
  1. #define CALLBACK    __stdcall  
  2. #define WINAPI      __stdcall  
  3. #define WINAPIV     __cdecl  
  4. #define APIENTRY    WINAPI  
  5. #define APIPRIVATE  __stdcall  
  6. #define PASCAL      __stdcall  
  7. #define cdecl       _cdecl  
  8.   
  9. #ifndef CDECL  
  10. #define CDECL       _cdecl  
  11. #endif  

  而C和C++程序的缺省调用方式则为__cdecl,下图为VC++6.0的默认设置,因此在不显式写明调用约定的情况下,一般都是采用__cdecl方式,而在与Windows API打交道的场景下,通常都是显式的写明使用__stdcall,才能与Windows API保持一致。



  另外,还要注意的是,如printf此类支持可变参数的函数,由于不知道调用者会传递多少个参数,也不知道会压多少个参数入栈,因此函数本身内部不可能清理堆栈,只能由调用者清理了。


(2)函数返回值传递

出自《程序员的自我修养-链接、装载与库》P299

eax是函数传递返回值的一个通道。

1.对于小于4个字节的数据函数将返回值存储在eax中。

2.5~8个字节对象的情况调用惯例都是采用eax和edx的联合返回方式进行。

3.大于8个字节的返回类型,用一下代码测试:

复制代码
 1 typedef struct big_thing
 2 {
 3     char buf[128];
 4 }big_thing;
 5 
 6 big_thing return_test()
 7 {
 8     big_thing b;
 9     b.buf[] = 0;
10     return b;
11 }
12 
13 int main()
14 {
15     big_thing n = return_test();
16 }
复制代码
  • 首先main函数在栈额外开辟了一片空间,并将这块空间的一部分作为传递返回值的临时对象,这里称为temp
  • 将temp对象的地址作为隐藏参数传递个return_test函数
  • return_test 函数将数据拷贝给temp对象,并将temp对象的地址用eax传出。
  • return_test返回以后,mian函数将eax 指向的temp对象的内容拷贝给n。

如果返回值的类型的尺寸太大,c语言在函数的返回时会使用一个临时的栈上内存作为中转,结果返回值对象会被拷贝两次。因而不到万不得已,不要轻易返回大尺寸对象。

再来看看函数返回一个C++对象会如何:

复制代码
 1 #include <iostream>
 2 using namespace std;
 3 
 4 struct cpp_obj
 5 {
 6     cpp_obj()
 7     {
 8         cout << "ctor\n";
 9     }
10 
11     cpp_obj(const cpp_obj& c)
12     {
13         cout << "copy ctor\n";
14     }
15 
16     cpp_obj& opearator=(const cpp_obj& rhs)
17     {
18         cout << "operator = \n";
19         return *this;
20     }
21 
22     ~cpp_obj()
23     {
24         cout << "dtor\n";
25     }
26 };
27 
28 cpp_obj return_test()
29 {
30     cpp_obj b;
31     cout << "before return\n";
32     return b;
33 }
34 int main()
35 {
36     cpp_obj n;
37     n = return_test();
38 }
复制代码

运行后的输出结果可以得出:函数返回之后,进行了一个拷贝函数的调用,以及一次operator=的调用,也就是说,仍然产生了两次拷贝。因此C++的对象同样会产生临时对象。

在这段代码中我们还能看到在c++返回一个对象时,对象要经过两次拷贝构造函数的调用才能够完成返回对象的传递,1次拷贝到栈上的临时对象里,另一次把临时对象拷贝到存储返回值的对象里。在某些编译器里,返回一个对象甚至要经过更多的步骤。

为了减少返回对象的开销,C++提出了返回值优化(RVO)技术,可以将某些场合下的对象拷贝减少一次,例如:

1 cpp_obj return_test()
2 {
3     return cpp_obj();
4 }

目的是直接将对象的构造在传出时使用的临时对象上,减少一次复制过程。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值