参数传递对堆栈的影响

来源:互联网 发布:android 引导蒙版源码 编辑:程序博客网 时间:2024/05/16 17:11

前言


虽然在.Net Framework 中我们不必考虑内在管理和垃圾回收(GC),但是为了优化应用程序性能我们始终需要了解内存管理和垃圾回收(GC)。另外,了解内存管理可以帮助我们理解在每一个程序中定义的每一个变量是怎样工作的。

简介


这篇文章我们将介绍一些方法参数传递行为在堆与栈中的影响。前几节我们介绍了堆与栈的基本工作原理,程序执行时值类型与引用类型在堆栈中的存储。另外,我们已经介绍了一些关于指针的基本知识。这一节中参数传递对堆栈的影响很重要,下面会慢慢道来。

参数,大画面

下面是当代码运行时会产生的一个详细过程。上几节已经介绍过当一个方法被调用时会产生的基本情况,让我们来看一下更加详细的内容。
当我们调用一个方法时会发生以下情形:
  1. 栈会分配一块内存空间给程序执行所需要的信息(我们叫它栈结构Stack Frame)。一个栈结构包含方法调用地址(指针),它以一个GOTO指令的形式存在栈里。因此,当程序执行完方法(method)时,它会知道怎么样返回进而曳继续执行代码。
  2. 方法的所有参数将被复制到栈里,这是我们将要更加详细介绍的部分。
  3. 控制被传递到JIT编译过的方法里,同时线程开始执行代码。此时,我们将有另一个方法呈现在栈结构的“回调栈”里。
代码:
[csharp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
  1. public int AddFive(int pValue) 
  2.           { 
  3.                 int result; 
  4.                 result = pValue + 5; 
  5.                 return result; 
  6.           } 

栈像下图所示:


注意:ReturnValue方法不会存在栈上,图中把ReturnValue作为此栈结构的开始只是为了解释栈原理。

像前几节介绍的,值类型和引用类型在栈里的存储是不同的。栈为任何值类型创建副本,栈也为任何引用类型的指针创建副本。

值类型传递

下面是值类型传递在栈里的内幕。

首先,当我们传递一个值类型变量时,栈会为它分配一块内存空间并把值类型变量的值存储进去。看下面的代码:
[csharp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
  1. class Class1 
  2.      { 
  3.           public void Go() 
  4.           { 
  5.               int x = 5; 
  6.               AddFive(x); 
  7.   
  8.               Console.WriteLine(x.ToString()); 
  9.                
  10.           } 
  11.   
  12.           public int AddFive(int pValue) 
  13.           { 
  14.               pValue += 5; 
  15.               return pValue; 
  16.           } 
  17.      } 

当代码执行时,栈为x分配一块内存空间并存储值5

然后,AddFive()被放到栈上,同时栈分配内存空间给参数pValue并复制x的值给它。

当AddFive()执行完成,线程被传递回Go()。同时因为AddFive()执行完,它的参数pValue也实质上被移除。

所以结果是5是合理的。关键点是任何被传递的值类型参数仅是一个碳复制,因为我们希望保护原始变量的值。
有一点要记住的是,如果我们有一个非常庞大的值类型(如,庞大的struct类型)传递到栈里,当处理器循环复制它并循环占有栈空间时将会非常耗资源。栈没有无限的空间去使用,就像用水杯不断的接水早晚会溢出一样。Struct类型可以变得非常庞大,我们要小心并清醒的使用它。

下面是一个比较大的struct结构类型:
[csharp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
  1. public struct MyStruct 
  2.           { 
  3.               long a, b, c, d, e, f, g, h, i, j, k, l, m; 
  4.           } 

让我们看看执行下面代码Go()方法时再到DoSomething()方法会发生的情况:
[csharp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
  1. public void Go() 
  2.          { 
  3.             MyStruct x = new MyStruct(); 
  4.             DoSomething(x); 
  5.               
  6.          } 
  7.  
  8.  
  9.           public void DoSomething(MyStruct pValue) 
  10.           { 
  11.                    // 省略具体实现.... 
  12.           } 


这可能会非常低效。想像一下如果我们传递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)
           {
                    // 省略实现....
           }


这样就能节省内存并提升内存使用效率


唯一需要注意的是传递引用时我们在访问原始变量x的值,任可对pValue的改变都会影响到x。
下面的代码会将x改变成"12345",因为pValue.a实际上指向原始x声明时所在的内存地址。
[csharp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
  1. public void Go() 
  2.           { 
  3.              MyStruct x = new MyStruct(); 
  4.              x.a = 5; 
  5.              DoSomething(ref x); 
  6.   
  7.              Console.WriteLine(x.a.ToString()); 
  8.                 
  9.           } 
  10.   
  11.           public void DoSomething(ref MyStruct pValue) 
  12.           { 
  13.                    pValue.a = 12345; 
  14.           } 

未完待续。。。
0 0
原创粉丝点击