[译文]C# Heap(ing) Vs Stack(ing) in .NET: Part II

原文地址:http://www.c-sharpcorner.com/UploadFile/rmcochran/csharp_memory2B01142006125918PM/csharp_memory2B.aspx

PartI中,我们讨论了堆和栈的基本功能以及程序执行时,值类型和引用类型是如何被分配内存的。我们还讨论了什么是指针。

Parameters, the Big Picture

下面是当我们代码执行时,发生哪些事情的详细视图。PartI我们讨论了基本的内容,这部分我们将更加深入……

当我们调用一个方法时,下面是所发生的事情:

1. 为执行方法所需要的信息分配内存空间(称作栈帧(Stack Frame)),这其中包括调用地址(一个指针),该地址是一个GOTO指令,用于告诉程序,当这个方法调用完毕后,程序该从哪里继续执行。

2. 方法的参数将被拷贝。这是我们将进一步讨论的。

3. 控制将传递给JIT编译后的方法,然后线程开始执行代码。因此,我们有另外一个位于“调用栈”(call stack)上的代表栈帧(stack frame)的方法(原文:Hence, we have another method represented by a stack frame on the "call stack".)。

代码:

public int AddFive(int pValue)
          {
                int result;
                result = pValue + 5;
                return result;
          }

其对应的栈图是这样的:

正如PartI所讨论的,位于栈上的参数根据值类型和引用类型的不同将有不同的处理方式。

传递值类型

当我们传递值类型的时候,将分配新的内存空间,同时,我们的值将被拷贝到栈上的新内存空间。让我们看下下面的方法:

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.

然后,AddFive()被放到栈上,同时为它的参数开辟空间,将x的值拷贝给该参数。

AddFive()方法执行完毕后,线程重新传回到Go()方法上,由于AddFive()已经执行完成,pValue就被完全的移除掉了:

因此,上述代码的返回值为5,对吧?这个问题的关键点是,我们传递给方法的值类型参数都只是一个副本,原始的变量得以保留。

我们需要记住的一点是,如果我们将一个很大的值类型(如,一个大的struct)传递到栈上,那么这将花费巨大的空间和处理周期。栈上的空间并不是无限的,正如我们从水龙头给杯子加水,它是会溢出来的。一个结构体(struct)可能是非常大的,我们需要注意如何处理它。

下面是一个大的结构体:

public struct MyStruct

           {
               long a, b, c, d, e, f, g, h, i, j, k, l, m;
           }
 
如果我们这样来处理该结构体:
 
public void Go()

          {
             MyStruct x = new MyStruct();

             DoSomething(x);              

          }

           public void DoSomething(MyStruct pValue)

           {
                    // DO SOMETHING HERE....
           }

 

这将是非常低效的。试想一下,如果我们将MyStruct传递上千次,那么我们的程序将被搞得无法动弹。

 

那么,我们该如何来解决这个问题呢?

我们可以通过传递原来值类型的一个引用,如下:

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。来看下面的例子:

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;
          }

我们的输出结果就是12345,而不是5了。因为pValue和x事实上共享同一块内存空间,我们对pValue.a的改变,同样会改变x.a的值。

传递应用类型

传递引用类型和通过引用传递值类型是类似的.

如果我们使用引用类型:

public class MyInt

           {

               public int MyValue;

           }

然后调用Go()方法,那么MyInt将被放在堆上,因为它是一个引用类型:

public void Go()

          {
             MyInt x = new 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;
           }

我们得到的将是:

    1. 开始调用Go(),变量x位于栈上;

    2. 开始调用DoSomething(),参数pValue位于栈上;

    3. x的值(位于栈上的MyInt的地址)被拷贝给pValue

因此,显然的,当我们通过pValue改变位于堆上的MyInt的属性MyValue后,再通过x去访问堆上的对象时,我们得到的值就是”12345”.

 

有趣的事情是:当我们通过引用传递引用类型的时候,会发生怎样的事情呢?

 

我们来检测一下。假设我们有一个Thing类,Animal和Vegetable都继承自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的地址

 

如果我们不是通过引用传递Thing,我们将得到相反的结果。

 

总结

我们已经讨论了参数传递在内存中是如何处理的,现在也知道该留心哪些东西了。

在下一节了,我们将探讨位于栈上的引用型变量(reference variables),以及如何攻克对象拷贝时的一些问题。

 

待续……

原文地址:https://www.cnblogs.com/tian2010/p/2497711.html