C/C++的参数传递机制(一)

2015-01-25 19:28:09 · 作者: · 浏览: 19
近来公司招人较多,由此面试了非常多的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传参规范的,不在本文讨论范围)
?
下面是测试代码
?
?
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
? ?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(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的变量被分成2个32Bit的参数压入栈中。这也是我们常说的,值传递会形成一个拷贝。如果是一个自定义的结构类型,并且有很多参数,那么如果用值传递,这个结构体将被分割为非常多个32Bit的逐个拷贝到栈中去,这样的参数传递效率是非常慢的。所以结构体等自定义类型,都使用引用传递,如果不希望别人修改结构体变量,可以加上const修饰,如(const MY_STRUCT& ?_value);
?