您的位置:首页 > 其它

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

2016-09-25 17:54 260 查看
(1)参数传递

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

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

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

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

  下面给出实例分析:

[cpp] view
plain copy

#include "stdio.h"  

#include <iostream>  

#include <Windows.h>  

#include <conio.h>  

  

using namespace std;  

  

int __stdcall Func_stdcall(int nParam1, int nParam2)  

{  

    return 1;  

}  

  

int __cdecl Func_cdecl(int nParam1, int nParam2)  

{  

    return 1;  

}  

  

int main()    

{  

    int a = Func_stdcall(1, 2);  

  

    a = Func_cdecl(1, 2);  

  

    return 0;    

}   

  以上代码在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

#define CALLBACK    __stdcall  

#define WINAPI      __stdcall  

#define WINAPIV     __cdecl  

#define APIENTRY    WINAPI  

#define APIPRIVATE  __stdcall  

#define PASCAL      __stdcall  

#define cdecl       _cdecl  

  

#ifndef CDECL  

#define CDECL       _cdecl  

#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 }


目的是直接将对象的构造在传出时使用的临时对象上,减少一次复制过程。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: