C#中的函数(三)参数传递及返回值

时间:2022-08-29 20:49:15

接前面二篇,继续开始新的研究

 

前面忘了说什么是主调函数与被调函数

主调函数:执行调用其它函数语句所在的函数

被调函数:被其它函数所调用的函数

简单说就是一个是发起调用者,另一个是被调用者

写个小例子说明下,一看就懂

Main函数就是主调函数,test_A()这句语句所在的函数就是主调函数

tset_A就是被调函数, 它是被主调函数Main中的语句test_A()进行调用的

C#中的函数(三)参数传递及返回值

 重归正题

参数传递分为2类

1.普通传递(形参数据类型前面没有ref或者out关键字,传递的是变量中的数据)

2.引用传递(形参数据类型前面加上ref或者out关键字,传递的是变量在栈中的地址)

 

普通传递根据参数的数据类型分为普通传递值类型跟普通传递引用类型

普通传递值类型

小例子如下:

C#中的函数(三)参数传递及返回值

主调函数Main部份

1.定义二个变量a与b

2.主调函数里调用MyAdd,把变量a与b当作实参进行传递

被调函数MyAdd部份

1.把参数a与参数b进行相加,保存在临时变量c中

2.返回前对a与b进行修改,然后返回变量c中的二个数相加的结果

最终返回到Main函数中,变量a与b的值没有发生改变

总结下:

普通传递值类型,传递实参后被调用函数内部对它进行修改不会影响到主调函数中的变量

因为变量是值类型,变量中的数据是数值

普通传递引用类型

小例子如下:

C#中的函数(三)参数传递及返回值

主调函数Main部份

1.主调函数main中先实例化一个自定义的类TestClass,然后保存在引用类型变量temp中

2.调用被调函数test_A, 把变量temp当作实参进行传递

被调函数tset_A部份

1.把参数TestClass类型的对象temp中的MyName属性修改为大白腿

最终返回到Main函数中,引用类型变量temp中的Myname字段发生改变

总结下:

普通传递引用类型,传递实参后被调用函数内部对它进行修改会影响到主调函数中的变量

因为变量是引用类型,变量中的数据是引用地址,引用地址是对象在托管堆中的内存地址

当外部对引用类型变量进行修改时,相当于直接对托管堆中的数据进行了修改.

 

引用传递(形参数据类型前面加上ref或者out关键字,传递的是变量在栈中的地址)

引用传递的主要目的是对参数进行修改,然后让外部数据进行同步更新,返回多个数据的作用

写到这估计又有人有疑问了,直接在函数前面定义返回值用来接收返回值不行吗?

一来麻烦,因为如果需要对返回值进行接收,同步更新变量,那接收返回值还得进行赋值操作

二来返回值只能返回一个数据,如果函数有多个形参,需要对多个参数进修改,让外部数据进行同步更新,

起到返回多个数据的目的,那就只能使用引用传递的方式。

引用传递根据参数的数据类型分为引用传递值类型跟引用传递引用类型

根据前面说的普通传递,那引用类型的变量需要引用传递吗?大多数情况下不需要,因为前面说过,

引用类型的实参,传递的是引用地址,其它地方对它进行修改,会直接对堆中数据进行更改

真正常用的是多个值类型的实参,才需要使用引用传递的方式

 

写了一大堆引用传递,还是写个值类型的引用传递来看下效果

引用传递值类型 (out)

小例子如下:

C#中的函数(三)参数传递及返回值

主调函数Main部份

1.定义四个变量a,b,addNum,maxNum

2.调用Test函数,前二个实参a,b只是用来计算的,并不需要返回数据或者修改数据,所以不使用引用传递.

后二个参数是用来接收返回的二个数相加的结果,所以需要使用引用传递参数,这里使用的是out方式

被调函数

1. 对二个数进行相加,然后根据传递过来的addNum在主函数main中的变量地址,对这个地址写入新的数据

2. 对二个数进行比较,然后根据传递过来的maxNum在主函数main中的变量地址,对这个地址写入新的数据

从这里就能看出使用引用传递的好处了

 

顺带验证Test函数中addNum下到底是不是主函数main中的变量地址呢?

在Test中的addNum = a + b; 下个断点,添加临视,看下addNum中是什么

从图中看,addNum是值啊,不是地址…估计到这有人迷茫了.

其实这是vs特意隐藏了内部的细节,这里的值是是addNum在主函数main中的变量地址下的值,

并不是真正addNum下的数据…

C#中的函数(三)参数传递及返回值

为了证明这点,在主调函数的Test(a,b, out addNum, out maxNum); 这一行再下个断点

C#中的函数(三)参数传递及返回值

F5运行调试,此时断下来,我们转到反汇编,好好分析下它是怎么传递参数的

这七行反汇编代码对应的就是Test(a,b, out addNum, out maxNum);

C#中的函数(三)参数传递及返回值

第一句Lea eax,[ebp - 48h]    //ebp-48h 是变量addNum在栈中的地址,Lea是传地址操作,相当于mov eax,ebp-0x48

第二句push eax   //看过前二篇的就知道这是因为参数多于2个,使用压栈的方式传递参数,这里push的是addNum在栈中的地址

第三句Lea  eax,[ebp - 4ch]   //ebp-4ch 是变量maxNum在栈中的地址,Lea是传地址操作,相当于mov eax,ebp-0x4c

第四句push eax //看过前二篇的就知道这是因为参数多于2个,使用压栈的方式传递参数,这里push的是maxNum在栈中的地址

第五句mov ecx,dowrd ptr [ebp-40h] //这是传递的临时变量a

第六句mov edx,dowrd ptr [ebp-44h] //这是传递的临时变量b

第七句 call 00360c30   //通过间接调用的方式调用Test函数

这里可以单步执行,纪录下ebp-0x48 与ebp-0x4c的值

addNum = 071CE950
maxNum = 071CE94C

进入到Test函数内部, 先单步执行完前二句,查看下堆栈中的情况

[Ebp] = 0x071CE928 = 当前ESP栈顶

[EBP + 4] = 执行完当前函数后的返回地址

[EBP + 8 ] = 传递过来的第四个实参maxNum

[EBP + 0xC] = 传递过来的第三个实参addNum

这里的参数顺序是因为主调函数中是从左向右往堆栈中压入参数三跟参数四,但是在堆栈中栈的增长是从内存地址的高位往

内存地址的低位进行增长的,每压入一个数据,esp - 4开辟四个字节空间,然后esp指针指向esp-4的位置

C#中的函数(三)参数传递及返回值

后面的一些不需要关心了,转回源代码,直接在addNum = a + b; 这一行下个断点

 C#中的函数(三)参数传递及返回值

F5运行调试,转到反汇编继续看

这四行反汇编代码对应的就是addNum = a + b;

C#中的函数(三)参数传递及返回值

第一行mov eax,dword ptr [ebp - 3ch]; //取得第一个实参a,赋值给寄存器eax

第二行add eax,dowrd ptr [ebp - 40h]; //取得第二个实参b,把寄存器eax与b进行相加,再赋值给eax

第三行mov edx,word ptr[ebp + 0ch]; //取得堆栈中的addNum的变量地址

第四行mov dword ptr [edx],eax  //把二个数相加的结果写入到addNum的变量地址下

 

其实这里的引用传递的实参就是C++中的一个指针变量,不过C#没有指针这玩意就没法讲

确定了引用传递参数是传递地址,继续回来,使用ref来写个小例子

 

引用传递值类型 (ref)

小例子如下:

C#中的函数(三)参数传递及返回值

 

总结下:

ref跟out都是引用传递参数, 传递的是实参在主调函数的变量地址,在被调函数中参数是个指针变量,

参数指向了主调函数中的变量地址, 对参数进行操作,

实际就是对指针所指向的主调函数中的变量地址进行了读取或者写入数据

ref 跟 out的区别

ref 传递的实参在主调函数部份必须对它进行赋值,调用函数时实参前面加上ref关键字

在被调函数部份可以直接使用实参,不需要在被调函数里进行赋值

out 传递的实参在主调函数部份不需要对它进行赋值,调用函数时实参前面上out关键字

在被调函数部份不能直接使用实参,需要在被调函数里先赋值,才能进行访问

 

还有更复杂的引用传递引用类型,很少能用到,多余了,大多还是面试题会问.