开发者社区> 杰克.陈> 正文

C/C++的参数传递机制

简介: 原文:C/C++的参数传递机制 近来公司招人较多,由此面试了非常多的C++程序员。面试时,我都会问到参数传递的相关问题,尤其侧重指针。因为指针毕竟是C/C++最重要的一个优势(在某种情况下也可以说是劣势)。
+关注继续查看
原文:C/C++的参数传递机制

近来公司招人较多,由此面试了非常多的C++程序员。面试时,我都会问到参数传递的相关问题,尤其侧重指针。因为指针毕竟是C/C++最重要的一个优势(在某种情况下也可以说是劣势)。但其结果是,1/3的人基本上讲错了,1/3的知其然却不知其所以然。所以我觉得有必要把这些知识点梳理下,分享出来。(下面的讨论都是基于VSGCC的默认编译方式,其他特殊编译方式不在本文作用范围内。)

C/C++函数参数的传递方式有三种:值传递(pass by value)、指针传递(pass bypointer)、引用传递(pass by reference)。

C/C++函数参数的传递通道是通过堆栈传递,默认遵循__cdecl(C声明方式),参数由调用者从右往左逐个压入堆栈,在函数调用完成之后再由调用者恢复堆栈。(Win32API遵循stdcall传参规范的,不在本文讨论范围)

下面是测试代码

void Swap(__int64* _pnX, __int64* _pnY)
{
    __int64 nTemp = *_pnX;
    *_pnX = *_pnY;
    *_pnY = nTemp;
}

void Swap(__int64& _nX, __int64& _nY)
{
    __int64 nTemp = _nX;
    _nX = _nY;
    _nY = nTemp;
}

void SetValue(__int64 _nX)
{
    __int64 nTemp = _nX;
}

// Test001
void GetMemory(__int64* _pBuff)
{
    _pBuff = new __int64[4];
}

// Test002
void GetMemory(__int64** _ppBuff)
{
    *_ppBuff = new __int64[4];
}

int _tmain(int argc, _TCHAR* argv[])
{
    __int64 nA = 0x10;
    __int64 nB = 0x20;

    // Test to pass by pointer
    Swap(&nA, &nB);

    // Test to pass by reference
    Swap(nA, nB);

    // Test to pass by value
    SetValue(nA);

    // Test the pointer that points the pointer
    __int64* _pArray = NULL;
    GetMemory(&_pArray);
    delete[] _pArray;
    _pArray = NULL;

    // Test the pointer 
    GetMemory(_pArray);

    return 0;
}

指针传递和引用传递

// 下面看一下对应的反汇编的代码(VS版)
__int64 nA = 0x10;
0041370E  mov         dword ptr [nA],10h 
00413715  mov         dword ptr [ebp-8],0 
__int64 nB = 0x20;
0041371C  mov         dword ptr [nB],20h 
00413723  mov         dword ptr [ebp-18h],0 

// Test to pass by pointer
Swap(&nA, &nB);
0041372A  lea         eax,[nB] 
0041372D  push        eax  
0041372E  lea         ecx,[nA] 
00413731  push        ecx  
00413732  call        Swap (4111E5h) 
00413737  add         esp,8 

// Test to pass by reference
Swap(nA, nB);
0041373A  lea         eax,[nB] 
0041373D  push        eax  
0041373E  lea         ecx,[nA] 
00413741  push        ecx  
00413742  call        Swap (4111E0h) 
00413747  add         esp,8 

// GCC版
   0x00401582 <+30>:    lea    eax,[esp+0x18]
   0x00401586 <+34>:    mov    DWORD PTR [esp+0x4],eax
   0x0040158a <+38>:    lea    eax,[esp+0x1c]
   0x0040158e <+42>:    mov    DWORD PTR [esp],eax
   0x00401591 <+45>:    call   0x401520 <Swap(int*, int*)>
   0x00401596 <+50>:    lea    eax,[esp+0x18]
   0x0040159a <+54>:    mov    DWORD PTR [esp+0x4],eax
   0x0040159e <+58>:    lea    eax,[esp+0x1c]
   0x004015a2 <+62>:    mov    DWORD PTR [esp],eax
   0x004015a5 <+65>:    call   0x401542 <Swap(int&, int&)>

通过上面的反汇编代码,我们可以看出指针传递和引用传递在机制是一样的,都是将指针值(即地址)压入栈中,调用函数,然后恢复栈。Swap(nA, nB)和Swap(&nA, &nB);在实际上的汇编代码也基本上一模一样,都是从栈中取出地址来。由此可以看出引用和指针在效率上是一样的。这也是为什么指针和引用都可以达到多态的效果。指针传递和引用传递其实都是改变的地址指向的内存上的值来达到修改参数的效果。

值传递

下面是值传递对应的反汇编代码

// Test to pass by value
SetValue(nA);
0041374A  mov         eax,dword ptr [ebp-8] 
0041374D  push        eax  
0041374E  mov         ecx,dword ptr [nA] 
00413751  push        ecx  
00413752  call        SetValue (4111EAh) 
00413757  add         esp,8 

因为我的机器是32位的CPU,从上面的汇编代码可以看64Bit的变量被分成232Bit的参数压入栈中。这也是我们常说的,值传递会形成一个拷贝。如果是一个自定义的结构类型,并且有很多参数,那么如果用值传递,这个结构体将被分割为非常多个32Bit的逐个拷贝到栈中去,这样的参数传递效率是非常慢的。所以结构体等自定义类型,都使用引用传递,如果不希望别人修改结构体变量,可以加上const修饰,如(const MY_STRUCT&  _value);

下面来看一下Test001函数对应的反汇编代码的参数传递

__int64* _pArray = NULL;
004137E0  mov         dword ptr [_pArray],0 
// Test the pointer 
GetMemory(_pArray);
00413812  mov         eax,dword ptr [_pArray] 
00413815  push        eax  
00413816  call        GetMemory (411203h) 
0041381B  add         esp,4 

从上面的汇编代码可以看出,其实是0被压入到栈中作为参数,所以GetMemory(_pArray)无论做什么事,其实都与指针变量_pArray无关。GetMemory()分配的空间是让栈中的临时变量指向的,当函数退出时,栈得到恢复,结果申请的空间没有人管,就产生内存泄露的问题了。《C++ Primer》将参数传递分为引用传递和非引用传递两种,非引用传递其实可以理解为值传递。这样看来,指针传递在某种意义上也是值传递,因为传递的是指针的值(14BYTE的值)。值传递都不会改变传入实参的值的。而且普通的指针传递其实是改变的指针变量指向的内容。

下面再看一下Test002函数对应的反汇编代码的参数传递

__int64* _pArray = NULL;
004137E0  mov         dword ptr [_pArray],0 
GetMemory(&_pArray);
004137E7  lea         eax,[_pArray] 
004137EA  push        eax  
004137EB  call        GetMemory (4111FEh) 

004137F0  add         esp,4 

从上面的汇编代码lea eax,[_pArray] 可以看出,_pArray的地址被压入到栈中去了。

然后看一看GetMemory(&_pArray)的实现汇编代码。

   0x0040159b <+0>:        push   ebp

   0x0040159c <+1>:        mov    ebp,esp

   0x0040159e <+3>:        sub    esp,0x18

   0x004015a1 <+6>:        mov    DWORD PTR [esp],0x20

   0x004015a8 <+13>:        call   0x473ef0 <_Znaj>

   0x004015ad <+18>:        mov    edx,DWORD PTR [ebp+0x8]

   0x004015b0 <+21>:        mov    DWORD PTR [edx],eax

   0x004015b2 <+23>:        leave 

   0x004015b3 <+24>:        ret  

蓝色的代码是分配临时变量空间,然后调用分配空间函数分配空间,得到的空间指针即eax.

然后红色的汇编代码即从ebp+0x8的栈上取到上面压入栈中的参数_pArray的地址.

mov DWORD PTR [edx],eax即相当于把分配的空间指针eaxedx指向,也即让_pArray指向分配的空间eax.

总之,无论是哪种参数传递方式,参数都是通过栈上的临时变量来间接参与到被调用函数的。指针作为参数,其本身的值是不可能被改变的,能够改变的是其指向的内容。引用是通过指针来实现的,所以引用和指针在效率上一样的。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
18723 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,阿里云优惠总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系.
25100 0
c/c++处理参数
直接上代码:涉及函数getopt(),getopt_long() 1 #include 2 #include 3 #include 4 #include 5 6 /* 7 int main(int argc, char *argv[]) 8 { 9 ...
950 0
+关注
杰克.陈
一个安静的程序猿~
10427
文章
2
问答
文章排行榜
最热
最新
相关电子书
更多
JS零基础入门教程(上册)
立即下载
性能优化方法论
立即下载
手把手学习日志服务SLS,云启实验室实战指南
立即下载