C# 中的栈和堆
本文是《C# 图解教程》的笔记。
程序运行时,它的数据必须存储在内存中。一个数据项需要多大的内存、存储在什么地方、以及如何存储都依赖于该数据项的类型。
运行中的程序使用两个内存区域来存储数据:栈和堆。
栈
栈是一个内存数组,是一个LIFO(Last-In First-Out,后进先出)的数据结构。栈存储几种类型的数据:
- 某些类型变量的值
- 程序当前的执行环境
- 传递给方法的参数
系统管理所有的栈操作。作为程序员,你不需要显式地对它做任何事情。但了解栈的基本功能可以更好地了解程序在运行时正在做什么,并能更好地了解C#文档和著作。
image堆
堆是一块内存区域,在堆里可以分配大块的内存用于存储某类型的数据对象。与栈不同,堆里的内存能够以任意顺序存入和移除。图3-7展示了一个在堆里放了4项数据的程序。
image虽然程序可以在堆里保存数据,但并不能显式地删除它们。CLR的自动GC(Garbage Collector,垃圾收集器)在判断出程序的代码将不会再访问某数据项时,自动清除无主的堆对象。我们因此可以不再操心这项使用其他编程语言时非常容易出错的工作了。
image值类型和引用类型
数据项的类型定义了存储数据需要的内存大小及组成该类型的数据成员。类型还决定了对象在内存中的存储位置——栈或堆。
类型被分为两种:值类型和引用类型,这两种类型的对象在内存中的存储方式不同。
- 值类型只需要一段单独的内存,用于存储实际的数据。
- 引用类型需要两段内存。
- 第一段存储实际的数据,它总是位于堆中。
- 第二段是一个引用,指向数据在堆中的存放位置。
存储引用类型对象的成员
例如,假设有一个引用类型的实例,名称为MyType,它有两个成员:一个值类型成员和一个引用类型成员。它将如何存储呢?是否是值类型的成员存储在栈里,而引用类型的成员如图3-9所示的那样在栈和堆之间分成两半呢?答案是否定的。
请记住,对于一个引用类型,其实例的数据部分始终存放在堆里。既然两个成员都是对象数据的一部分,那么它们都会被存放在堆里,无论它们是值类型还是引用类型。
image对于引用类型的任何对象,它所有的数据成员都存放在堆里,无论它们是值类型还是引用类型。
C#类型的分类
image数组
数组是引用类型,与所有引用类型一样,有数据的引用以及数据对象本身。引用在栈或堆上,而数组对象本身总是在堆上。
image尽管数组总是引用类型,但是数组的元素可以是值类型也可以是引用类型。
- 如果存储的元素都是值类型,数组被称作值类型数组。
- 如果存储在数组中的元素都是引用类型对象,数组被称作引用类型数组。
方法
方法的局部变量和参数是位于栈上的。
class MyClass
{
public void ListInts(params int[] inVals)
{
if ((inVals != null) && (inVals.Length != 0))
for (int i = 0; i < inVals.Length; i++) //处理数组
{
inVals[i] = inVals[i] * 10;
Console.WriteLine("{0}", inVals[i]); //显示新值
}
}
}
class Program
{
static void Main()
{
int first = 5, second = 6, third = 7; //声明3个int
MyClass mc = new MyClass();
mc.ListInts(first, second, third); //调用方法
Console.WriteLine("{0}, {1}, {2}", first, second, third);
}
}
输出
50
60
70
5, 6, 7
image
栈帧
在调用方法的时候,内存从栈的顶部开始分配,保存和方法关联的一些数据项。这块内存叫做方法的栈帧(stack frame)。
-
栈帧包含的内存保存如下内容
- 返回地址,也就是在方法退出的时候继续执行的位置。
- 这些参数分配的内存,也就是方法的值参数,或者还可能是参数数组(如果有的话)。
- 各种和方法调用相关的其他管理数据项。
-
在方法调用时,整个栈帧都会压入栈。
-
在方法退出的时候,整个栈帧都会从栈上弹出。弹出栈帧有的时候也叫做栈展开(unwind)。
class Program
{
static void MethodA(int par1, int par2)
{
Console.WriteLine("Enter MethodA: {0}, {1}", par1, par2);
MethodB(11, 18); //调用MethodB
Console.WriteLine("Exit MethodA");
}
static void MethodB(int par1, int par2)
{
Console.WriteLine("Enter MethodB: {0}, {1}", par1, par2);
Console.WriteLine("Exit MethodB");
}
static void Main()
{
Console.WriteLine("Enter Main");
MethodA(15, 30); //调用MethodA
Console.WriteLine("Exit Main");
}
}
输出
Enter Main
Enter MethodA: 15, 30
Enter MethodB: 11, 18
Exit MethodB
Exit MethodA
Exit Main
image
递归
除了调用其他方法,方法也可以调用自身。这叫做递归。
调用方法自身的机制和调用其他方法其实是完全一样的。都是为每一次方法调用把新的栈帧压入栈顶。
class Program
{
public void Count(int inVal)
{
if (inVal == 0)
return;
Count(inVal - 1); //再一次调用方法
Console.WriteLine("{0}", inVal);
}
static void Main()
{
Program pr = new Program();
pr.Count(3);
}
}
image