C#:图解值类型,引用类型,栈,堆,ref,out [转]
标签:
程序执行的原理要彻底搞明白那一堆概念及其它们之间的关系似乎并不是一件容易的事,这是因为大部分C#程序员并不了解托管堆(简称“堆”)和线程栈(简称“栈”),或者知道它们,但了解得并不深入,只知道:引用类型保存在托管堆里,而值类型“通常”保存在栈里。要搞明白那一堆概念的关系,我认为先要明白程序执行的基本原理,从而理解栈和托管堆的作用,才能理清它们的关系。考虑下面代码,Main调用Method1,Method1调用Method2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
class Program { static void Main(string[] args) { var num = 120; Method1(num); } static void Method1(int num) { var num2 = num + 250; Method2(num2); Console.WriteLine(num); } static void Method2(int i) { Console.WriteLine(i); } }
大家都知道Windows程序通常是多个线程的,这里不考虑多线程的问题。程序由Main方法进入开始执行,这时这个(主)线程会分配得到一个1M大小的只属于它自己的线程栈。这1M的的栈空间用于向方法传递参数,定义局部变量。所以在Main方法进入Method1前,大家心理面要有一个”内存图“:把num压入线程栈,如下图:
接着把num作为参数传入Method1方法,同样在Method1内定义一个局部变量num2,调用加方法得到最后的值,所以在进入Method2前,“内存图”如下,num是参数,num2是局部变量
接着调用Method2的过程雷同,然后退出Method2方法,回到上图的样子,再退出Method1方法,再回到第一副图的样子,然后退出程序,整个过程如下图:
所以去除那些if,for,多线程等等概念,只保留对象内存分配相关概念的话,程序的执行可以简单总结为如下:
程序由Main方法进入执行,并不断重复着“定义局部变量,调用方法(可能会传参),从方法返回”,最后从Main方法退出。在程序执行过程中,不断压入参数和局部变量到线程栈里,也不断的出栈。
注意,其实压入栈的还有方法的返回地址等,这里忽略了。
引用类型和堆上面的例子我只用了一种简单的int值类型,目的是为了只关注线程栈的压栈(生长)和出栈(消亡)。很明显C#还有种引用类型,引入引用类型,再考虑上面的问题,看下面代码:
1 2 3 4 5 6 7 8 9 10 11 12
static void Main(string[] args) { var user = new User { Age = 15 }; var num = 23; Console.WriteLine(user.Age); Console.WriteLine(num); } class User { public int Age; }
我想很多人都应该知道,这时应该引入托管堆的概念了,但这里我想跟上面一样,先从栈的角度去考虑问题,所以在调用WriteLine前,“内存图”应该是这样的(地址是乱写的):
这也就是人们常说的:对于引用类型,栈里保存的是指向在堆里的实例对象的地址(指针,引用)。既然只是个地址,那么要获取一个对象的实例应该有一个根据地址或寻找对象的步骤,而事实正是这样,如果Console.WriteLine(num),这样获取栈里的num的值给WriteLine方法算一步的话,要获取上面user的实例对象,在运行时是要分两步的,也就是多了根据地址去寻找托管堆里实例对象的字段或方法的步骤。IL反编译上面的Main方法,删去一些无关代码后:
1 2 3 4 5
//load local 0=>获取局部变量0(是一个地址) IL_0012: ldloc.0 // load field => 将指定对象中字段的值推送到堆栈上。 IL_0013: ldfld int32 CILDemo.Program/User::Age IL_0018: call void [mscorlib]System.Console::WriteLine(int32)
1 2 3
//load local 1=>获取局部变量1(是一个值) IL_001e: ldloc.1 IL_001f: call void [mscorlib]System.Console::WriteLine(int32)
温馨提示: 本文由Jm博客推荐,转载请保留链接: https://www.jmwww.net/file/68226.html