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传参规范的,不在本文讨论范围)

下面是测试代码

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

指针传递和引用传递

__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
<span style="font-weight: bold;">
</span>// 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&)><span style="font-weight: bold;">
</span>

通过上面的反汇编代码,我们可以看出指针传递和引用传递在机制是一样的,都是将指针值(即地址)压入栈中,调用函数,然后恢复栈。

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修饰,如(constMY_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》将参数传递分为引用传递和非引用传递两种,非引用传递其实可以理解为值传递。这样看来,指针传递在某种意义上也是值传递,因为传递的是指针的值(1个4BYTE的值)。值传递都不会改变传入实参的值的。而且普通的指针传递其实是改变的指针变量指向的内容。

下面再看一下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即相当于把分配的空间指针eax让edx指向,也即让_pArray指向分配的空间eax.

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

时间: 2024-11-06 21:23:43

C/C++的参数传递机制的相关文章

方法的参数传递机制(C#)

六 方法的参数传递机制 值参数,引用参数,输出参数 //参数的传递机制 using System; class Method { //值参数,传递的是数值本身,不改变外部变量的值 public static void ValueMethod(int i) { i++; } //引用参数,传递的是数据地址,直接对数据进行操作,原值要变化 //要注意的是string类型,赋值以后原值就不好改变了 public static void ReferenceMethod(ref int i) { i++;

深入剖析C/C++函数的参数传递机制

2014-07-29 20:16 深入剖析C/C++函数的参数传递机制 C语言的函数入口参数,可以使用值传递和指针传递方式,C++又多了引用(reference)传递方式.引用传递方式在使用上类似于值传递,而其传递的性质又象是指针传递,这是C++初学者经常感到困惑的.为深入介绍这三种参数传递方式,我们先把话题扯远些: 1. C/C++函数调用机制及值传递: 在结构化程序设计方法中,先辈们告诉我们,采用“自顶向下,逐步细化”的方法将一个现实的复杂问题分成多个简单的问题来解决.而细化到了最底层,就是

我的Java开发学习之旅------&gt;Java语言中方法的参数传递机制

实参:如果声明方法时包含来了形参声明,则调用方法时必须给这些形参指定参数值,调用方法时传给形参的参数值也被称为实参. Java的实参值是如何传入方法?这是由Java方法的参数传递机制来控制的,Java里方法的参数传递方式只有一种:值传递.所谓值传递,就是将实际参数的副本(复制品)传入方法内,而参数本身不会收到任何影响. 一.参数类型是原始类型的值传递 下面通过一个程序来演练 参数类型是原始类型的值传递的效果: public class ParamTransferTest { public sta

python中的*和**参数传递机制

python的参数传递机制具有值传递(int.float等值数据类型)和引用传递(以字典.列表等非值对象数据类型为代表)两种基本机制以及方便的关键字传递特性(直接使用函数的形参名指定实参的传递目标,如函数定义为def f(a,b,c),那么在调用时可以采用f(b=1,c=2,a=3)的指定形参目标的传递方式,而不必拘泥于c语言之类的形参和实参按位置对应) 除此之外,python中还允许包裹方式的参数传递,这未不确定参数个数和参数类型的函数调用提供了基础: def f(*a,**b) 包裹参数传递

Java中的参数传递机制

通过前一篇文章的介绍,我们从整体上明白了,Java类中变量的差异性.不同变量在内存中的存储位置,以及变量的生命周期等.今天,我们来看一下Java中参数传递的机制. 形参:方法声明时包含的参数声明 实参:调用方法时,实际传给形参的参数值 Java方法的参数传递机制: Java方法的参数传递只有一种:值传递.所谓值传递,就是将实际参数值的副本,传入方法内,而参数本身不会收到任何影响. PS:传入方法的时实际参数值的复制品,不管方法中对这个复制品如何操作,实际参数本身不会受到任何影响. 基本类型的参数

java参数传递机制浅析

Git Community Book 中文版书上,摘录如下: 一.基本 git rebase用于把一个分支的修改合并到当前分支. 假设你现在基于远程分支"origin",创建一个叫"mywork"的分支. $ git checkout -b mywork origin 假设远程分支"origin"已经有了2个提交,如图 现在我们在这个分支做一些修改,然后生成两个提交(commit). $ vi file.txt $ git commit $ vi

尚硅谷面试第一季-04方法的参数传递机制

面试题代码: 1 package 方法的参数传递机制; 2 3 import java.util.Arrays; 4 5 /** 6 * @author zsh 7 * @company wlgzs 8 * @create 2019-03-27 9:37 9 * @Describe 方法的传递机制 10 * (1)形参是基本数据类型的 11 * 传递数据值 12 * (2)形参是引用数据类型的 13 * 传递地址值 14 * 特殊的类型:String.包装类等对象不可变性 15 */ 16 pu

深入理解Java中方法的参数传递机制

形参和实参 我们知道,在Java中定义方法时,是可以定义参数的,比如: public static void main(String[] args){ } 这里的args就是一个字符串数组类型的参数. 在程序设计语言中,参数有形式参数和实际参数之分,先来看下它们的定义: 形式参数:是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传入的参数,简称"形参". 实际参数:在主调函数中调用一个函数时,函数名后面括号中的参数称为"实际参数",简称"

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

对函数的形参感兴趣的可以看一下 一. 函数参数传递机制的基本理论 函数参数传递机制问题在本质上是调用函数(过程)和被调用函数(过程)在调用发生时进行通信的方法问题.基本的参数传递机制有两种:值传递和引用传递.以下讨论称调用其他函数的函数为主调函数,被调用的函数为被调函数. 值传递(passl-by-value)过程中,被调函数的形式参数作为被调函数的局部变量处理,即在堆栈中开辟了内存空间以存放由主调函数放进来的实参的值,从而成为了实参的一个副本.值传递的特点是被调函数对形式参数的任何操作都是作为

JavaSE 面试题: 方法的参数传递机制

JavaSE 面试题 方法的参数传递机制 import java.util.Arrays; public class Test { public static void main(String[] args) { int i = 1; String str = "hello"; Integer num = 200; int[] arr = {1, 2, 3, 4, 5}; MyData my = new MyData(); change(i, str, num, arr, my); S