天天看點

C/C++的參數傳遞機制

近來公司招人較多,由此面試了非常多的C++程式員。面試時,我都會問到參數傳遞的相關問題,尤其側重指針。因為指針畢竟是C/C++最重要的一個優勢(在某種情況下也可以說是劣勢)。但其結果是,1/3的人基本上講錯了,1/3的知其然卻不知其是以然。是以我覺得有必要把這些知識點梳理下,分享出來。(下面的讨論都是基于VS和GCC的預設編譯方式,其他特殊編譯方式不在本文作用範圍内。)

C/C++函數參數的傳遞方式有三種:值傳遞(pass by value)、指針傳遞(pass bypointer)、引用傳遞(pass by reference)。

C/C++函數參數的傳遞通道是通過堆棧傳遞,預設遵循__cdecl(C聲明方式),參數由調用者從右往左逐個壓入堆棧,在函數調用完成之後再由調用者恢複堆棧。(Win32API遵循stdcall傳參規範的,不在本文讨論範圍)

下面是測試代碼

指針傳遞和引用傳遞

通過上面的反彙編代碼,我們可以看出指針傳遞和引用傳遞在機制是一樣的,都是将指針值(即位址)壓入棧中,調用函數,然後恢複棧。Swap(nA, nB)和Swap(&nA, &nB);在實際上的彙編代碼也基本上一模一樣,都是從棧中取出位址來。由此可以看出引用和指針在效率上是一樣的。這也是為什麼指針和引用都可以達到多态的效果。指針傳遞和引用傳遞其實都是改變的位址指向的記憶體上的值來達到修改參數的效果。

值傳遞

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

因為我的機器是32位的CPU,從上面的彙編代碼可以看64Bit的變量被分成2個32Bit的參數壓入棧中。這也是我們常說的,值傳遞會形成一個拷貝。如果是一個自定義的結構類型,并且有很多參數,那麼如果用值傳遞,這個結構體将被分割為非常多個32Bit的逐個拷貝到棧中去,這樣的參數傳遞效率是非常慢的。是以結構體等自定義類型,都使用引用傳遞,如果不希望别人修改結構體變量,可以加上const修飾,如(const MY_STRUCT&  _value);

下面來看一下Test001函數對應的反彙編代碼的參數傳遞

從上面的彙編代碼可以看出,其實是0被壓入到棧中作為參數,是以GetMemory(_pArray)無論做什麼事,其實都與指針變量_pArray無關。GetMemory()配置設定的空間是讓棧中的臨時變量指向的,當函數退出時,棧得到恢複,結果申請的空間沒有人管,就産生記憶體洩露的問題了。《C++ Primer》将參數傳遞分為引用傳遞和非引用傳遞兩種,非引用傳遞其實可以了解為值傳遞。這樣看來,指針傳遞在某種意義上也是值傳遞,因為傳遞的是指針的值(1個4BYTE的值)。值傳遞都不會改變傳入實參的值的。而且普通的指針傳遞其實是改變的指針變量指向的内容。

下面再看一下Test002函數對應的反彙編代碼的參數傳遞

從上面的彙編代碼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即相當于把配置設定的空間指針eax讓edx指向,也即讓_pArray指向配置設定的空間eax.

總之,無論是哪種參數傳遞方式,參數都是通過棧上的臨時變量來間接參與到被調用函數的。指針作為參數,其本身的值是不可能被改變的,能夠改變的是其指向的内容。引用是通過指針來實作的,是以引用和指針在效率上一樣的。

繼續閱讀