好了,我们再来看一下参数传递的 另外两种情况,就是进行引用传递。
那么当进行引用传递的时候,整个参数进行 采用这样的一个引用的方式进行传递。
引用传递是指的什么呢? 引用传递是指的把实参传给形参的时候
不是把实参拷贝一份传给形参,而是把实参在内存中的位置
或者它的引用传递给形参。
这时候一旦有了这样的一个传递,那么在这个
方法里面就有可能根据传递过来的引用
而直接能够改变原来的调用的实参的值。
那么当进行引用传递的时候,实参跟形参将 共享同一个存储单元。
任何形参的改变都会影响到 实参的改变上。
或者我们可以这样说,形参此时是实参的一个 别名,别名。
它俩是一模一样的,指向了同一块内存单元。
那么如果要使参数的这样的一个传递,使这样的一个引用传递, 需要在参数前面多加一个关键字
ref,它是一个 reference 的前三个字母的缩写。
如果一个参数在前面标记了 ref,
指的是在这个参数进行传递的时候采用引用传递。
也就是说不是把这个参数拷贝一份传递给
这样的一个形参,而是把
参数本身的地址或者它在内存中的位置传递给形参,
使得形参也具有同样的位置,和实参具有同样一块 内存单元。
因此任何形参的改变就意味着实参的改变。
我们再来看一下这个幻灯片所示的。
如果实参同样是一个整数 a5,只是在传递的时候前面
多了这样的一个关键字 ref,那么进行引用的传递,reference
的传递的话, 那么这时候就不会把
a 拷贝一份传给形参的 b5, 而是让形参
b 和实参 a 都指向同一个内存空间。
也就是说形参 b 就是实参 a 的 存储位置。
那么在形参 b 里面, 在这个方法执行的时候,如果对
b 的任何改变 都会导致原有的 a 的同样的改变,
因为本身 a、 b 就会指的同一个内存空间。
同一个内存空间,那么也就是什么呢?也就是 b 改,a 就会跟着改。
那么即使在调用结束 之后把 b 的这样的一个别名销毁掉了之后,a 已经发生了改变。
那么这就是当引用传递的时候发生的事情。
那么下面我们再来看一下, 如果引用传递本身又传递的是一个引用类型的数据,
会发生一个什么样的情况呢? 这时候情况稍微有一下复杂,我们来看一下这个幻灯片,看一下幻灯片。
那么也就是什么呢意思呢?这时候的一个实参 本身这个 array
它就是一个引用类型的数据, 我们知道所谓引用类型的数据,它就是指的是一块
指向堆空间的一个内存的标识符。
那么这样的一个标识符标识了一块的内存空间,
当进行参数调用的时候,那么会把这样的一个标识符传递到,
传递到方法里面去。
那么我们前面已经说了, 如果是值传递的话,会把这个标识符拷贝到这样的一个
方法里面来,而方法通过这样一个拷贝后的标识符就能够改变 原这个数组里面实际的值。
那么如果在这个标识符 前面再加上一个引用,变成了引用传递引用型的
参数,是什么样的结果呢?就是会使得方法里面的这个标识符 array1
和原有的这个标识符 array 是同一块内存空间。
当然了,一般情况下,如果你在这个方法里面不去改这个标识符,
而只是使用这个标识符改这个标识符所指向空间的数据的话,
那么这样的调用和刚才的值传递引用类型的数据 效果是一样的,没有什么区别。
唯一的区别是 如果你在方法里面重新为 array1
分配了新的空间和数据的话, 这时候 array1
就会指向到一块新的 内存空间里面去。
这时候由于 array1 跟 array
进行的引用传递是同样一个东西, 那么当把 array1
关联到一个新的空间上了之后, 原有的 array 也会关联到这样的一个新的空间上。
那么当你的这样的一个方法调用结束的时候,array 就会变到指向了一个新的空间,
而它就永远找不到原来的数组空间在哪里了。
当然了,一般情况下,我们是不希望这样的,但是也 有的情况下我们是有意地让它指到了一个新的空间上。
而如果这时候的调用是一个值传递的调用, 那么也就是说是我们上一次课讲的
就刚才讲的这个值传递传递的是引用参数调用的话, 那么如果也就是说把
array 是拷贝了一份给 array1 的话, 那么即使在这个方法里面你为
array1 分配了新的空间, 把 array1
指到了新的空间上,那么当这样的一个方法调用结束的时候, array
并不会跟着指到一个新的空间,因为这时候值传递是一个 拷贝传递。
那么也就是说当方法结束的时候, array 仍然会在有原有的空间上。
那么把这样的一个事情总结一下就是这样的一个问题:
如果一个引用型的参数也通过引用的方法进行传递的话,
那么在方法里面如果没有为这个
参数或者这个句柄分配新的存储空间或者指向新的存储空间的话,
此时此刻的引用传递跟值传递的效果基本是等同的。
除非你在方法里面为这个传递的形参新分配了空间或者指到了别处,
那么当值传递的时候,不会把这样的一个影响带给原来的这样的一个
句柄,它将会还会指向原来的空间。
而如果是引用传递,当把形参的 这样的一个句柄指向为新的空间的时候,
原来实参的空间的指向也将会跟着 改变。
好了,我们关于这样的一个事情我们再来看一些例题。
那么我们在前面讲了一下这样的一个当这个参数类型是值参数的时候,它的这样的一个
值调用和引用调用的这样的一个参数传递。
接下来我们再 讨论一下如果参数类型本身就是一个引用类型的参数,
那么进行这样的传递的时候会发生什么样的有趣的事情呢? 那么我们,同样,我们来启动 Visual Studio 来做一个例子。
[空白音频] 我们新建一个项目。
然后我们的项目命名为 CSHARP,CSHARP,
EX09, CSHARPEX09。
然后选择这样的一个 "确定"。
我们仍然是把我们的这个 拷贝过来。
[空白音频]
[空白音频] 好。
同样地我们来考虑一下。
我们考虑的 这样的一个引用型的参数,那么什么是引用类型的这样的一个变量呢或者这样的一个东西呢?
那么毫无疑问我们前面学过的这个数组,它就是一个引用型的这样的一个变量。
什么叫做一个引用型的变量呢?所谓一个引用型的这样的一个 参数或者这样的变量,是指的是
在这个变量的名字里面存的是一个string,或者说叫做一个句柄。
这个句柄指示了一块内存,这个内存里面存的是正儿八经的真正的这样的一个 数据。
那么也就是说如果我有这样的一个 数组
a ,在这个数组名的这个 a 里面,存的并不是说数组的全部的数据,
在这个数组名 a 里面仅仅存的是一个 标记、 索引号,或者说是一个标牌号。
按照这个标牌号能够找到 一块内存,那么在这个内存里面真正存的是 a0、
a1、 a2、 a3、 a4 这样的一个数组的数据。
特别要记住的是对于引用类型的 变量来说,引用类型的那个名字里面并没有存储真正的数据,
它存的是数据所在内存的标牌号、 索引号,准确的说法叫做 句柄。
ok。
我们看一下。
我们假设我们有这样的一个 AddOne
函数,那么它接收这样的一个数组作为这样的一个参数。
那在这样的一个数组类型里面,
[无声]
我们便利这样的一个这样的一个一维数组元素里面的每一个数据,
每一个数据,然后怎么样呢?让这样的一个所有的这样的一个数据里边呢 增加,让所有的数据增加
1 , 就干这样的一个简单的事情,这个函数本身并不返回任何的值。
在我们的这样的一个主函数里面,我们声明这样的一个数组 a,
然后它具有三个元素,假设分别是 1、 2、 3。
1、 2、 3 三个元素。
随后怎么样呢?随后我们调用了这样的一个 AddOne 把这个数组 a 传递过来。
然后传递完了之后,我们做了一个这样的一个 循环
foreach,把数组里面的每一个元素都打印出来。
[无声]
把数组里面的每一个元素全部打印出来,
[无声] 打印出来。
打印出来了之后,我们看一下 这样的一个,首先我们先来运行一下这个程序,看一下这个结果,
摁一下这个 ctrl 加上这个 F5 键。
结果是 2、 3、 4。
我们看到数组里面的每一个元素被加了 1。
那么首先我们来解释一下。
首先我们来看到这样的一个调用前面并没有一个 reference 这样的指示,
所以就参数的传递方法来说,它是按值方法来调用的,进行的值传递。
那么虽然,那么既然是这样的一个值传递,为什么还改变了 a 数组里面的元素呢?我们刚看到在前头这个例子里面,
如果是值传递的话,那么并不会改变这样的一个实参的内容, 但是对于这样的一个,这样的一个
a 是一个 引用参数来讲,是不是这样的。
因为 a 本身 里面并没有存储这个数组里面的 1、 2、 3 这个数据,
a 本身里面存储的是什么呢?存储的是这个数据 1、 2、 3 在内存的位置信息。
所以说它发生的是一次值传递的拷贝,只是把 这样的一个位置信息拷贝到了一个新的变量 a 里面, 而这个变量
a 里面也拷贝的是这样的一个 1、 2、 3 在内存里面的位置信息。
所以说这时候就有两个 a 的这样的一个位置信息同时指向了这块内存变量 1、 2、 3。
既然拷贝过来的是一个 a 在内存里面的位置信息,
那么不管是哪一个新的位置信息还是新拷贝的这个位置信息, 那么根据这样的一个位置信息找到
a0、 a1、 a2、 a3 进行操作的时候, 当然就改变了 1、 2、 3 这样的值。
也就是说 当数组进行这样传递的时候,我们拷贝了一个数组,
仅仅是把数组的位置信息做了一份拷贝, 而数组在内存里面的存储仍然只有一份,
只有一份在内存里面的存储,它就是 1、 2、 3,只是有
两个位置信息同时都指向了这样的一个 1、 2、 3。
那么我们现在再来想一想如果, 如果这样的一个函数
就是我讲的 AddOne2 ,它前面有这样的一个
跟我们刚才的值传递一样,有这样的一个 reference 这样的一个标记, 把
a 的引用进行这样的一个
传递,
[无声] add
to 把调用这样的一个我们
我们把它把这句话为了看得清楚一点,我们把这句话注释掉, 我们先写一句
AddOne 2(ref
a), 那么进行这样的一个新的调用。
这样的一个调用跟刚才的一个调用有什么区别呢? 我们再来运行一下,结果看一下,按一下 ctrl F5。
2、 3、 4,没有任何的区别。
那么 这时候如果增加了一个 reference 的话,那么就意味着
我们不会把这样的一个 a ,a 再说一遍,a 是标记着内存的一块区域的一个标记,
我们不把这个标记拷贝一份给这个 a ,而是让这个 a 的
新的标记就是这个 a 的标记,两个是同样一个标记。
但是我说了,但是我们想它不管是同样的是一个 标记还是拷贝了一份标记,它其实都是标记了这样的一块内存。
所以说就传递它了一个拷贝的标记过来,和传递了一个同样的一个标记过来,共用一个标- 记过来,
其结果都是指到了这一块内存。
所以说这样的两个调用貌似没有太大的 区别,没有太大区别。
那么区别在哪呢?区别在这。
如果我在这个新的方法里面为这个 a 重新分配了一块内存,
它有 4 个元素, 分别是
11、 12、 13、 14。
然后这 4 个元素 再加了 1 ,随后怎样呢?随后我们再来看一下。
我们打印一下。
按一下 ctrl F5 运行一下看。
结果是 12、 13、 14、 15。
12、 13、 14、 15。
也就是说即这个 a 经过 reference 传递的话, 它和原来的这个标记
a 就是共用了一个存储空间, 我为这个新的标记重新指到新的一个内存上的时候,
当然原来这个标记怎么样呢?也会重新指到一个新的内存上。
这时候 1、 2、 3 这块内存就丢失了,因为没有任何标记再去指向它了。
再去指向它了。
所以说这时候再打印,这时候再为这个 a++ 是为新的这个 4
个元素再 ++, 然后这时候再打印的这样的一个 a 也是一个新的 a 了。
我们再来看,我们把这个 AddOne2 注释掉,还是把这个
AddOne 这个打开,同样地我们把在这个数组里面
为它分配一个新的空间,这时候再运行一下程序。
结果是 1、 2、 3。
为什么又是这样子呢? 在这个里面,a
是按值传递的,它的这样的一个标记拷贝了一份给这个 a , 然后在这个新的
a 里面为它分配了新的数组,这个 a 又指到了, 一开始拷贝过来这个
a 指到的是 1、 2、 3,所以说在 第一句话里面就让这个 a 指到了一个新的空间 11、
12、 13、 14, 但是由于这个 a 跟这个 a 是拷贝过来的,所以说它指到了
11、 12、 13、 14,但是它 并没有,它仍然还是指的 1、
2、 3,然后它把这个 11、 12、 13、 14 的这个空间增加为 12、
13、 14、 15, 那么退出来这个程序之后,这个 a 会被销毁。
这个 a 是它的植入变量,会被销毁。
那么空间当然未必会被销毁。
空间是靠垃圾回收去回收的,但是这个标记已经被销毁了。
而回到主程序来之后怎么样呢?这个指向这个的 a 并没有被销毁。
它仍然会是 1、 2、 3。
当然了,它也没有加 1 ,它也没有加
1, 因为我刚把这个标记拷贝过来的时候,这个标记就指到了一个新的空间上去,
在新的空间上做了加 1 的操作,回来之后这个新的标记又被
销毁了,结果等于是对于这个主函数来讲,等于是什么都没有发生, 仍然这个 a 还是原来的 a。
那么我们希望大家再接下来再反复地再运行一下例子, 再体会一下这个参数之间的传递。
[无声]