C#将数据类型分为两种:值类型和引用类型。
这两种类型存储在内存的不同的地方:值类型存储在堆栈(Stack)中,而引用类型则存储在托管堆(managed)上。区分类型是值类型还是引用类型非常重要,这会造成不同的结果。在了解上面的知识之前首先应该弄清楚两个问题。
1.什么是栈?
栈可以理解为一种内存结构,它是先进后出的,就像桶装薯片一样,放进去的时候是一个接一个压进去的,在这里我们成为压栈,而要去拿的时候都是从最后一个进去的开始那。这就是它的特点:先进后出。
在语言中比如我们的程序入口Main方法中有一个GetMoney的方法。它的执行顺序就是先将Main方法压栈再将GetMoney方法压栈,那么执行的时候就是先执行GetMoney方法,执行完毕之后再执行Main方法,这也是栈的应用。
A:栈只能在一端进行操作,也就是栈顶,因为栈底是封闭的。
B:栈不需要开发人员进行管理内存,压栈的时候自行分配内存,出栈的时候自行清理内存。
C:栈的可用空间不大,因此它的执行效率很高,它不能动态请求内存,只能够为内存大小确定的数据分配内存。
2.什么是堆?
A:堆在C#中用于存储实例对象,对象可能会有很多数据,因此它的第一个特性就是:容量大能够存储很多数据,而且能够动态的分配存储空间。
B:栈我们已经知道了只能在栈顶一端进行操作,但堆就不一样了,它可以随意的存取,非常灵活。
C:事情都是有双面性的,堆的内存空间既然大能够存储的数据多,那么它的执行效率肯定是没有栈高的。
一:那么值类型和引用类型到底是怎么样在内存中分配的?
A:对于值类型来说:其变量对应的值是存放在栈中。如果该值类型的实例作为类型的成员,而该实例作为引用类型的一部分的时候,则它被创建在GC堆上。
B:对于引用类型来说:一块空间被分配在堆上,存储应用本身的数据,而另外一块则被分配在栈上,存储对 堆上数据的引用(其实就是内存地址 也叫做指针)例如Person p = new Person();这里可以分为两部分来理解。Person p相当于定义了对象的引用,也就是记录了对象实例的指针,而不是对象本身。这个引用存储在栈中,当没有使用p = new Person()的时候,引用本身为空也就是相当于指针没有指向任何位置;当p = new Person()后,才根据真正的对象的大小动态的在堆中分配空间给对象实例,然后会将实例的引用赋值给p。到这里才算是完成了一个对象的实例化。如下图:
在C#中还有一种概念叫按值传递和按引用传递?
1.什么时按值传递?我们先来看两段代码。
public struct GameStateStruct
{
public int a { get ; set ; }
public int b { get ; set ; }
}
public class GameStateClass
{
public int a { get ; set ; }
public int b { get ; set ; }
}
1.定义了一个结构体 名称为GameStateStruct
2.定义了一个类 名称为GameStateClass
3.在Main方法中操作如下
var tempStract1 = new GameStateStruct ( ) ;
tempStract1. a = 1 ;
tempStract1. b = 1 ;
var tempStract2 = tempStract1;
tempStract1. a = 2 ;
tempStract1. b = 2 ;
Console. WriteLine ( tempStract1. a + " :" + tempStract1. b) ;
Console. WriteLine ( tempStract2. a + " :" + tempStract2. b) ;
4.结果如下:2:2 1:1 那就说明了:对于值类型而言会在栈上重新开辟一块新的空间,将值复制过去。tempStract1和tempStract2是相互独立的,在上述中改变了tempStract1的值并不会影响到tempStract2.
2 什么是引用传递?
var tempClass1 = new GameStateClass ( ) ;
tempClass1. a = 1 ;
tempClass1. b = 1 ;
var tempClass2 = tempClass1;
tempClass1. a = 2 ;
tempClass1. b = 2 ;
Console. WriteLine ( tempClass1. a + " :" + tempClass1. b) ;
Console. WriteLine ( tempClass2. a + " :" + tempClass2. b) ;
结果如下:2:2 2:2 那就说明了:对于引用类型而言也会在栈上重新开辟一块新的空间,但注意这里和值传递的不同的是:这里会将栈上的引用复制到新开辟的空间,引用,引用,引用!为什么可以这么说呢?因为从打印结果来看我们只是改变了tempClass1的a和b的值,并没有去改变tempClass2的a和b的值,但是结果却是一样的。说明了改变tempClass1的值会对tempClass2的值产生影响。如下图所示:
从上图就可以很好的解释为什么。当我们执行完var tempClass1 = new GameStateClass();时候如图中黑线所示,在栈中会开辟一个空间存tempClass1对 堆上new的GameStateClass对象的引用(也可以说为指针)。然后当tempClass2 = tempClass1执行的时候会在栈中重新开辟一个空间存tempClass2然后将tempClass1的引用赋值给他,也就是说tempClass2存储的也是指向堆的new的GameStateClass的对象。为了形象化这里的箭头可以看做引用。最后当我们通过tempClass1去修改对象的数据之后,因为tempClass1和tempClass2都是指向同一个对象,所以我们通过tempClass2去访问对象的属性a和b的时候,结果也为一样的。这就验证了上面的打印结果为什么时2:2 2:2了。
综上所述:值传递和引用传递的区别就是:值类型是复制数据本身,形成相互独立的数据存储区,引用类型是复制引用(指针),存储的是引用,引用指向同一对象。