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

指針傳遞和引用傳遞spa

// 下面看一下對應的反彙編的代碼(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);在實際上的彙編代碼也基本上如出一轍,都是從棧中取出地址來。由此能夠看出引用和指針在效率上是同樣的。這也是爲何指針和引用均可以達到多態的效果。指針傳遞和引用傳遞其實都是改變的地址指向的內存上的值來達到修改參數的效果。指針

值傳遞code

下面是值傳遞對應的反彙編代碼blog

// 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函數對應的反彙編代碼的參數傳遞it

__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.

總之,不管是哪一種參數傳遞方式,參數都是經過棧上的臨時變量來間接參與到被調用函數的。指針做爲參數,其自己的值是不可能被改變的,可以改變的是其指向的內容。引用是經過指針來實現的,因此引用和指針在效率上同樣的。

相關文章
相關標籤/搜索