内存组织、拆箱、和装箱

抄书笔记:Visual C# 从入门到精通(第九版)
C#大多数基元类型(int、float、doubule和char等,string除外,这是个特殊的引用类型,他的本质上是一个char类型的数组)都是值类型;将变量声明为值类型,编译器会生成代码来分配足以容纳这种值得内存块。
例如:声明int类型的变量,会导致编译器分配4字节(32位)内存块,像这个变量赋值,将导致值会被赋值在内存块当中。
引用类型和值类型的处理方式不一样。例如一个类,声明一个类的变量的时候,编译器不生成代码来分配足以容纳一个类的内存块。相反,他唯一做的事情就是分配一小块,其中刚好容纳一个地址内存块。类的实际占用内存块的地址就存储在这个内存块中,类对象实际占用的内存,是使用了new关键字创建对象时分配的。(new:本质上就是向系统申请一块内存)。引用类型,实际上是容纳对内存块的引用。
注意:C#的string类型实际上是类类型,由于字符串大小不固定,所以更高效的策略是在程序运行时动态分配内存,不是在编译时静态分配内存,事实上,C#中的string关键字是system.string类的别名。(和万物之父object类似)。
举例子:
值类型:
int num1=9; //声明初始化 num1;在栈中分配的内存;
int num2=num1;//将num1的值赋值给了num2,会为num2分配了一块内存。(虽然num1和num2容纳了相同的值,实际上,他们拥有各自的内存块,后续会解释栈和堆的内容)。也就是说,当修改num2的时候,不会在修改num1的值内容。

引用类型:
比如已经定义了一个Person类。
Preson zhangsan=new Person();
Preson lisi=zhangsan;
声明了一个Preson类型的变量,zhangsan其实容纳的是一个对堆内存引用的地址,实际实例对象的内容在堆中。这个时候把这个引用地址赋值给了lisi,那么实际上两个变量指向了同一块内容。修改其中一个,就会导致另一个会被修改。
这个区别十分重要:意味着方法参数行为取决于他们是值类型还是引用类型。
其他知识点:可空类型,out和ref。

重点:计算机内存的组织方式
计算机使用内存来容纳要执行的程序,以及这些程序使用的数据。为了理解值类型和引用类型的区别。有必要理解数据在内存中如何组织的。
操作系统和“运行时”通常将用于容纳数据的内存划分为两个独立的区域。每个区域用不同的方式去管理。这个区域通常是堆和栈。
重点1:
调用方法的时候,他的参数和局部变量所需要的内存总是从栈中获取的。方法结束以后(不管正常返回还是抛出异常),为参数和局部变量分配的内存都自动归还给栈,并且可以在另一个方法调用时重新使用。栈上的方法参数和局部变量具有良好定义的生存周期,方法开始进入生存期,结束时生存周期结束。
重点2:
使用new关键字创建对象(类的实例)时,构造对象所需的内存总是从堆中获取。使用引用变量的时候,可以从多个地方引用同一个对象。对象最后一个引用消失之后(没有直接或者间接的引用该堆内存),对象占用的内存就可供重用(虽然不一定立即回收)。(如果有空会详细整理一下关于垃圾回收机制的一些资料)。堆上创建的对象具有不确定的生存期,使用new关键字将创建对象,只有在删除了最后一个对象引用之后的某个不确定时刻,他才会真正的消失。
所有的值类型都在栈上创建,所有引用类型的实例(对象)都在对上创建(虽然引用本身还在栈上)。可空类型实际是引用类型,所以在堆上创建。
重点3:
栈(stack)内存就像一系列的堆得越来越高的箱子。调用方法时,他的每个参数都被放入一个箱子并放到栈顶。每个局部变量也同样分配一个箱子,并同样放在栈顶。方法结束以后,他的所有的箱子都从栈中移除。
堆(heap)内存则是散步在房间里的一大堆箱子。不像栈那样每个箱子都严格的堆在另一个箱子上面,每个箱子都有一个标签,标记了这个箱子是否正在使用。创建新对象时,“运行”就会查找空箱子,把它分配给对象。对对象的引用则存在栈上的一个局部变量中。“运行时”跟踪每个箱子的引用数量(记住:两个变量可能引用同一个对象),一旦对某一个堆内存的对象直接或者间接的引用全部消失,那么运行时就会把这个箱子标记为“未使用”将来某个时候,会清楚箱子里的东西,使他能够被重用。
使用堆和栈的具体讲解:
public Method(int num)
{
Person c;
c=new Preson();
}
假如传给num的值是42,调用方法的时候,栈中将分配一小块内存(刚好够存储一个int),并用值42初始化。在方法内部,还是要从栈中分配出另一小块内存,他刚好能够存储一个内存的引用地址。只是暂时不初始化。这是为了Preson类型的变量c准备的。接着,要从堆中分配一个足够大的内存区域来容纳一个Preson对象,这是new关键字所执行的操作。运行Preson的构造器,将该原始堆内存转换成Preson的对象。对该对象的引用将存储到变量c中。
注意两点:
1、虽然对象本身存储在堆中,但对象引用的(变量c)存储在栈中;
2、堆内存是有限的资源。如果堆内存耗尽,new操作符就会抛出异常,对象创建就失败了。这种情况下,Person的构造器(构造函数)也有可能抛出异常。分配给Preson对象的内存会被回收,构造器就会返回null值。
方法结束以后,参数和局部变量c就离开了作用域,为c和num分配的内存就会被自动回收到栈中。运行时发现已不存在对Person的引用,所以会在将来某个时候,安排垃圾回收机制收回内存空间。(如果有空的话,我后续整理关于垃圾回收机制的笔记)

重点:装箱和拆箱
.NET最终的引用类型之一是System命名空间下的object类。如果想要完全理解System.Object类的重要性,首先需要理解继承(面向对象东西太多就不说了,以前我是自行百度学的)
第一:所有的类其实都是System.Object类的派生类(子类)。
第二:System.Object类型的变量能引用任何对象。
小知识点:实际写代码中,既可以写object,也可以写System.Object,两者没有区别。
简单的理解什么是装箱:
object类型的变量能够引用任何引用引用类型的变量。此外,object类型用的变量也能引用值类型的实例。
int i=40;
object p=i;
第二个语句:i是值类型,所以他在栈中。如果p直接引用i,那么引用的就是栈。然后,规定就是所有的引用都必须引用堆上的对象;引用栈上的数据项,那么就损坏了运行的健壮性,造成了潜在的安全漏洞,所以不允许。
第二个语句实际发生的操作:
运行时,在堆内存分配一小块内存,然后把i的值复制到了这块内存中。最后让p引用了这个被拷贝的值。这种将数据项从栈自动复制到堆的行动成为装箱。因此,修改变量i的值,o所引用的值不会改变。同样的修改堆上的值,i也不会改变。
装箱:值类型转换到引用类型的具体实现,就是这么一个过程。

同理,拆箱只不过是反过来而已。
int i=34;
Preson c=new Person();
object o;
o=c;
i=o;
为了访问已经装箱的值,必须进行强制类型转换。这个操作会先检查是否能将一种类型安全的转换到另一种类型,然后才执行转换。为了能够进行转换,需要在object前添加强制转换语法:
int i=42;
object o=i;
i=(int)o;
这个过程是这样的:编译器发现了制定类型int,所以会在运行时生成代码检查o实际引用了什么。他可能引用任何东西,不能因为在转型时说o引用了是int,就真的引用了一个int。如果o真的引用了一个已装箱的int,转型成功执行。编译器生成的代码会从装箱的int提取出值,这个过程就是所谓的拆箱。
注意:装箱和拆箱都会造成很大的性能开销。因为他们会涉及很多检查工作,并且需要分配额外的堆内存。装箱拆箱的与泛型实际上有异曲同工之妙。(有时间在整理关于泛型的笔记在发出来吧)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值