#引用传递和值传递

#堆(heap)和栈(stack)

在之前的学习里,我不止一次提到值类型和引用类型,虽然很早就想告诉大家这两个东西到底是啥,但感觉需要讲不小的篇幅,拖拖拖,最后决定放到了这里讲,接下来就让我们详细来看看值类型和引用类型的区别和联系。
基本上值类型有所有基础数据结构(不包含string),int,double等,还有结构是值类型。
除了以上提到的其他的都是引用类型,各种类,以及string。
要理解值类型和引用类型就需要先了解一下什么是 栈(stack)和堆(heap) 。
栈(stack)在计算机科学中是限定仅在表尾进行插入或删除操作的线性表。因为只能堆表尾进行操作,所以有先进后出的规则。
堆(heap)是区别于栈区、全局数据区和代码区的另一个 内存 区域。堆允许程序在运行时动态地申请某个大小的内存空间。
怎么说呢,细节我也不好说(其实我也说不清楚),但大家可以理解为有两个内存区域,一个叫做堆,一个叫做栈。
以下为栈的简单示意图
以下为堆的简单示意图
我们来看看两者的区别,
  • 堆栈空间分配的区别
栈:由操作系统自动分配释放,在使用完毕(比如函数内的变量,在函数结束时),内存立刻释放(变量销毁)。
堆:一般由程序员分配释放,分配方式类似于链表。(C++中需要程序员手动释放内存,C#和Java中则是等待GC(垃圾回收)进行释放。这两种方式各有优劣,GC(垃圾回收)减少了程序员的工作量,降低了难度,手动管理则可以明确知道销毁时间,方便其他操作。)
  • 堆栈缓存方式的区别
栈:使用的是一级缓存,它们通常处于存储空间中,调用完毕立即释放。
堆:存放于二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象(无变量引用的对象)就能被回收)。
  • 堆栈数据结构的区别
栈:一种先进后出的数据结构。
堆:堆可以被看成是一棵树。
为什么要有堆和栈呢?
栈对应的是值类型,而堆对应的是引用类型。
栈的读取速度比堆要快上很多,在读取值类型上由很大的优势(这也是为什么我们会使用结构而不是类,当然结构比起类还更加节省内存空间。)
堆的优势则在于可以动态的申请内存,可以申请很大的内存。
总结一下,说了这么多,大家主要是需要知道有堆和栈,两种操作系统对进程占用的内存空间的两种管理方式,栈里面存值类型,堆里面是引用类型(硬要说的话是引用类型的本体)。

#值类型和引用类型

上面已经说了,哪些是值类型哪些是引用类型,那么接下来我们来看看值类型和引用类型在堆栈中的存储方式。
首先是值类型,值类型存储在栈里面。
引用类型则是将主要的数据内存存储于堆中,在栈中只存放一条指着堆中本体的地址。(论引用类型为什么叫引用类型?因为它是引用的类型。)
因为每次调用引用类型都现在栈里面找到,然后再去堆里面找到,这一来一回的就更加耗时间了。
好,基本上值类型和引用类型的基础内容就讲完了,其实也挺好理解的(如果不细究堆栈的话),接下来我们来看看实操吧。

#值传递和引用传递

最能看出值类型和引用类型的区别的无疑是方法中参数的传递了。
    
    
void Start ( ) { A a1 = new A { x = 1 } ; //这是在创建时附上初值 ChangeA ( a1 ) ; Debug . Log ( a1 . x ) ; } private void ChangeA ( A a ) { a . x = 2 ; }
这里的start()是unity生命周期里的函数,在一个代码绑定的物体被创建时调用。debug.log()表示输出提示内容。
问:当上面的代码运行后,输出的数字是多少?
恭喜你回答错误!
如果你回答了这道题那么你就是错的。如果你眼睛一转,发觉这题不对啊,这作者怕不是失了志,那么恭喜你,你才是正确的。不过我还没有给大家讲值传递和引用传递的内容,大家也发觉不了,嘿嘿。
这题还需要加上一个条件,那就是A倒是是一个类还是一个结构。结构和类将直接决定调用函数时传参是值传递还是引用传递。
我们先来假设A是一个结构,那么这里就是值传递了(如果是值类型用于参数传递就是值传递,引用类型就引用传递。)。我们回忆一下值类型是咋个存的,栈里面单独的存储一个数据结构,这里的值传递就相当于再在栈里面开一个存储数据,然后让这个存储数据的值和传递的值类型的值一样。
简单的来说,就是两者啥关系莫得。函数中的变量早就是另一个数据内存了,无论怎么改变对原来的数据结构都莫得影响,输出的值为1。(很简单,咱就不画图了。)
然后我们再来看看引用传递,假设A是一个类。类与结构就不同了,类是将栈中存储一个指向堆中存储数据的地址,在进行参数传递的时候,函数中的变量实际上是栈上的用于存储地址的存储数据,他被赋值值就是引用传递的值栈中所指向的地址。函数中修改的数值实际上是修改堆上面存储的数据,所以这里输出的值会变成2.
简单的来说,就是方法中所有的修改都会反应到外面的变量上。
真的是这样吗?让我们来看看下面的代码
    
    
private void ChangeA ( A a ) { a . x = 2 ; a = new A { X = 3 } ; }
外面将上面的函数变成这样,A还是类(刚刚说了,值传递方法内外的变量莫得关系。),那么这个时候输出的值会变成多少呢。
答案是2。对的,最后一步代码对并不影响输出结果的值,为什么?
很简单,首先我们要知道,new A{ X = 3 }的意思是在堆上开辟一个新的存储数据,这一步操作与之前a指向的那个存储数据毫无关系。这一步的代码代表的是,在堆上开辟一个新的存储数据,然后将变量a栈上的指向的地址,变成新的存储数据的地址。在方法结束后,a会被销毁,而这个新的数据则会因为没有变量引用它,之后会被GC(垃圾回收)给销毁(无需在意何时被销毁,这不是你现阶段该管的。)。
实际上,A a这种声明是指在栈上放入一个数据存储,在没有被赋值的时候就为null,被赋值后就是指向堆中的地址。(就更C++里面的指针一样。)
懂了吗,没懂多看两遍。该讲的都讲了,如果你没有听懂……只能证明我讲的不好啊(┭┮﹏┭┮) 。看懂了,那可太好了!后面还有更变态的。

#ref参数

    
    
void Start ( ) { A a1 = new A { x = 1 } ; //这是在创建时附上初值 ChangeA ( ref a1 ) ; Debug . Log ( a1 . x ) ; } private void ChangeA ( ref A a ) { a . x = 2 ; }
ref写在开头就表示这是个引用传递的参数,在调用函数的时候需要在实参前面也加上ref关键字。
这个时候的A如果是结构的话,它就可以享受类的全部体验了,它将以引用传递的姿态登场!也就是变成了上面讲的类那样,我就复读了。
可怕的来了,A可以是类,这里就将变成引用的引用传递(套娃)。听着都觉得难搞。
我们将函数变成这样
    
    
private void ChangeA ( ref A a ) { a . x = 2 ; a = new A { X = 3 } ; }
然后传入的A变为类,这个时候的答案又是多少呢?
是3。为什么!且听我娓娓道来:在上面的内容里我们了解到了方法中的a实际上存储的是堆上的地址,即为一个引用。这里的ref关键字则让我们的引用再升级,变成了引用的引用,方法中的a仍然是栈上的存储数据,它仍然是存储了一个地址,但这个地址,变成了外面传递实参时那个实参在栈上的存储数据的地址!
我的评价是:好家伙,隔着套娃呢。
在最后一步的代码里,因为方法中的变量存储着外面变量的地址,由此访问到了外面的变量,然后将新开辟的堆上数据结构的地址赋值给了外面的变量。
简单的来说,我简单说不了,你们加油理解吧(入门到入土)。要是可以理解C++中指针的概念,这些东西还是比较好理解的。

#out参数

    
    
void Start ( ) { A a1 = new A { x = 1 } ; //这是在创建时附上初值 ChangeA ( out a1 ) ; Debug . Log ( a1 . x ) ; } private void ChangeA ( out A a ) { a . x = 2 ; }
前面介绍了这么多,这里就可以少介绍点了。out和ref的用法很像,唯一不太一样的就是out的值不需要提前初始化也可以,ref则不行,over。
out和ref差不多为什么会有两个呢,还有什么时候该用ref,什么时候该用out呢?
问的好,能问出这种问题证明你是个善于思考的人,但很可惜,我回答不了,我连宇宙的尽头在哪都不知道,怎么回答得了这种问题呢。
开个玩笑,我确实不知道为啥,但不是有while和do……while吗,明明那么相似,不也是两个吗,这里应该也差不多。至于什么场景用哪个,依你喜欢就好。

#小习题

我觉得吧,指望我给大家出题多少有点难为我了,劳心费神,所以我不生产题,我只是网络题库的搬运工。
因为这是这个系列第一次出现练习题,所以会极度简单,现在开始。
  • 编写一个程序,输入半径r的值,求出圆的面积。(∏(pai)取3.14)
  • 输入三个数,判断其中的最大数字。
  • 输入一个字母,如果这个字母是大写字母就变为小写,是小写就转换成大写。(这里需要使用Asall码,百度吧骚年,查找答案的能力对程序员也很重要!)
  • 输入一个字符,判断它是小写字母,大写字母,数字还是其他字符。
  • 创建一个结构体Student,包含学号id,姓名name,性别⚥sex,以及成绩grade。并创建一个学生。然后在整个类的,类里面所有的字段都要是私有的,并且有他们公有的属性。
  • 输入一个正数,判断其四舍五入后的整数。如12.32就是12,14.53就是15。
  • 算个1加到100的答案吧!
  • 求出所有水仙花数。水仙花数是指一个三位数,其各位数字立方和等于该数本身。例如, 153 = 1*1*1 + 5*5*5 + 3*3*3,所以153是水仙花数。(诶呀,好经典的问题。)
  • 输入十个整数,把他们按从大到小排列起来。(因为我还没有讲数组,大家可以把数字按顺序打出来。)
就这些吧。这些本来是C#控制台程序的,不过unity可以通过debug进行输出,input Field的UI进行输入,差别不大,如果觉得麻烦,你可以去使用C#控制台程序实现。
庆贺吧,会做这些题的话,基本上可以在大学里面编程选修课上拿个及格分了。

#答案

欸嘿( •̀ ω •́ )✧,哪有人会把习题和参考答案放一起的,实在解决不了可以评论区问一下,没准会有神奇的海螺回答你呢。

#结束语

  • 杂谈
我要吐槽一下,unity社区里面写文章,使用富文本,然后使用里面的插入代码,然后敲得时候按返回就有一定概率,直接变白屏,很刺激,体验很差,希望后面改一下。
下一篇主要讲继承吧,如果内容少了咱就加上泛型一起讲。
拜拜,各位,下周见。
Logo

分享前沿Unity技术干货和开发经验,精彩的Unity活动和社区相关信息

更多推荐