译文---C#堆VS栈(Part Two)

前言

在本系列的第一篇文章《C#堆栈对比(Part One)》中,介绍了堆栈的基本功能和值类型以及引用类型在程序运行时的表现,同时也包含了指针作用的讲解。

本文为文章的第二部分,主要讲解参数在堆栈的作用。

注:限于本人英文理解能力,以及技术经验,文中如有错误之处,还请各位不吝指出。

目录

C#堆栈对比(Part One)

C#堆栈对比(Part Two

C#堆栈对比(Part Three)

C#堆栈对比(Part Four)

参数---重点讨论事项

这就是当我们执行代码时的详细情况。我们在第一步已经讲述了调用方法时所发生的情况,现在让我们来看看更多细节…

当我们调用方法时,如下事情将发生:

  1. 当我们执行一个方法时需要在栈上创建一个空间。这包含了一个GOTO指令的地址调用(指针),所以当线程执行完我们的方法后它知道如何返回并继续执行程序。
  2. 我们方法的参数将被拷贝。这就是我们要仔细去研究的东西。
  3. Control is passed to the JIT‘ted method and the thread starts executing code. 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;
 }

  栈将会是这样:

  注:方法并不真正在栈上,这里只是举例演示说明。

  正如我们Part One中所讨论的,栈上的参数将被不同的方式处理,处理的方式又取决于它是值类型,还是引用类型。值类型是复制拷贝,引用类型是在传递引用本身。(A value types is copied over and the reference of a reference type is copied over.ed over.)

  注:值类型是完全拷贝(复制)对象,新对象的值改变与否与影响原值;引用类型则拷贝的仅仅是指向类型的指针,在内存中共享同一个对象。

值类型传递

  下面我们将讨论值类型…

  首先,当我们传递值类型时,空间将被创建并且将复制我们的类型到栈中的一个新空间,让我们来分析如下代码:

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自然会被回收,如下图:

  注:此处线程指针回退到Go方法后临时变量pValue将被回收,即下图中的灰色模块。

  所以,正确的输出是5,对吗?重点的是,任何值类型被作为参数传递到一个方法时要进行一个全拷贝复制(carbon copy)并且原变量的值被保存下来而不受影响(we count on the original variable‘s value to be preserved.)。

  我们必须记住的是,如果我们有一个很大的值类型(例如很大的一个结构体)并且将它作为参数传递至方法时,每次它将被拷贝复制并且花费很大的内存和CPU时间。栈的空间是有限的,正如从水龙头往杯里灌水一样,它总会溢出的。结构体是值类型,可能会非常大,我们在使用时必须要注意。

  注:这里可以将结构体理解为一种值类型,在其作为参数传递至方法时,必然会进行复制拷贝,这样如果结构体很占空间的话,则必然引起空间上以及内存上的效率问题,这点必须引起重视。

  下面就是一个很大的结构体:

public struct MyStruct
{
       long a, b, c, d, e, f, g, h, i, j, k, l, m;
 }

  接下来,让我们看看当执行Go方法时发生了什么:

public void Go()
{
             MyStruct x = new MyStruct();
             DoSomething(x);

}

public void DoSomething(MyStruct pValue)
{
              // DO SOMETHING HERE....
}

  这将是非常没有效率的。想象一下,如果我们传递12000次,你就能理解为什么效率如此低下。

  那么,我们如何绕开这个问题呢?答案就是,传递一个指向值类型的引用。如下所示:

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

  这样,通过ref引用结构体之后我们将有效率的使用内存。

  当我们用引用的方式传递值类型时,我们仅需关注值类型值的改变。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对象最终处于堆上,因为它是引用类型:

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。

  所以,当我们改变堆上的MyValue内的pValue之后我们再调用x,将会得到“12345”。

  这就是十分有趣的地方。用引用的方式传递引用类型时发生了什么?

  仔细讨论一下。如果我们有“物体”(Thing Class),动物,蔬菜这几类事物:

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 is Animal    :   False
  x is Vegetable :   True

  接下来,让我们看看发生了什么,如下图:

  1. 开始执行Go方法,x指针在栈上被初始化。
  2. Animal类型在堆上被创建。
  3. 开始执行Switchroo方法,pValue在栈上被创建并指向x

  4. Vegetable类被创建在堆上。

  5. 更改x指针并指向Vegetable类型。

  如果我们没有用ref关键字传递“事物”(Thing),我们将保持Animal并从代码中得到想反的结果。

如果没有理解以上代码,请参考我的类型引用段落,这样能更好的理解引用类型如何工作的。

  注:当声明参数带有ref关键字时,引用类型传递的是引用类型的指针,相反如果没有ref关键字,参数传递的是新的指向引用内容的指针(引用)。在作者的例子中当存在ref关键字时,传递的是x(指针),如果Swtichroo方法不使用ref关键字时,实际是直接指向Animal。

  读者可去掉ref关键字,编译即可,输出结果则为:

  x is Animal    :   True
  x is Vegetable :
   False

  与原文答案正相反。

总结

  Part Two关注参数传递时在内存中的不同,在下一个部分,让我们看看在栈上的引用变量以及克服一些当我们拷贝对象时产生的问题。

  1.  值类型当参数时,复制拷贝为一个栈上的新对象,使用后回收。

  2.  值类型当参数时,会发生拷贝现象,所以对一些“很大”的结构体类型会产生很严重的效率问题,可尝试用ref 关键字将结构体包装成引用类型进行传递,节省空间及时间。

  3.  引用类型传递的是引用地址,即多个事物指向同一个内存块,如果更改内存中的值将同时反馈到所有其引用的对象上。

  4.  Ref关键字传递的是引用类型的指针,而非引用类型地址。

时间: 2024-08-10 01:37:34

译文---C#堆VS栈(Part Two)的相关文章

译文---C#堆VS栈(Part One)

原文:译文---C#堆VS栈(Part One) 前言 本文主要是讲解C#语言在内存中堆.栈的使用情况,使读者能更好的理解值类型.引用类型以及线程栈.托管堆. 首先感谢原文作者:Matthew Cochran 为我们带来了一篇非常好的文章,并配以大量图示,帮助我们更好的理解堆栈之间的调用,本文是在作者原文的基础上进行内容上的精简以及加入我个人在这方面的理解和注释. 最后要感谢博客园的田志良,当我搜索堆栈内部使用时,搜索到了作者的文章,吸取了大量有用的知识,而且翻译的也非常好.唯一美中不足的可能是

译文---C#堆VS栈(Part Three)

前言 在本系列的第一篇文章<C#堆栈对比(Part Two)>中,介绍了值类型和引用类型在参数传递时的不同,本文将讨论如何应用ICloneable接口实现去修复引在堆上的用变量所带来的问题. 本文是系列文章的第三部分. 注:限于本人英文理解能力,以及技术经验,文中如有错误之处,还请各位不吝指出. 目录 C#堆栈对比(Part One) C#堆栈对比(Part Two) C#堆栈对比(Part Three) C#堆栈对比(Part Four) 拷贝不是复制那么简单 为了更清楚的表达这个问题,我们

JVM学习(2)——技术文章里常说的堆,栈,堆栈到底是什么,从os的角度总结--转载http://www.cnblogs.com/kubixuesheng/p/5202561.html

转载自---http://www.cnblogs.com/kubixuesheng/p/5202561.html 俗话说,自己写的代码,6个月后也是别人的代码--复习!复习!复习!涉及到的知识点总结如下: 堆栈是栈 JVM栈和本地方法栈划分 Java中的堆,栈和c/c++中的堆,栈 数据结构层面的堆,栈 os层面的堆,栈 JVM的堆,栈和os如何对应 为啥方法的调用需要栈 属于月经问题了,正好碰上有人问我这类比较基础的知识,无奈我自觉回答不是有效果,现在深入浅出的总结下: 前一篇文章总结了:JV

堆VS栈

c#堆VS栈(Part One) 前言 本文主要是讲解C#语言在内存中堆.栈的使用情况,使读者能更好的理解值类型.引用类型以及线程栈.托管堆. 首先感谢原文作者:Matthew Cochran 为我们带来了一篇非常好的文章,并配以大量图示,帮助我们更好的理解堆栈之间的调用,本文是在作者原文的基础上进行内容上的精简以及加入我个人在这方面的理解和注释. 最后要感谢博客园的田志良,当我搜索堆栈内部使用时,搜索到了作者的文章,吸取了大量有用的知识,而且翻译的也非常好.唯一美中不足的可能是仅仅翻译了Mat

内存分配及堆与栈的区别

1.内存分配方式 内存分配方式有三种: 1.从静态存储区域分配.内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在.例如全局变量,static变量. 2.从堆栈上分配.函数内的局部变量的存储单元,函数执行结束时这些存储单元自动被释放.栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限. 3.从堆上分配,亦称动态内存分配.程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存.动态内存的生存期由程序员决定

(转)内存堆和栈的区别

原文: http://student.csdn.net/link.php?url=http://www.top-e.org%2Fjiaoshi%2Fhtml%2F427.html 在计算机领域,堆栈是一个不容忽视的概念,我们编写的C语言程序基本上都要用到.但对于很多的初学着来说,堆栈是一个很模糊的概念. 堆栈:一种数据结构.一个在程序运行时用于存放的地方,这可能是很多初学者的认识,因为我曾经就是这么想的和汇编语言中的堆栈一词混为一谈.我身边的一些编程的朋友以及在网上看帖遇到的朋友中有好多也说不清

定义类+类实例化+属性+构造函数+匿名类型var+堆与栈+GC回收机制+值类型与引用类型

为了让编程更加清晰,把程序中的功能进行模块化划分,每个模块提供特定的功能,而且每个模块都是孤立的,这种模块化编程提供了非常大的多样性,大大增加了重用代码的机会. 面向对象编程也叫做OOP编程 简单来说面向对象编程就是结构化编程,对程序中的变量结构划分,让编程更清晰. 类的概念: 类实际上是创建对象的模板,每个对象都包含数据,并提供了处理和访问数据的方法. 类定义了类的每个对象(称为实例)可以包含什么数据和功能. 类中的数据和函数称为类的成员:数据成员        函数成员 数据成员: 数据成员

堆和栈的区别

一.预备知识-程序的内存分配    一个由C/C++编译的程序占用的内存分为以下几个部分    1.栈区(stack)-   由编译器自动分配释放   ,存放函数的参数值,局部变量的值等.其    操作方式类似于数据结构中的栈.    2.堆区(heap)   -   一般由程序员分配释放,   若程序员不释放,程序结束时可能由OS回    收   .注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵.    3.全局区(静态区)(static)-,全局变量和静态变量的存储是放在一块的

Java堆、栈和常量池以及相关String的详细讲解(转)

一:在JAVA中,有六个不同的地方可以存储数据: 1. 寄存器(register). 这是最快的存储区,因为它位于不同于其他存储区的地方--处理器内部.但是寄存器的数量极其有限,所以寄存器由编译器根据需求进行分配.你不能直接控制,也不能在程序中感觉到寄存器存在的任何迹象. ------最快的存储区, 由编译器根据需求进行分配,我们在程序中无法控制. 2. 堆栈(stack).位于通用RAM中,但通过它的"堆栈指针"可以从处理器哪里获得支持.堆栈指针若向下移动,则分配新的内存:若向上移动