C# 堆内存与栈内存 - 参数传递
第一部分了解了堆内存和栈内存的基本概念,以及值类型和引用了类型的内存分配,同时也了解了指针的基本情况。本节主要是介绍在函数调用时参数的传递情况。
参数概述
在第一部分了解了函数调用时内存分配的基本情况,这一部分将对做进一步详细介绍。当调用一个函数时,会发生如下操作:
1. 在栈内存上为调用的方法分配空间,这里主要包括返回地址指针和参数传递所需空间。
2. 复制方法参数,这是将要详细研究的部分。
3. 指令指针指向被调函数的JIT代码,然后开始执行代码,因此在堆栈上又分配了一个栈帧。
代码:
public int AddFive(int pValue)
{
int result;
result = pValue + 5;
return result;
}
调用该函数时,堆栈类似下面:
在第一部分中讨论过,参数内存的分配与参数是值类型还是引用类型有关,值类型直接进行拷贝,而引用类型参数拷贝参数的引用(指针)。
传递值类型参数
首先,当传递值类型参数时,会在栈内存上分配空间,并将值复制到分配的空间,通过下面例子看下具体情况。
public class Class1
{
public void Go()
{
int x = 5;
AddFive(x);
Console.WriteLine(x.ToString());
}
public int AddFive(int pValue)
{
pValue += 5;
return pValue;
}
}
当调用 “Go” 方法时,在栈内存上为局部变量 x 分配空间,并将值 5 拷贝到分配的空间。栈内存变化如下图:
接下来,当调用 AddFive 时,记录函数的返回地址,并为AddFive所需内存(局部变量、参数)并将值复制到分配的地址。
当 AddFive 调用结束之后,根据上一步记录的返回地址,返回到Go函数中;并且将 AddFive调用时分配的栈内存空间进行释放。
所以最终 x 的值为5,主要原因在于,所有值类型参数在传递时都是传递的副本,而非直接修改原始值。基于这一点,我们需要知道,当传递一个非常大的值类型时(比如一个struct),每次复制都非常的消耗空间和时间,栈内存空间是有限的,所以在使用struct时,需要慎重考虑。
通过一个例子来看一下结构体做参数时的情况:
public struct MyStruct
{
public long a,b,c,d,e,f,g,h,i,j,k,l,m;
}
public class Program
{
public void Go()
{
MyStruct x = new MyStruct();
DoSomething(x);
}
public void DoSomething(MyStruct pValue)
{
//Do something here...
}
}
当执行 Go() 方法时,栈内存会发生如下变化:
这种方式是非常低效的,所以一种更好的替代方法是,通过传引用的方式传递结构体参数,下面对上面代码进行下修改:
public class Program
{
public void Go()
{
MyStruct x = new MyStruct();
DoSomething(ref x);
}
public void DoSomething(ref MyStruct pValue)
{
//Do something here...
}
}
此种方式减少了不必要内存的消耗,调用 Go 时栈内存变化如下:
当选择传值或者按引用传递时,需要注意对象值得变化,区分清楚再函数内修改对象时副本还是对象本身。
传递引用类型参数
传递引用类型参数,和按引用传递值类型参数时相似的。
public class MyInt
{
public int myValue;
}
public class Program
{
public void Go()
{
MyInt x = new MyInt();
}
}
当执行 Go() 函数时,堆栈内存变化如下:
下面修改一下 Go 函数的内容
public void Go()
{
MyInt x = new MyInt();
x.myValue = 2;
DoSomething(x);
Console.WriteLine(x.myValue.ToString());
}
public void DoSomething(MyInt pValue)
{
pValue.myValue = 12345;
}
此时在调用 Go 函数时,堆栈变化如下:
具体过程如下:
1. 在Go 方法开始,在堆内存上为 x 分配内存,并将内存地址复制到栈内存。
2. 在开始调用 DoSomething 时,参数pValue 的将在栈内存进行分配。
3. x 对象的指针值复制到 pValue内存。
所以当在 DoSomething 函数中修改MyInt属性myValue时,修改的其实是堆内存上的内容,所以在调用Console.WriteLine() 时输出内容是:12345
按引用传递引用类型参数
下面研究一下按引用传递引用类型时会发生什么,看下下面的代码。
public class Thing{}
public class Animal:Thing{}
public class Vegetable:Thing{}
public void Go()
{
Thing x = new Animal();
Switcharoo(ref x);
Console.WriteLine("x is Vegetable : "+(x is Animal).ToString());
Console.WriteLine("x is Vegetable : "+(x is Vegetable).ToString());
}
public void Switcharoo(ref Thing pValue)
{
pValue = new Vegetable();
}
输出结果:
x is Animal : False
x is Vegetable : True
看下堆栈内存发生了什么变化:
具体步骤:
1. 开始调用 Go 函数时,在栈内存在 x 指针分配空间。
2. 然后将 Animal 对象的地址,复制到 x 指针地址。
3. 开始调用 Switchroo 函数,将 x 的地址复制到 pValue 内存中。
4. 在 堆内存 上为 Vegetable 分配内存。
5. 利用 pValue,使 x 指向刚分配的 Vegetable 内存地址。
如果不是按引用传递 x,x的值就不会改变。如果对上面的例子还有上面疑惑,可以参考上一篇 增加一下对按引用传值的理解。
总结
大致总结一下这一部分内容:
1. 传递值类型参数时,将实参的副本传递给函数。
2. 传递引用类型参数时,将引用对象地址传递给函数。
3. 当按引用传递引用类型参数时,将栈内存地址(保存着对象在堆内存的地址),传递给函数。
通过本节介绍,对于内存如何处理参数传递,应该有了一个比较直观的理解,直到如何处理一些参数传递相关问题。