C#中的值类型和引用类型,深拷贝,浅拷贝

from https://www.jianshu.com/p/2d27b06e253f

一.C#中的值类型和引用类型

  • 概念

值类型直接存储其值。

引用类型存储对值的引用。

说起来有些拗口,其本质是ValueReference的区别,在文档翻译过程中也有译者将Reference翻译为参考。两种类型在内存中的存储方式有显著区别。

  • 不同的存储对象

值类型变量存储的是变量的值,直接储存在栈内存中。

引用类型变量存储的是变量所在的内存地址,引用类型变量的实际数据存储于托管堆,变量本身仅仅是一个指向堆中实际数据的地址,存储于栈内存中,通常是四个字节。

  • 不同的存储位置

值类型Value存储在线程堆栈中

引用类型Reference存储在托管堆上

内存格局通常划分为四个区:

全局数据区:存放全局变量,静态数据,常量

代码区:存放所有的程序代码

栈区:存放为运行而分配的局部变量,参数,返回数据,返回地址等

扫描二维码关注公众号,回复: 5409869 查看本文章

堆区:即自由存储区

为了理解值类型变量和引用类型变量的内存分配模型,我们应先区分两种不同的内存区域——线程堆栈Thread Stack和托管堆Managed Heap

每一个正在运行的程序都对应着一个进程Process,在一个进程内部,可以有一个或多个线程Thread,每个线程都拥有一块“自留地”,成为线程堆栈,大小为1M,用于保存自身的一些数据,如函数中定义的局部变量、函数调用时传送的参数值等。

现在我们可以解释第一句话——值类型存储在线程堆栈中,也就是说所有值类型的变量都是在线程堆栈中分配的。

另一块内存区域称为堆Heap,在.NET这种托管环境下,堆由CLR(Common Language Runtime)管理,所以又称托管堆Managed Heap。例如使用new关键字创建类的对象实例时,分配给对象的内存单元就位于托管堆中。

  • 不同的类型

这里类型区分的对象是C#中内建的类型Type和用户自定义的类型。

C#中的值类型:C#有15个预定义类型,其中13个是值类型,两个是引用类型(stringobject)。

 
C#中的值类型和引用类型

由此分类可以得知,struct是轻量级的类这句话本质上就不成立,两者的内存模型和行为表现都有区别。

  • 不同的表现

1.值类型的表现

int a = 5;
int b = a;

上面这段代码中我们赋予a一个常量值5,而赋予ba的值,这会在内存中两个不同的地方存储值20。我们改变a的值,不会影响b的值,这两个值时独立存储的。可以在上述代码之后改变a的值,输出b的值进行查看。

2.引用类型的表现

首先创建一个简单的类,只包含一个int类型的属性。

    class TestRef
    {
        public int A { get; set; } } 

主方法中与值类型的代码类型:

        public static void Main(string[] args) { TestRef testA = new TestRef {A = 20}; TestRef testB = testA; // 将testA赋值给testB Console.WriteLine("Before:testA中A的值:{0}", testA.A); Console.WriteLine("Before:testB中A的值:{0}", testB.A); testB.A = 15; // 改变testB的属性值 Console.WriteLine("After:testA中A的值:{0}",testA.A); Console.WriteLine("After:testB中A的值:{0}", testB.A); Console.ReadKey(); } 

运行结果如图所示:


 
运行结果

可以看到testB改变了属性值之后,testA的属性值也随之改变,这是由于这两个对象只是一个指向堆内存的地址,实际指向的只有一份实际的值。

3.与null的关系
如果变量是引用类型变量,则可以将其值设置为null,表示它不引用任何对象(可以将理解为将指针指向空)。而值类型不能为null,这也是为什么值类型初始化时必须指定初始值或默认值。

  • 设计立足点

    大多数更复杂的数据类型,包括我们自己声明的类都是引用类型。它们分配在堆中,其生存期可以跨多个函数调用,可以通过一个或几个别名来访问。CLR执行一种精细的算法,来跟踪哪些引用变量仍是可以访问的,哪些引用变量已经不能访问了。CLR会定期删除不能访问的对象,把它们占用的内存返回给操作系统。这是通过垃圾收集器实现的。

    把基本类型规定为值类型,而把包含许多字段的较大类型(通常在有类的情况下)规定为引用类型,C#设计这种方式的原因是可以得到最佳性能。如果要把自己的类型定义为值类型,就应把它声明为一个结构。

  • 深拷贝和浅拷贝

深拷贝——源对象与拷贝对象互相独立,其中任何一个对象的改动都不会对另外一个对象造成影响。

浅拷贝——拷贝对象后,两个对象并未完全“分离”,改变一个对象实际储存的内容,则两个对象同时被改变。

这种差异的产生,即是取决于拷贝子对象时复制内存还是复制指针。深拷贝为子对象重新分配了一段内存空间,并复制其中的内容;浅拷贝仅仅将指针指向原来的子对象。

我们假设有了一个对象orignalObj,并且对象orignalObj已经有了一些具体的值,现在我们想创建一个orignalObj的副本即对象copyObj,我们希望,操作对象copyObj的同时不改变对象orignalObj的值,也就是说对象a和对象b是两个完全独立的对象,这即是深拷贝。

当两个对象指向同一个地址时,如果我们改变其中一个对象的值,另一个对象也被相应的改变,这即是浅拷贝。

  • 额外需要注意

(1)String字符串对象是引用对象,但是很特殊,它表现的如值对象一样,即对它进行赋值,分割,合并,并不是对原有的字符串进行操作,而是返回一个新的字符串对象。但这其实是运算符重载的结果,将string实现为语义遵循一般的、直观的字符串规则。 String对象被分配在堆上,而不是栈上。

(2)Array数组对象是引用对象,在进行赋值的时候,实际上返回的是源对象的另一份引用而已;因此如果要对数组对象进行真正的复制(深拷贝),那么需要新建一份数组对象,然后将源数组的值逐一拷贝到目的对象中。



作者:SolaceClover
链接:https://www.jianshu.com/p/2d27b06e253f
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

猜你喜欢

转载自www.cnblogs.com/dusf/p/10470162.html