zoukankan      html  css  js  c++  java
  • [译文]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),以及如何攻克对象拷贝时的一些问题。

     

    待续……

  • 相关阅读:
    字符串数组 去重 研究
    监听 dom 改变
    清除body 不改变路径 页面信息加载第三方
    使用img标签实现xss的常见方法
    禁止ios浏览器页面上下滚动 (橡皮筋效果)
    遍历 Request.Form.Keys
    selenium-java之使用浏览器打开网页举例
    docker搭建redis
    docker镜像无法删除 Error:No such image:xxxxxx
    连接查询SQL说明举例
  • 原文地址:https://www.cnblogs.com/tian2010/p/2497711.html
Copyright © 2011-2022 走看看