.NET中栈和堆的比较(二)

 

尽管在.NET framework下我们并不需要担心内存管理和垃圾回收(Garbage Collection),但是我们还是应该了解它们,以优化我们的应用程序。同时,还需要具备一些基础的内存管理工作机制的知识,这样能够有助于解释我们日常程序编写中的变量的行为。在本文中我将讲解我们必须要注意的方法传参的行为。

在第一部分里我介绍了栈和堆的基本功能,还介绍到了在程序执行时值类型和引用类型是如何分配的,而且还谈到了指针。


* 参数,大问题

这里有一个代码执行时的详细介绍,我们将深入第一部分出现的方法调用过程...

当我们调用一个方法时,会发生以下的事情:

1.方法执行时,首先在上为对象实例中的方法分配空间,然后将方法拷贝到上(此时的被称为帧),但是该空间中只存放了执行方法的指令,并没有方法内的数据项。
2.方法的调用地址(或者说指针)被放置到上,一般来说是一个GOTO指令,使我们能够在方法执行完成之后,知道回到哪个地方继续执行程序。(最好能理解这一点,但并不是必须的,因为这并不会影响我们的编码)
3.方法参数的分配和拷贝是需要空间的,这一点是我们需要进一步注意。
4.控制此时被传递到了帧上,然后线程开始执行我们的代码。因此有另一个方法叫做"调用"。

示例代码如下:


此时开起来是这样的:
单击显示全图,Ctrl+滚轮缩放图片

就像第一部分讨论的那样,放在上的参数是如何被处理的,需要看看它是值类型还是引用类型。值类型的值将被拷贝到上,而引用类型的引用(或者说指针)将被拷贝到上。


* 值类型传递

首先,当我们传递一个值类型参数时,上被分配好一个新的空间,然后该参数的值被拷贝到此空间中。

来看下面的方法:


方法Go()被放置到 上,然后执行,整型变量"x"的值"5"被放置到 顶空间中。
单击显示全图,Ctrl+滚轮缩放图片

然后AddFive()方法被放置到 顶上,接着方法的形参值被拷贝到 顶,且该形参的值就是"x"的拷贝。
单击显示全图,Ctrl+滚轮缩放图片

当AddFive()方法执行完成之后,线程就通过预先放置的指令返回到Go()方法的地址,然后从 顶依次将变量pValue和方法AddFive()移除掉:
单击显示全图,Ctrl+滚轮缩放图片

所以我们的代码输出的值是"5",对吧?这里的关键之处就在于任何传入方法的值类型参数都是复制拷贝的,所以原始变量中的值是被保留下来而没有被改变的。

必须注意的是,如果我们要将一个非常大的值类型数据(如数据量大的struct类型)入 ,它会占用非常大的内存空间,而且会占有过多的处理器周期来进行拷贝复制。 并没有无穷无尽的空间,它就像在水龙头下盛水的杯子,随时可能溢出。struct是一个能够存放大量数据的值类型成员,我们必须小心地使用。

这里有一个存放大数据类型的struct:
public   struct  MyStruct
{
    
long  a, b, c, d, e, f, g, h, i, j, k, l, m;
}

来看看当我们执行了Go()和DoSometing()方法时会发生什么:
public   void  Go()
{
    MyStruct x 
=   new  MyStruct();
    DoSomething(x);
}

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

单击显示全图,Ctrl+滚轮缩放图片

这将会非常的低效。想象我们要是传递2000次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....
}

通过这种方式我们能够提高内存中对象分配的效率。
单击显示全图,Ctrl+滚轮缩放图片

唯一需要注意的是,在我们通过引用传递值类型时我们会修改该值类型的值,也就是说pValue值的改变会引起x值的改变。执行以下代码,我们的结果会变成"123456",这是因为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();
}


单击显示全图,Ctrl+滚轮缩放图片

如果我们执行下面的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 ;
}


将发生这样的事情...
单击显示全图,Ctrl+滚轮缩放图片

1.方法Go()入
2.Go()方法中的变量x入
3.方法DoSomething()入
4.参数pValue入
5.x的值(MyInt对象的在中的指针地址)被拷贝到pValue中

因此,当我们通过MyInt类型的pValue来改变中MyInt对象的MyValue成员值后,接着又使用指向该对象的另一个引用x来获取了其MyValue成员值,得到的值就变成了"12345"。

而更有趣的是,当我们通过引用来传递一个引用类型时,会发生什么?

让我们来检验一下。假如我们有一个"Thing"类和两个继承于"Thing"的"Animal"和"Vegetable" 类:

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

让我们来看看发生了什么:
单击显示全图,Ctrl+滚轮缩放图片

1.Go()方法入
2.x指针入
3.Animal对象实例化到
4.Switcharoo()方法入
5.pValue入 且指向x
单击显示全图,Ctrl+滚轮缩放图片

6.Vegetable对象实例化到
7.x的值通过被指向Vegetable对象地址的pValue值所改变。


如果我们不使用Thing的引用,相反的,我们得到结果变量x将会是Animal类型的。

如果以上代码对你来说没有什么意义,那么请继续看看我的文章中关于引用变量的介绍,这样能够对引用类型的变量是如何工作的会有一个更好的理解。

我们看到了内存是怎样处理参数传递的,在系列的下一部分中,我们将看看 中的引用变量发生了些什么,然后考虑当我们拷贝对象时是如何来解决某些问题的。  
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在JavaScript中,栈和是用来存储变量和数据的两种不同的内存区域。 栈是一种后进先出(LIFO)的数据结构,用于存储基本类型的变量和引用类型的指针。当我们声明一个变量时,它的值会被直接存储在栈内存中。基本类型的值(如数字、布尔值、字符串等)被直接存储在栈中,而引用类型的变量则存储了指向内存中实际数据的指针。 是一种动态分配的内存区域,用于存储引用类型的数据。当我们创建一个引用类型的变量时,它的值实际上是一个指向内存中对象的引用。对象本身的数据存储在内存中,而栈中的变量只是存储了指向内存中对象的引用。 深拷贝是指创建一个新的对象,将原始对象的所有属性和嵌套对象的属性都复制到新对象中。这样,新对象和原始对象是完全独立的,对新对象的修改不会影响原始对象。在JavaScript中,可以使用不同的方法实现深拷贝,如使用JSON.parse(JSON.stringify(obj))或自定义递归函数来复制对象的属性和嵌套对象。 浅拷贝是指创建一个新的对象,将原始对象的属性复制到新对象中,但嵌套对象的引用仍然指向原始对象中的相同嵌套对象。这意味着对新对象的修改可能会影响原始对象。在JavaScript中,可以使用Object.assign()或展开运算符(...)来实现浅拷贝。 总结来说,栈用于存储基本类型的变量和引用类型的指针,而用于存储引用类型的数据。深拷贝是创建一个新对象并复制所有属性和嵌套对象的值,而浅拷贝只复制属性,嵌套对象的引用仍然指向原始对象。 #### 引用[.reference_title] - *1* *3* [JavaScript 中和栈的区别](https://blog.csdn.net/qq_29850249/article/details/110500006)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [JavaScript中栈内存与内存分别是什么?](https://blog.csdn.net/qq_43807473/article/details/123816682)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值