在上一部分中覆盖了堆栈的基本功能以及程序执行中变量类型和引用类型的分配。也讲到了指针的基本概念。
参数
这是代码执行时发生的详细情况。在第一部分中也提到了函数被调用时发生的事情,接下来我们就深入到细节
当函数被调用时发生了什么:
- 在栈上为必要的函数信息分配内存(叫做栈帧),这包括调用地址(一个指针),主要是一个GOTO指令,当线程结束执行函数后知道应该返回到哪里继续执行。
- 函数参数被copy,这也是我们想要近距离观察的点。
- 控制权交给JIT并且开始执行代码。此后,在调用栈中就增加了一帧。
代码:
public int AddFive(int pValue)
{
int result;
result = pValue + 5;
return result;
}
它的栈看起来是这样的:
注意:函数并不在栈上,只是示意一下这是一个方法的引用,栈帧从这开始。
在第一节讨论过,参数在栈上的分配取决于它是值类型还是引用类型。值类型被直接复制过来而引用类型则是一个指针。
传递值类型
下面是一个值类型的细节
首先,当传入一个值类型时,会在栈上分配一个空间并将参数copy进去,看下面的代码:
class Class1
{
public void Go()
{
int x = 5;
AddFive(x);
Console.WriteLine(x.ToString());
}
public int AddFive(int pValue)
{
pValue += 5;
return pValue;
}
}
代码执行时,为变量"x"分配栈空间,值是5
下一步,AddFile()函数和它的参数被分配到栈空间上,参数被完整的从x变量复制过来
当AddFive()函数结束执行,线程回到GO()函数,因为AddFive()执行完了,pValue这个参数被移除
所以,输出理应是5。需要强调的是值类型的参数在传入函数时是完全copy的,并且原来的变量也会被保留。
一个需要注意的是如果我们要传一个非常大的值类型参数(比如一个大结构体)到栈里,每次消耗的空间和处理周期是相当大的。栈并不是无穷大,并且它会溢出。结构体是一个可以很大值类型,并且我们要知道如何处理它。
下面是一个大结构体:
public struct MyStruct
{
long a, b, c, d, e, f, g, h, i, j, k, l, m;
}
看一下当执行Go()运行到DoSomething()时的情形
public void Go()
{
MyStruct x = new MyStruct();
DoSomething(x);
}
public void DoSomething(MyStruct pValue)
{
// DO SOMETHING HERE....
}
它是低效的,想象一下如果把这个结构体传递成百上千次,就会明白这种事有多屎。解决这个问题的方式就是传递一个值类型的引用。如下:
public void Go()
{
MyStruct x = new MyStruct();
DoSomething(ref x);
}
public struct MyStruct
{
long a, b, c, d, e, f, g, h, i, j, k, l, m;
}
public void DoSomething(ref MyStruct pValue)
{
// DO SOMETHING HERE....
}
用这种方法避免了反复开辟空间
传递值类型引用有一点需要注意,我们使用的是原值。任何对pValue的更改都会改变x。下面代码会返回“12345”,因为pValue和x指向的是同一个值类型。
public void Go()
{
MyStruct x = new MyStruct();
x.a = 5;
DoSomething(ref x);
Console.WriteLine(x.a.ToString());
}
public void DoSomething(ref MyStruct pValue)
{
pValue.a = 12345;
}
传递引用类型
传递引用类型的参数和上面提到的,传递值类型的引用很像。
public class MyInt
{
public int MyValue;
}
调用Go()方法MyInt被分配在堆上,因为它是引用类型
如果以下面方式调用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(),变量x入栈
- 开始调用DoSomething(),单数pValue入栈
- x的值copy给pValue
所以,当我们使用pValue改变在堆中的MyInt类中的MyValue属性时,我们再通过变量x访问这个对象,MyValue变成了“12345”,这就有意思了,我们传递引用的引用会发生什么?
看一下,假设有类Thing,Animal和Vegetables都继承Thing:
public class Thing
{
}
public class Animal:Thing
{
public int Weight;
}
public class Vegetable:Thing
{
public int Length;
}
像下面这样执行Go()方法
public void Go()
{
Thing x = new Animal();
Switcharoo(ref x);
Console.WriteLine(
"x is Animal : "
+ (x is Animal).ToString());
Console.WriteLine(
"x is Vegetable : "
+ (x is Vegetable).ToString());
}
public void Switcharoo(ref Thing pValue)
{
pValue = new Vegetable();
}
变量x变成了Vegetable
x is Animal : False
x is Vegetable : True
我们来看一下发生了什么
1. 开始这行Go(),x变量作为一个指针入栈
2. 在堆上创建Animal
3. 调用Switcharoo()方法,pValue入栈并指向x
4. 在堆上创建Vegetable
5. x的值被通过pValue改成了Vegetable的地址
如果不通过ref关键字传递Thing,仍然会保留Animal并得到相反的结果。
原文链接:
https://www.c-sharpcorner.com/article/C-Sharp-heaping-vs-stacking-in-net-part-ii/