您的位置:首页 > 编程语言 > C#

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

2012-05-13 13:20 519 查看
原文地址:http://www.c-sharpcorner.com/UploadFile/rmcochran/csharp_memory2B01142006125918PM/csharp_memory2B.aspx

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

Parameters,theBigPicture

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

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

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

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

3.控制将传递给JIT编译后的方法,然后线程开始执行代码。因此,我们有另外一个位于“调用栈”(callstack)上的代表栈帧(stackframe)的方法(原文:Hence,wehaveanothermethodrepresentedbyastackframeonthe"callstack".)。

代码:

publicintAddFive(intpValue) { intresult; result=pValue+5; returnresult; }

其对应的栈图是这样的:



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

传递值类型

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

classClass1 { publicvoidGo() { intx=5; AddFive(x); Console.WriteLine(x.ToString()); } publicintAddFive(intpValue) { pValue+=5; returnpValue; } }

当执行方法的时候,变量”x”的空间被分配到栈上,并且值为5.



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



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



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

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

下面是一个大的结构体:

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

{
MyStructx=newMyStruct();

DoSomething(x);

}

publicvoidDoSomething(MyStructpValue)

{
//DOSOMETHINGHERE....
}



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

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

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

publicvoidGo()

{
MyStructx=newMyStruct();

DoSomething(refx);

}
publicstructMyStruct { longa,b,c,d,e,f,g,h,i,j,k,l,m; }

publicvoidDoSomething(refMyStructpValue)

{
//DOSOMETHINGHERE....
}

这样,我们就可以更加有效的分配内存了。



当然,将值类型作为引用来传递参数,我们有一点需要注意的就是,我们可以访问该值类型了(译注:而不仅仅是它的副本)。我们队pValue的任何改变都将影响到x。来看下面的例子:

publicvoidGo()

{
MyStructx=newMyStruct();

x.a=5;

DoSomething(refx);

Console.WriteLine(x.a.ToString());
}

publicvoidDoSomething(refMyStructpValue)

{
pValue.a=12345;
}

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

传递应用类型

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

如果我们使用引用类型:

publicclassMyInt

{

publicintMyValue;

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

publicvoidGo()

{
MyIntx=newMyInt();
}



如果我们将Go()方法改成如下:

publicvoidGo()

{
MyIntx=newMyInt();

x.MyValue=2;

DoSomething(x);

Console.WriteLine(x.MyValue.ToString());
}

publicvoidDoSomething(MyIntpValue)

{
pValue.MyValue=12345;
}

我们得到的将是:



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

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

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

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

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

我们来检测一下。假设我们有一个Thing类,Animal和Vegetable都继承自Thing:

publicclassThing

{

}

publicclassAnimal:Thing

{
publicintWeight;
}

publicclassVegetable:Thing

{
publicintLength;
}

当我们执行下面的Go()方法时:

publicvoidGo()

{
Thingx=newAnimal();

Switcharoo(refx);

Console.WriteLine("xisAnimal:"+(xisAnimal).ToString());

Console.WriteLine(

"xisVegetable:"+(xisVegetable).ToString());
}

publicvoidSwitcharoo(refThingpValue)

{
pValue=newVegetable();
}变量x将变成Vegetable类型,即输出结果为;
xisAnimal:False
xisVegetable:True
通过图来看看发生了什么情况:



1.开始调用Go()方法,x位于栈上;
2.Animal位于堆上;
3.开始调用Switcharoo()方法,pValue位于栈上,并且指向x



4.Vegetable位于堆上;
5.x的值通过pValue变成指向Vegetable的地址
如果我们不是通过引用传递Thing,我们将得到相反的结果。
总结
我们已经讨论了参数传递在内存中是如何处理的,现在也知道该留心哪些东西了。
在下一节了,我们将探讨位于栈上的引用型变量(referencevariables),以及如何攻克对象拷贝时的一些问题。
待续……
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: