[译文]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),以及如何攻克对象拷贝时的一些问题。
待续……
在
Parameters,theBigPicture
下面是当我们代码执行时,发生哪些事情的详细视图。PartI我们讨论了基本的内容,这部分我们将更加深入……
当我们调用一个方法时,下面是所发生的事情:
1.为执行方法所需要的信息分配内存空间(称作栈帧(StackFrame)),这其中包括调用地址(一个指针),该地址是一个GOTO指令,用于告诉程序,当这个方法调用完毕后,程序该从哪里继续执行。
2.方法的参数将被拷贝。这是我们将进一步讨论的。
3.控制将传递给JIT编译后的方法,然后线程开始执行代码。因此,我们有另外一个位于“调用栈”(callstack)上的代表栈帧(stackframe)的方法(原文:Hence,wehaveanothermethodrepresentedbyastackframeonthe"callstack".)。
代码:
其对应的栈图是这样的:
正如PartI所讨论的,位于栈上的参数根据值类型和引用类型的不同将有不同的处理方式。
传递值类型
当我们传递值类型的时候,将分配新的内存空间,同时,我们的值将被拷贝到栈上的新内存空间。让我们看下下面的方法:
当执行方法的时候,变量”x”的空间被分配到栈上,并且值为5.
然后,AddFive()被放到栈上,同时为它的参数开辟空间,将x的值拷贝给该参数。
AddFive()方法执行完毕后,线程重新传回到Go()方法上,由于AddFive()已经执行完成,pValue就被完全的移除掉了:
因此,上述代码的返回值为5,对吧?这个问题的关键点是,我们传递给方法的值类型参数都只是一个副本,原始的变量得以保留。
我们需要记住的一点是,如果我们将一个很大的值类型(如,一个大的struct)传递到栈上,那么这将花费巨大的空间和处理周期。栈上的空间并不是无限的,正如我们从水龙头给杯子加水,它是会溢出来的。一个结构体(struct)可能是非常大的,我们需要注意如何处理它。
下面是一个大的结构体:
{
MyStructx=newMyStruct();
DoSomething(x);
}
publicvoidDoSomething(MyStructpValue)
{
//DOSOMETHINGHERE....
}
这将是非常低效的。试想一下,如果我们将MyStruct传递上千次,那么我们的程序将被搞得无法动弹。
那么,我们该如何来解决这个问题呢?
我们可以通过传递原来值类型的一个引用,如下:
{
MyStructx=newMyStruct();
DoSomething(refx);
}
publicstructMyStruct { longa,b,c,d,e,f,g,h,i,j,k,l,m; }
publicvoidDoSomething(refMyStructpValue)
{
//DOSOMETHINGHERE....
}
这样,我们就可以更加有效的分配内存了。
当然,将值类型作为引用来传递参数,我们有一点需要注意的就是,我们可以访问该值类型了(译注:而不仅仅是它的副本)。我们队pValue的任何改变都将影响到x。来看下面的例子:
{
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的值。
传递应用类型
传递引用类型和通过引用传递值类型是类似的.
如果我们使用引用类型:
{
publicintMyValue;
}
然后调用Go()方法,那么MyInt将被放在堆上,因为它是一个引用类型:
{
MyIntx=newMyInt();
}
如果我们将Go()方法改成如下:
{
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:
{
}
publicclassAnimal:Thing
{
publicintWeight;
}
publicclassVegetable:Thing
{
publicintLength;
}
当我们执行下面的Go()方法时:
{
Thingx=newAnimal();
Switcharoo(refx);
Console.WriteLine("xisAnimal:"+(xisAnimal).ToString());
Console.WriteLine(
"xisVegetable:"+(xisVegetable).ToString());
}
publicvoidSwitcharoo(refThingpValue)
{
pValue=newVegetable();
}
xisAnimal:False
xisVegetable:True
通过图来看看发生了什么情况:
1.开始调用Go()方法,x位于栈上;
2.Animal位于堆上;
3.开始调用Switcharoo()方法,pValue位于栈上,并且指向x
4.Vegetable位于堆上;
5.x的值通过pValue变成指向Vegetable的地址
如果我们不是通过引用传递Thing,我们将得到相反的结果。
总结
我们已经讨论了参数传递在内存中是如何处理的,现在也知道该留心哪些东西了。
在下一节了,我们将探讨位于栈上的引用型变量(referencevariables),以及如何攻克对象拷贝时的一些问题。
待续……
相关文章推荐
- [译文]C# Heap(ing) Vs Stack(ing) in .NET: Part III
- [译文]C# Heap(ing) Vs Stack(ing) in .NET: Part I
- C# Heap(ing) Vs Stack(ing) in .NET: Part II
- [译文]C# Heap(ing) Vs Stack(ing) in .NET: Part IV
- C# Heap(ing) Vs Stack(ing) in .NET: Part I
- C# Heap(ing) Vs Stack(ing) in .NET: Part III
- C# Heap(ing) Vs Stack(ing) in .NET: Part I
- C# Heap(ing) Vs Stack(ing) in .NET: Part IV
- 深入浅出图解C#堆与栈 C# Heap(ing) VS Stack(ing) 第四节 参数传递对堆栈的影响 1
- 深入浅出图解C#堆与栈 C# Heap(ing) VS Stack(ing) 第五节 引用类型复制问题及用克隆接口ICloneable修复
- 深入浅出图解C#堆与栈 C# Heap(ing) VS Stack(ing) 第三节 栈与堆,值类型与引用类型
- 深入浅出图解C#堆与栈 C# Heap(ing) VS Stack(ing) 第三节 栈与堆,值类型与引用类型
- 深入浅出图解C#堆与栈 C# Heap(ing) VS Stack(ing) 第六节 理解垃圾回收GC,提搞程序性能
- C# Heap(ing) Vs Stack(ing) in .NET: Part IV
- 深入浅出图解C#堆与栈 C# Heap(ing) VS Stack(ing) 第四节 参数传递对堆栈的影响 1
- C# Heap(ing) Vs Stack(ing) in .NET: Part I
- C# Heap(ing) Vs Stack(ing)
- 深入浅出图解C#堆与栈 C# Heap(ing) VS Stack(ing) 第一节 理解堆与栈
- 深入浅出图解C#堆与栈 C# Heap(ing) VS Stack(ing) 第二节 栈基本工作原理
- 深入浅出图解C#堆与栈 C# Heap(ing) VS Stack(ing) 第五节 引用类型复制问题及用克隆接口ICloneable修复