深入C#内存管理来分析值类型引用类型,装箱拆箱,堆栈几个概念组

C#初学者经常被问的几道辨析题,值类型与引用类型,装箱与拆箱,堆栈,这几个概念组合之间区别,看完此篇应该可以解惑。

  俗话说,用思想编程的是文艺程序猿,用经验编程的是普通程序猿,用复制粘贴编程的是2B程序猿,开个玩笑^_^。

  相信有过C#面试经历的人,对下面这句话一定不陌生:

  值类型直接存储其值,引用类型存储对值的引用,值类型存在堆栈上,引用类型存储在托管堆上,值类型转为引用类型叫做装箱,引用类型转为值类型叫拆箱。

  但仅仅背过这句话是不够的。

  C#程序员不必手工管理内存,但要编写高效的代码,就仍需理解后台发生的事情。

  在学校的时候老师们最常说的一句话是:概念不清。最简单的例子,我熟记了所有的微积分公式,遇到题就套公式,但一样会有套不上解不出的,因为我根本不清楚公式是怎么推导出来的,基本的原理没弄清楚。

  (有人死了,是为了让我们好好的活着;有人死了,也不让人好好活:牛顿和莱布尼茨=。=)。

  有点扯远了。下面大家来跟我一起探讨下C#堆栈与托管堆的工作方式,深入到内存中来了解C#的以上几个基本概念。

一,stack与heap在不同领域的概念

  在C/C++中:

  Stack叫做栈区,由编译器自动分配释放,存放函数的参数值,局部变量的值等。

Heap则称之为堆区,由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。

而在C#中:

  Stack是指堆栈,Heap是指托管堆,不同语言叫法不同,概念稍有差别。(此处若有错误,请指正)。

  这里最需要搞清楚的是在语言中stack与heap指的是内存中的某一个区域,区别于数据结构中的栈(后进先出的线性表),堆(经过某种排序的二叉树)。

  讲一个概念之前,首先要说明它所处的背景。

  若无特别说明,这篇文章讲的堆栈指的就是Stack,托管堆指的就是Heap。

二,C#堆栈的工作方式

  Windwos使用虚拟寻址系统,把程序可用的内存地址映射到硬件内存中的实际地址,其作用是32位处理器上的每个进程都可以使用4GB的内存-无论计算机上有多少硬盘空间(在64位处理器上,这个数字更大些)。这4GB内存包含了程序的所有部份-可执行代码,加载的DLL,所有的变量。这4GB内存称为虚拟内存。

  4GB的每个存储单元都是从0开始往上排的。要访问内存某个空间存储的值。就需要提供该存储单元的数字。在高级语言中,编译器会把我们可以理解的名称转换为处理器可以理解的内存地址。

  在进程的虚拟内存中,有一个区域称为堆栈,用来存储值类型。另外在调用一个方法时,将使用堆栈复制传递给方法的所有参数。

  我们注意一下C#中变量的作用域,如果变量a在变量b之前进入作用域,b就会先出作用域。看下面的例子:

{int a;//do something {int b;//do something }}

  声明了a之后,在内部代码块中声明了b,然后内部代码块终止,b就出了作用域,然后a才出作用域。在释放变量的时候,其顺序总是与给它们分配内存的顺序相反,后进先出,是不是让你想到了数据结构中的栈(LIFO–Last IN First Out)。这就是堆栈的工作方式。

  我们不知道堆栈在地址空间的什么地方,其实C#开发是不需要知道这些的。

  堆栈指针,一个由操作系统维护的变量,指向堆栈中下一个自由空间的地址。程序第一次运行时,堆栈指针就指向为堆栈保留的内存块的末尾。

  堆栈是向下填充的,即从高地址向低地址填充。当数据入栈后,堆栈指针就会随之调整,指向下一个自由空间。我们来举个例子说明。

  如图,堆栈指针800000,下一个自由空间是799999。

下面的代码会告诉编译器需要一些存储单元来存储一个整数和一个双精度浮点数。

{int a=1;double b = 1.1;//do something}

  这两个都是值类型,自然是存储在堆栈中。声明a赋值1后,a进入作用域。int类型需要4个字节,a就存储在799996~799999上。此时,堆栈指针就减4,指向新的已用空间的末尾799996,下一个自由空间为799995。下一行声明b赋值1.1后,double需要占用8个字节,所以存储在799988~799995上,堆栈指针减去8。

  当b出作用域时,计算机就知道这个变量已经不需要了。变量的生存期总是嵌套的,当b在作用域的时候,无论发生什么事情,都可以保证堆栈指针一直指向存储b的空间。

  删除这个b变量的时候堆栈指针递增8,现在指向b曾经使用过的空间,此处就是放置闭合花括号的地方。然后a也出作用域,堆栈指针再递增4。

  此时如果放入新的变量,从799999开始的存储单元就会被覆盖了。

二,托管堆的工作方式

  堆栈有灰常高的性能,但要求变量的生命周期必须嵌套(后进先出决定的),在很多情况下,这种要求很过分。。。通常我们希望使用一个方法来分配内存,来存储一些数据,并在方法退出后很长的一段时间内数据仍是可用的。用new运算符来请求空间,就存在这种可能性-例如所有引用类型。这时候就要用到托管堆了。

不敢面对自己的不完美,总是担心自己的失败,

深入C#内存管理来分析值类型引用类型,装箱拆箱,堆栈几个概念组

相关文章:

你感兴趣的文章:

标签云: