C# 关于堆,堆栈的总结

原帖地址:http://blog.csdn.net/baoxuetianxia/article/details/3218913

原帖地址:http://www.cr173.com/html/17291_1.html

本文是参考上面两篇文章以及自己的理解进行的总结

一、概念

堆栈stack:

      堆栈中存储值类型。

      堆栈实际上是向下填充,即由高内存地址指向低内存地址填充。
      堆栈的工作方式是先分配内存的变量后释放(先进后出原则)。
      堆栈中的变量是从下向上释放,这样就保证了堆栈中先进后出的规则不与变量的生命周期起冲突!
      堆栈的性能非常高,但是对于所有的变量来说还不太灵活,而且变量的生命周期必须嵌套。

通常我们希望使用一种方法分配内存来存储数据,并且方法退出后很长一段时间内数据仍然可以使用。此时就要用到堆托管堆!

堆(托管堆)heap:
堆(托管堆)存储引用类型。
此堆非彼堆,.NET中的堆由垃圾收集器自动管理。
与堆栈不同,堆是从下往上分配,所以自由的空间都在已用空间的上面。

通过比较来理解堆和堆栈的区别:
Customer cus;
cus = new Customer();
申明一个Customer的引用cus,在 堆栈 上给这个引用分配存储空间。这仅仅只是一个引用,不是实际的Customer对象!
cus占4个字节的空间,包含了存储Customer的引用地址。
接着分配 上的内存以存储Customer对象的实例,假定Customer对象的实例是32字节,为了在堆上找到一个存储Customer对象的存储位置。
.NET运行库在堆中搜索第一个从未使用的,32字节的连续块存储Customer对象的实例!
然后把分配给Customer对象实例的地址赋给cus变量!
从这个例子中可以看出,建立对象引用的过程比建立值变量的过程复杂,且不能避免性能的降低!
实际上就是.NET运行库保存对的状态信息,在堆中添加新数据时,堆栈中的引用变量也要更新。性能上损失很多!
有种机制在分配变量内存的时候,不会受到堆栈的限制:把一个引用变量的值赋给一个相同类型的变量,那么这两个变量就引用同一个堆中的对象。
当一个应用变量出作用域时,它会从堆栈中删除。但引用对象的数据仍然保留在堆中,一直到程序结束 或者 该数据不被任何变量应用时,垃圾收集器会删除它。

  二、数值类型

值类型:
在C#中,继承自System.ValueType的类型被称为值类型,主要有以下几种(CLR2.0中支持类型有增加):
* bool
* byte
* char
* decimal
* double
* enum
* float
* int
* long
* sbyte
* short
* struct
* uint
* ulong
* ushort

引用类型:
以下是引用类型,继承自System.Object:
* class
* interface
* delegate
* object
* string

三、内存的分配

扫描二维码关注公众号,回复: 5674188 查看本文章
值类型变量与引用类型变量的内存分配模型也不一样。为了理解清楚这个问题,读者首先必须区分两种不同类型的内存区域:线程堆栈(Thread Stack)和托管堆(Managed Heap)。每个正在运行的程序都对应着一个进程(process),在一个进程内部,可以有一个或多个线程(thread),每个线程都拥有一块“自留地”,称为“线程堆栈”,大小为1M,用于保存自身的一些数据,比如函数中定义的局部变量、函数调用时传送的参数值等,这部分内存区域的分配与回收不需要程序员干涉。所有值类型的变量都是在线程堆栈中分配的。另一块内存区域称为“堆(heap)”,在.NET 这种托管环境下,堆由CLR 进行管理,所以又称为“托管堆(managed heap)”。用new 关键字创建的类的对象时,分配给对象的内存单元就位于托管堆中。在程序中我们可以随意地使用new 关键字创建多个对象,因此,托管堆中的内存资源是可以动态申请并使用的,当然用完了必须归还。打个比方更易理解:托管堆相当于一个旅馆,其中的房间相当于托管堆中所拥有的内存单元。当程序员用new 方法创建对象时,相当于游客向旅馆预订房间,旅馆管理员会先看一下有没有合适的空房间,有的话,就可以将此房间提供给游客住宿。当游客旅途结束,要办理退房手续,房间又可以为其他旅客提供服务了。

所有引用类型变量所引用的对象,其内存都是在托管堆中分配的。


  内存格局通常分为四个区:
  全局数据区:存放全局变量,静态数据,常量
  代码区:存放所有的程序代码
  栈区:存放为运行而分配的局部变量,参数,返回数据,返回地址等,
  堆区:即自由存储区,
四、具体实例

1、简单实例

装箱转化
using System;
class Boxing
{
  public static void Main()
  { int i=110;
    object obj=i;
    i=220;
    Console.WriteLine("i={0},obj={1}",i,obj);
    obj=330;
    Console.WriteLine("i={0},obj={1}",i,obj);
  }
}
定义整数类型变量I的时候,这个变量占用的内存是内存栈中分配的,第二句是装箱操作将变量 110存放到了内存堆中,而定义object对象类型的变量obj则在内存栈中,并指向int类型的数值110,而该数值是付给变量i的数值副本。
所以运行结果是
i=220,obj=110
i=220,obj=330


2、再简单的实例

class A
02 {
03 public int i;
04 }
05 class Program
06 {
07 static void Main(string[] args)
08 {
09 A a ;
10 a= new A();
11 a.i = 100;
12 A b=null;
13 b = a; //对象变量的相互赋值
14 Console.WriteLine("b.i=" + b.i); //b.i=?
15 }
16 }
注意第12 和13 句。
程序的运行结果是:
b.i=100;
请读者思索一下:两个对象变量的相互赋值意味着什么?事实上,两个对象变量的相互赋值意味着赋值后两个对象变量所占用的内存单元其内容是相同的。
讲得详细一些:
第10 句创建对象以后,其首地址(假设为“1234 5678”)被放入到变量a 自身的4 个字节的内存单元中。第12 句又定义了一个对象变量b,其值最初为null(即对应的4 个字节内存单元中为“0000 0000”)。第13 句执行以后,a 变量的值被复制到b 的内存单元中,现在,b 内存单元中的值也为
“1234 5678”。根据前面介绍的对象内存模型,我们知道现在变量a 和b 都指向同一个实例对象。如果通过b.i 修改字段i 的值,a.i 也会同步变化,因为a.i 与b.i 其实代表同一对象的同一字段。
整个过程可以用图 9 来说明:
图 9 对象变量的相互赋值
由此得到一个重要结论:
对象变量的相互赋值不会导致对象自身被复制,其结果是两个对象变量指向同一对象。另外,由于对象变量本身是一个局部变量,因此,对象变量本身是位于线程堆栈中的。
严格区分对象变量与对象变量所引用的对象,是面向对象编程的关键之一。由于对象变量类似于一个对象指针,这就产生了“判断两个对象变量是否引用同一对象”
的问题。C#使用“==”运算符比对两个对象变量是否引用同一对象,“!=”比对两个对象变量22是否引用不同的对象。参看以下代码:
//a1与a2引用不同的对象
A a1= new A();
A a2= new A();
Console.WriteLine(a1 == a2);//输出:false
a2 = a1;//a1与a2引用相同的对象
Console.WriteLine(a1 == a2);//输出:true

需要注意的是,如果“==”被用在值类型的变量之间,则比对的是变量的内容:
int i = 0;
int j = 100;
if (i == j)
{
Console.WriteLine("i与j的值相等");
}
理解值类型与引用类型的区别在面向对象编程中非常关键。

3、详细过程的实例(进阶)

类型,对象,堆栈和托管堆

C#的类型和对象在应用计算机内存时,大体用到两种内存,一个叫堆栈,另一个叫托管堆,下面我们用直角长方形来代表堆栈,用圆角长方形来代表托管堆。

首先讨论一下方法内部变量的存放。
先举个例子,有如下两个方法,Method_1 和Add,分别如下:
public void Method_1()
{
int value1=10; //1
int value2=20; //2
int value3=Add(value,value); //3
}
public int Add(int n1,int n2)//4
{
rnt sum=n1+n2;//5
return sum;//6
}
这段代码的执行,用图表示为:
上述的每个图片,基本对应程序中的每个步骤。在开始执行Method_1的时候,先把value1 压入堆栈顶,然后是value2,接下来的是调用方法Add,因为方法有两个参数是n1 和n2,所以把n1 和n2 分别压入堆栈,因为此处是调用了一个方法,并且方法有返回值,所以这里需要保存Add的返回地址,然后进入Ad
d方法内部,在Add内部,首先是给sum 赋值,所以把sum 压入栈项,然后用return 返回,此时,先前的返回地址就起到了作用,return 会根据地址返回去的,在返回的过程中,把sum推出栈顶,找到了返回地址,但在Method_1 方法中,我们希望把Add的返回值赋给value3,此时的返回地址也被推出堆栈,把value3 压入堆栈。虽这个例子的结果在这里没有多大用途,但这个例子很好的说明了在方法被执行时,变量与进出堆栈的情况。这里也能看出为什么方法内部的局变量用过后,不能在其他方法中访问的原因。

其次来讨论一下类和对象在托管堆和堆栈中的情况。
先看一下代码:
class Car
{
public void Run()
{
Console.WriteLine("一切正常");
}
public virtual double GetPrice()
{
return 0;
}
public static void Purpose()
{
Console.WriteLine("载人");
}
PDF 文件使用 "pdfFactory Pro" 试用版本创建 fw w w . f i n e p rint.cn
}
class BMW : Car
{
public override double GetPrice()
{
return 800000;
}
}
上面是两个类,一个Father一个Son,Son 继承了Father,
因为你类中有一个virtual的 GetPrice()方法,所以Son类可以重
写这个方法。
下面接着看调用代码。
public void Method_A()
{
double CarPrice;//1
Car car = new BMW();//2
CarPrice = car.GetPrice();//调用虚方法(其实调用的是重写后
的方法)
car.Run();//调用实例化方法
Car.Purpose();//调用静态方法
}
这个方法也比较简单,就是定义一个变量用来获得价格,同时
定义了一个父类的变量,用子类来实例化它。
接下来,我们分步来说明。
看一下运行时堆栈和托管堆的情部我
这里需要说明的是,类是位于托管堆中的,每个类又分为四个
类部,类指针,用来关联对象;同步索引,用来完成同步(比如线
程的同步)需建立的;静态成员是属于类的,所以在类中出现,还
有一个方法列表(这里的方法列表项与具体的方法对应)。
当Method_A方法的第一步执行时:
这时的CarPrice 是没有值的
当Method_A方法执行到第二步,其实第二步又可以分成
Car car;
car = new BMW();
先看Car car;
car在这里是一个方法内部的变量,所以被压到堆栈中。
再看 car = new BMW();
这是一个实例化过程,car变成了一个对象
这里是用子类来实例化父类型。对象其实是子类的类型的,但变量的类型是父类的。接下来,在Method_A中的调用的中调用car.GetPrice(),对于Car来说,这个方法是虚方法(并且子类重写了它),虚方法在调用是不会执行类型上的方法,即不会执行Car类中的虚方法,而是执行对象对应类上的方法,即BMW中的GtPrice。
如果Method_A中执行方法Run(),因为Run是普通实例方法,所以会执行Car类中的Run 方法。如果调用了Method_A的Purpose 方法,即不用变量car调
用,也不用对象调用,而是用类名Car调用,因为静态方法会在类中分配内存的。如果用Car生成多个实例,静态成员只有一份,就是在类中,而不是在对象中。

(完)

看完这篇文章大家应该对这个基本了解了吧















猜你喜欢

转载自blog.csdn.net/yalunwang123/article/details/43530235