译文---C#堆VS栈(Part Two)
前言
在本系列的第一篇文章《C#堆栈对比(Part One)》中,介绍了堆栈的基本功能和值类型以及引用类型在程序运行时的表现,同时也包含了指针作用的讲解。
本文为文章的第二部分,主要讲解参数在堆栈的作用。
注:限于本人英文理解能力,以及技术经验,文中如有错误之处,还请各位不吝指出。
目录
参数---重点讨论事项
这就是当我们执行代码时的详细情况。我们在第一步已经讲述了调用方法时所发生的情况,现在让我们来看看更多细节…
当我们调用方法时,如下事情将发生:
- 当我们执行一个方法时需要在栈上创建一个空间。这包含了一个GOTO指令的地址调用(指针),所以当线程执行完我们的方法后它知道如何返回并继续执行程序。
- 我们方法的参数将被拷贝。这就是我们要仔细去研究的东西。
- 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 + ;
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 = ;
AddFive(x); Console.WriteLine(x.ToString()); } public int AddFive(int pValue)
{
pValue += ;
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 = ;
DoSomething(ref x); Console.WriteLine(x.a.ToString()); } public void DoSomething(ref MyStruct pValue)
{
pValue.a = ;
}
传递引用类型
引用类型的传递类似于包装值类型的引用方式,正如前面所提到的例子。
如果我们使用引用类型:
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 = ; DoSomething(x); Console.WriteLine(x.MyValue.ToString());
} public void DoSomething(MyInt pValue)
{
pValue.MyValue = ;
}
- 开始执行Go方法,变量x进入栈中。
- 执行DoSomething方法,参数pValue进入栈中。
- x(堆上MyInt的指针)被传递给pValue。(Thanks To CityHunter,纠正了语言表达上的错误~)
所以,当我们改变堆上的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
接下来,让我们看看发生了什么,如下图:
- 开始执行Go方法,x指针在栈上被初始化。
- Animal类型在堆上被创建。
- 开始执行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关键字传递的是引用类型的指针,而非引用类型地址。
译文---C#堆VS栈(Part Two)的更多相关文章
- 译文---C#堆VS栈(Part One)
前言 本文主要是讲解C#语言在内存中堆.栈的使用情况,使读者能更好的理解值类型.引用类型以及线程栈.托管堆. 首先感谢原文作者:Matthew Cochran 为我们带来了一篇非常好的文章,并配以大量 ...
- 译文---C#堆VS栈(Part Three)
前言 在本系列的第一篇文章<C#堆栈对比(Part Two)>中,介绍了值类型和引用类型在参数传递时的不同,本文将讨论如何应用ICloneable接口实现去修复引在堆上的用变量所带来的问题 ...
- 译文---C#堆VS栈(Part Four)
前言 在本系列的第一篇文章<C#堆栈对比(Part Three)>中,介绍了值类型和引用类型在Copy上的区别以及如何实现引用类型的克隆以及使用ICloneable接口等内容. 本文为文章 ...
- JVM学习(2)——技术文章里常说的堆,栈,堆栈到底是什么,从os的角度总结
俗话说,自己写的代码,6个月后也是别人的代码……复习!复习!复习!涉及到的知识点总结如下: 堆栈是栈 JVM栈和本地方法栈划分 Java中的堆,栈和c/c++中的堆,栈 数据结构层面的堆,栈 os层面 ...
- java中内存分配策略及堆和栈的比较
Java把内存分成两种,一种叫做栈内存,一种叫做堆内存 在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配.当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间 ...
- 在JS中关于堆与栈的认识function abc(a){ a=100; } function abc2(arr){ arr[0]=0; }
平常我们的印象中堆与栈就是两种数据结构,栈就是先进后出:堆就是先进先出.下面我就常见的例子做分析: main.cpp int a = 0; 全局初始化区 char *p1; 全局未初始化区 main( ...
- C语言堆和栈
堆和栈的区别 一个由C/C++编译的程序占用的内存分为以下几个部分1.栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等.其操作方式类似于数据结构中的栈.2.堆区(heap ...
- JAVA中用堆和栈的概念来理解equals() "=="和hashcode()
在学习java基本数据类型和复杂数据类型的时候,特别是equals()"=="和hashcode()部分时,不是很懂,也停留了很长时间,最后终于有点眉目了. 要理解equals() ...
- C/C++ 堆和栈的区别
堆和栈的区别 一个由C/C++编译的程序占用的内存分为以下几个部分 1.栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等.其 操作方式类似于数据结构中的栈. 2.堆区(h ...
随机推荐
- advanced validation on purchase.
安装模块 此模块在 标准功能的 2级审批基础上 增加 老板审批 增加 不同技术类和 非技术类的分支 核心审批工作流 如下图示 为审批用户 授予 purchase manager 权限 否则,看不到 审 ...
- vim 查看文件二进制格式
用vim打开文件,vim -b file,选项-b是二进制模式打开 然后输入 :%!xxd,就可看到二进制编码 其实在linux下,直接输入xxd file 也是可以看到的文件二进制格式的
- [06]APUE:系统数据文件和信息
[a] getpwent / setpwent / endpwent #include <pwd.h> struct passwd *getpwent(void) //成功返回指针,出错或 ...
- 解压版MySQL5.7.1x的安装与配置
解压版MySQL5.7.1x的安装与配置 MySQL安装文件分为两种,一种是msi格式的,一种是zip格式的.如果是msi格式的可以直接点击安装,按照它给出的安装提示进行安装(相信大家的英文可以看懂英 ...
- 搭建maven环境
有两种方式可以配置maven的环境配置,本人推荐使用第二种,即使用本地的maven安装文件,个人感觉这样可以方便管理下载jar包的存放位置,错误信息的输出等,可以在dos窗口中可以清晰看到,虽然比较麻 ...
- MySQL 创建表
MySQL中create table语句的基本语法是: CREATE [TEMPORARY] TABLE [IF NOT EXISTS] tbl_name [(create_definition,. ...
- 【Mail】搭建邮件服务器(LAMP+Postfix+Dovcot+PostfixAdmin+Roundcubemail)
大纲 一.mail部署说明 二.安装准备 三.LMAP环境配置 四.配置postfixadmin 五.配置postfix 六.配置dovecot 七.测试SMTP和POP3服务 八.配置Roundcu ...
- appium踩过的坑(1):NoClassDefFoundError
1.引入jar包错误导致的错误: 引入的jar包引起的 应该引入下面的jar包
- 树型dp
树形dp主要有两种,比较重要的共同点就是要想全所有情况. [一] 第一种是简单的父子关系型,即动规只与一个节点和它的子节点有关. [例]codevs1380没有上司的舞会: 有个公司要举行一场晚会.为 ...
- 工具fiddler学习
1:Fiddler 是以代理web服务器的形式工作的,它使用代理地址:127.0.0.1, 端口:8888. 当Fiddler会自动设置代理.能支持HTTP代理的任意程序的数据包都能被Fiddler嗅 ...