[音乐]
前面我们讨论了方法的一些定义 调用等等。
下面我们着重再来讨论一下方法的参数传递
那么我们通过前面的使用一些例子,也会看到了在使用这样的一个方法的时候
我们在调用的时候,将会向方法传递一些数值或者说数据,称之为它的参数传递
那么进入到这样的一个方法呢之后,那么根据 传进来的数值怎么样呢?进行运算或者计算或者执行
最后会形成一个结果,作为怎么样呢?返回,这是通常的一个 方法调用的过程。
那么我们一个方法将会多次被调用 每次调用的时候怎么样呢?可能会传给它不同的数值
譬如我们求 sin x 的话,会可以多次传给不同的
x 的值,而求得 不同的这样的一个 sin 值。
在这一小节里面,我们将来仔细地研究一下方法和参数之间 到底是如何传递的。
那么在讲方法的参数 传递之前,我们先来再来探讨一下前面变量所涉及的一个问题
就是值类型的变量和引用类型的变量。
那么我们知道在 CSHARP
里面 把变量分成了两种类型:一种是值类型的变量,一种是引用类型的变量
那么在这里面我们将要仔细地再讨论一下这两种类型的变量的区别 在
CSHARP 的存储空间里面,我们把
程序运行的时候的空间分成了两部分的数据空间,一个空间叫做栈空间
一个空间叫做堆空间。
所谓栈空间 是指在程序进入到函数或者方法的一个调用的时候
临时分配的一个空间,当这个方法调用结束 的时候,这个空间怎么样呢?就会被随之立刻会被销毁
也就是说,我们在一个方法里面分配的局部变量,通常
只能够在这个方法里面进行使用,或者说我们在一个程序块里面
分配的变量只能够在这个程序块里面使用,而出了这个程序块
的时候,这样的一些变量怎么样?就会被销毁,或者说就无法再继续使用了 这些都是分配在栈上的一个例子。
那么还有一类变量我们是把它分配到 堆上的,而这个堆是由我们专用的有一个垃圾回收程序来管理的
当检测到没有人使用这一块 空间的时候,就会自动被回收,而如果还有人在
访问或者使用这个堆空间,怎么样呢?它就不会被 释放或者被收回。
那么在我们的 CSHARP 里面数据类型
一共有两种:值类型和引用类型。
所有值类型的变量都将被分配在栈空间上 也就是说什么呢?也就是说如果我们声明了一个整数
a 的话 那么将会在这个定义这个 a 的时候,就立刻会在
栈的上面为这个 a 分配一个存储空间,譬如讲
4 个字节的存储空间用来 存放一个整型变量
a,而当这个函数调用或者这个方法调用结束的时候 那么这样的一个栈空间怎么样?就会被销毁。
a 怎么样呢?就不会被使用 a 就无法再被访问了。
那么所有的数值类型上,整数呀、 浮点数呀
还有一些什么小数呀等等等等,还有像字符啊等等,这些怎么样呢?都是值类型的变量
而所有的类类型,包括这样的一个
字符串的类型,包括我们前面学的数组类型都是引用型的变量
那么对于引用型的变量却不是这样子的。
对于一个引用型 的变量,它是分配是这样子的两部分,其中我们要把引用型变量的这样的一个存储空间
将会分配到什么呢?会分配到一个堆里面去
那么而把这样的一个在堆里面甘于该空间的一个引用
或者说该空间的一个标识分配到栈里面去
看这样的一个幻灯片,是一个对数组这样的一个 a 的分配的一个示意图
也就是说,根据前面的这个讲述,我们当在这里面分配了一个数组 a 的时候
譬如讲我们写下了一个 int [] a 等于 new
int[100],那么会产生什么事情呢?会首先 在堆这个空间里面分配
100 个存放整数的 连续的空间,那么给你分好了这样的一个空间的时候
为了让你能够使用这样的一个空间,我们会要应该把这个空间的
第一个字节的地址,或者说这个空间的标识,我们称之为句柄
要告诉你,你才能够根据这样的一个标识来使用这样的一个空间 而这样的一个标识在
CSHARP 里面,我们称之为句柄,这个句柄是存在哪的呢?就是
存在了你的数组名 a 里面,而数组名
a 本身是存在程序的 栈空间里面的,也就是说我们在栈空间里面将会有一个
数组名 a,这个 a 仅仅存放的是一个
某一块内存的标识或者引用,而这 样的一个引用的具体的空间却是在堆空间里面的
那么任何一个标识引用都是一个 32 位的数,都是一个 32
位的数 所以说任何一个数组名的大小,都是一样的 4 个字节的一个数 在栈空间里面。
当一个方法结束调用的时候,栈空间会被销毁
但是堆里面的东西并不会被销毁,除非这个堆里面的东西没用
任何人用了,因此怎么样呢?因此我们当把这个数组 a 作为
如果在,方法里面我们事先把这样的一个数组 a 的变量
a 的这样的一个句柄能够保存到别的地方的话,那么 出了这样一个方法的调用,仍然可以访问到数组
a 里面的东西 因为怎么样呢?因为销毁了仅仅是数组的一个
引用和标识,数组的内容并不会被销毁 这就是这样的一个值,引用类型的
这样的一个变量,在内存里面的分配方法 当然了,我们前面讲数组的时候也说过了
只有在你使用了 new 的时候,才会在堆里面分配空间
如果不使用 new 的话,仅仅写了一个 int 方括号 a 的话,那么
造成的效果就是,仅仅只会在栈里面分配一个 a 的标识
此时 a 的标识不会指向任何空间,它的值是空,那 null 它的值是空值。
好了,那么有了 这样的一个概念了之后,我们再来看一下对于参数的
方法的参数传递,也分为了值传递和引用传递
那么因此,根据这样的修饰符的
不同,或者说要传递的这个参数的这样的一个类型的不同
我们就可以分为用值传递的方法,传递
值类型的变量 用值传递的方法传递引用类型的变量
以及用引用类型的方法传递值类型的变量,和用引用类型的方法传递引用类型的变量 这样 4 种情况。
当然了这 4 种情况是可以混用的,在同样一个方法的参数里面
既可以进行值传递引用,值传递跟引用传递可以根据不同的参数分别地进行怎样呢? 设定。
那么在一个方法的调用里面,在我们前面看到的简单例子都看到
每一个参数的方法要写出来参数类型以及参数的名字 那么在一般情况下,如果没有其他任何的标识符的
情况下,那么就认为这样的一个参数将是一个值传递 也就是说不使用任何修饰符标记的参数
参数的传递方式是值传递,这也是我们参数传递的一个 默认方式。
在进行值传递的时候系统将会将
实参的一个副本传递到参数的一个形参里面去,所谓实参
就是在函数调用时候传递过来的具体的数值称之为 实参。
所谓形参是指的在
方法的定义的时候,在方法的小括号里面定义的参数称之为形参
函数调用的过程是一个把实参的值传递给形参的过程,也就是说
实参和形参相结合的一个过程。
下面我们看一下幻灯片。
那么对于这样的一个 值传递,如果参数也是值类型的话,会发生什么情况呢?
发生的情况是:首先,值传递是指的 拷贝传递。
也就是说把现有的实参 的值拷贝一份传给方法的形参,让方法去通过这个值
怎么样呢?进行使用。
因此,如果实参的值 a ,譬如讲是一个整数,它是 5 的话,
那么会把这个 5 拷贝一份传递给方法的形参。
譬如讲, 对应的形参的名字叫做 b ,因此 b 的值就是 5。
那么下来由于这样的一个并且这样的一个形参也是在栈里面分配的空间, 下来所有对 b
的操作都是对这个副本的 b 进行操作,在方法里面。
不管对 b 如何改变,都不会影响到原来的
实参 a 的值,当方法调用结束的时候,b 会被销毁,
返回到函数的调用处,这时候 a 的值仍然会是 5 ,不会有任何的改变。
这就是我们的第一种情况:参数传递是 值传递并且参数的类型也是值类型。
下面我们看第二种情况的传递。
参数的传递仍然是值传递,但是 传过来的参数是一个引用类型的参数。
譬如讲,我们的参数是一个数组, 那么参数是一个数组也就是说,譬如讲我们的参数是一个
int [a] 这样的一个参数, 传递的是一个数组这样的一个引用类型的数据。
下来我们看一下幻灯片。
如果实参是一个 数组的话,那么 a r r a y,array
是一个数组的话, 这时候所谓的实参,根据我们刚才介绍的引用类型的这样的一个数据类型的存储方式,
这时候其实仅仅传递的只是这个实参的句柄,
而并不是说把整个数组作为参数传递给了方法。
那也就是说在整个参数的传递过程当中, 我们仅仅只是传递了这样的一个
引用类型数据的标识跟句柄。
那么根据值传递的这样的一个约定,
进行值传递的时候是一个拷贝传递,所以怎么样呢?所以我们进行传递的时候,
进行传递的时候我们把 array
这样的一个引用拷贝到了形参 array1 里面。
那么也就是说实参的 array 拷贝到了形参的 array1 里面,进行了一次句柄的复制。
这时候在形参里面的 array1 ,那么由于两个句柄是一样的,所以说在形参里面的
array1 也会 指向了同一块堆空间里面的内存。
也就是说 array1 也会指向了原来的 array 里面的 数据。
因此在这样的情况下,在这个方法里面, 任何对 array1
这个数组的改变 都会影响到原有数组数据的改变。
当这个方法 调用的时候,虽然这样的一个复制的句柄 array1
被销毁了, 但是实际的数据怎么样呢?已经被改变了。
因此当这样的一个方法调用返回的时候,
数组的数据会被改变,这就是值传递传递的引用的时候。
简单地说,如果传递的是一个引用数据的话,
那么在这个方法里面,如果改变了这个引用类型的数据或者数值的话,
那么原有的引用类型的数值跟数据怎么样?也会跟着发生改变。
那么下面我们首先根据前面的情况来看一个例题。
下面我们来研究一下 这个方法里面的参数传递。
我们首先来看的情况是如果参数是值类型,
那么分别进行值传递和引用传递的时候,会发生什么样的这样的一个事情。
我们通过一个例子来说明一下这个问题。
我们打开这样的一个 Visual Studio。
[空白_音频]
新建一个项目。
[空白_音频] [空白_音频]
[空白_音频] [空白_音频]
那么把我们的一个取名为 C
SHARPEX 08,这是我们的第八个例子。
然后选择的是 "CSHARP",然后选择的是 "控制台应用程序",然后选择 "确定"。
选择完确定了之后, 跟往常一样,我们把我们这个例
8 的这个目录作为一个注释拷贝到我们这样的一个程序这边来。
[空白_音频]
作为这样的例子,我们是研究了值类型的值传递。
好了。
那么当然了,我们的方法肯定是在一个类里边的,我们简单起见就在 这个 class Program 类里面来研究这样的一个参数的传递。
我们就不再新建这样的一个类了。
那么因为这个 static 是 void 这个 Main,是这样的一个,
Main 函数是这样的一个 static 的,所以说我们要也把这个建成一个 static 的。
我们在这样的一个函数里我们看一个,简单地我们看一个 Swap
这样的一个函数。
在这样的一个 Swap 这样的一个函数里面,我们干一件什么样的事情呢?
在这样的一个 Swap 函数里面,我们给出一个
参数 a ,再给出一个参数 b。
那么 在 Swap 里面我们把 a 和 b 怎么样呢?交换一下。
我们再设定一个临时的变量 temp ,让它等于 a。
然后我们让这样的一个 a=b, b 等于。
通过这样的一段代码,我们就把这样的一个,
我们就怎么样呢?我们就交换了这样的一个 Swap ,把 a 跟 b 交换了 一下。
那么它能不能达到交换两个数的目的呢?我们现在来看一下。
我在主函数里面设定一个 a ,让它的值等于,设定一个等于 10。
我们再设定一个变量 b ,让它的值等于
99。
然后 我调用这样的一个 Swap 这样的一个函数,传递 a 和 b 过来。
传递完了之后怎么样呢? 我们输出一下这个,再次输出一下这个 a 和 b 的值。
a=
and b=
,a ,b。
好了。
那么我们首先运行一下这样的一个程序来看一下这个结果。
按一下 control F5 ,建议输出生成这个项目,然后看。
a=10,b=99。
那么我们会发现 那问题是什么呢?a 和 b 并没有进行交换。
那么如果我们把这样的一个 Console
在这里面再打印一遍。
看到没?在 Swap 里面再一次打印这个 a 和
b 的值, 然后怎么样呢?再运行退出 Swap 之后,再一次打印了这个 a 跟 b 的值,我们再看一下。
control+F5运行。
a=99,b=10,a=10,b=99。
那么通过上面这样的运行我们看到了 在这里面,a
跟 b 确实做了交换了,但是 回到主函数了之后 a 跟 b 的值又变成了原来的值。
为什么?首先我们来解释一下这个过程。
首先我们知道这个是主函数的 a , 这里是这个的 Swap
函数的参数增量 a, 我们要明白,第一个问题是:这个 a 和这个
a 虽然叫同一个名字, 但是确实没有什么太大的关系。
也就是说它们在内存里面是 2 块不同的空间, 只是我不小心把它们同样命名成了 a 而已。
完全可以把它命名成 a1 或者 b1 ,现在就变成了 a1 , a1 b1 b1 b1 这样。
那么 在进行函数调用的时候,当这样的一个参数本身是这样的一个值参数的时候,
在调用的情况下并没有任何修饰符的时候,默认就是值传递。
所谓 " 值传递 ",那么意思就是说把
a 这样内存空间里面的这个数拷贝一份 到这个
a 里面,把这个 b 里面的数拷贝一份到这个 b 里面来。
所以进行函数调用的时候,在一开始此时 a 是 10 ,
b 是 99 ,但是这个 a 的 10 和 b 的 99
和这个 a 的 10 跟 b 的 99 一共占用了 4 块内存空间。
其中 2 块是主函数的 a = 10 ,b = 99
进入到这个函数进行调用的时候,会把这个函数调用时候会重新分配一个新的内存, 也叫
a ,再分配一个新的内存叫 b ,然后 把这个
a 的值拷贝到这个新的内存里面来, 把这个 b 的值拷贝到这个新的内存里面来。
在 Swap 函数里面对 a b进行了交换,是指 对这样的一个新分配的内存的
a b 进行了交换, 并没有影响到原来的这个 a 和这个 b
,所以在此时打印的这是, 此时打印的这样的一个 a b 是指的在这个
Swap 函数 里面的这个 a 和这个 b ,所以说显示它们进行了交换。
a 变成了 99 ,b 变成了 10。
那么从这个 Swap 函数 调用结束之后,那么这一块 a 跟 b
的内存就会被销毁, 被系统收回,被系统收回了之后,那么 Swap 调用结束。
调用结束了之后 在下面这样的一个语句打印出来的
a 跟 b 的值, 仍然是指的这个 a 和这个 b。
是也就是说是原先的那 2 块内存里的东西,它们并没有受到影响,仍然是 10 和 99。
这就是当参数是值参数,并且进行值传递调用的时候所发生的事情。
那么我们再来写一个函数,
void 假设 叫 Swap
2 ,那么这个函数在前面多增加了一个关键字
ref , [空白音频]
[空白音频]
[空白音频]
在这样的一个
函数里面多增加了,在这样的前面多增加了一个 ref, reference
的意思, 表示这 2 个参数按引用的方式进行传递。
那么在这里面我们并不是去改变它的 代码,它的代码跟我们刚才的
Swap 的代码是一模一样的,我们把它拷贝一份 做下来。
而在这个函数里面,我们将不再调用这样的一个 Swap
,我们调用一下我们刚才的这个 Swap2。
那么在调用的时候, 由于
a b 是这样的一个引用性的参数,在调用的时候我们也要加上这个 ref 的引用,这样的一个关键字。
那么在参数前面有一个 ref 称之为该参数按引用方式传递,
你需要注意的是对于一个方法而言,它可以有某个参数是引用的,也可以是不引用的。
那么根据你编程的需要,那么也就是说不一定所有的参数里面非要是都引用,或者是都不引用,
可以是某一个参数是引用的,某一个参数是不引用的。
只不过在我这两个例子,我这个例子里面 a 跟 b 都是引用类型的参数。
那么有了这样的一个 ref returns,我们 再看一下此时此刻运行的结果,Ctrl + F5 再运行一下。
a 99,b 10 ; a 99,b 10。
我们会看到什么呢?我们会看到 a 跟 b 确实做了交换, 那么有ref
跟没有 ref 的区别是什么呢? 区别就是如果一个参数前面有 ref
修饰的话, 那么此时此刻就不会再为这个 a 分配新的内存空间,
而是和这个原有的 a 共享一个内存空间。
因此我们说带有引用参数的 a ,就是这个参数 a。
它们俩之间是一回事儿,这个 a 是它的另外的一个名字, 或称为别名。
既然这样这个 a 就是这个 a ,这个 b 就是这个 b。
那么在这个 Swap 里面,这个 a b交换的时候,当然就把这 2 个 a b 也进行了交换。
所以说引用传递就是指的,在传递参数的时候不会为这个
方法的这个参数列表里面的参数再分配新的空间的了,
而是和传递过来的这样的一个参数共用同一个内存空间。
因此在这个方法里面对任何 ref 这样 reference 引用的参数,
引用的参数做的任何的改变都会影响到 原有参数的改变。
那么注意我们这里的 a 是一个
值类型的变量,这是值类型的变量进行这样的引用传递,和这样的
按值传递的 2 个情况的不同。
好了,就讲解到这儿。