C#中的值类型和引用类型,深拷贝,浅拷贝
from https://www.jianshu.com/p/2d27b06e253f
一.C#中的值类型和引用类型
- 概念
值类型直接存储其值。
引用类型存储对值的引用。
说起来有些拗口,其本质是Value
与Reference
的区别,在文档翻译过程中也有译者将Reference
翻译为参考。两种类型在内存中的存储方式有显著区别。
- 不同的存储对象
值类型变量存储的是变量的值,直接储存在栈内存中。
引用类型变量存储的是变量所在的内存地址,引用类型变量的实际数据存储于托管堆,变量本身仅仅是一个指向堆中实际数据的地址,存储于栈内存中,通常是四个字节。
- 不同的存储位置
值类型
Value
存储在线程堆栈中
引用类型
Reference
存储在托管堆上
内存格局通常划分为四个区:
全局数据区:存放全局变量,静态数据,常量
代码区:存放所有的程序代码
栈区:存放为运行而分配的局部变量,参数,返回数据,返回地址等
堆区:即自由存储区
为了理解值类型变量和引用类型变量的内存分配模型,我们应先区分两种不同的内存区域——线程堆栈Thread Stack
和托管堆Managed Heap
。
每一个正在运行的程序都对应着一个进程Process
,在一个进程内部,可以有一个或多个线程Thread
,每个线程都拥有一块“自留地”,成为线程堆栈
,大小为1M,用于保存自身的一些数据,如函数中定义的局部变量、函数调用时传送的参数值等。
现在我们可以解释第一句话——值类型存储在线程堆栈中,也就是说所有值类型的变量都是在线程堆栈中分配的。
另一块内存区域称为堆Heap
,在.NET这种托管环境下,堆由CLR(Common Language Runtime)管理,所以又称托管堆Managed Heap
。例如使用new
关键字创建类的对象实例时,分配给对象的内存单元就位于托管堆中。
- 不同的类型
这里类型区分的对象是C#中内建的类型Type
和用户自定义的类型。
C#中的值类型:C#有15个预定义类型,其中13个是值类型,两个是引用类型(string
和object
)。
由此分类可以得知,struct是轻量级的类
这句话本质上就不成立,两者的内存模型和行为表现都有区别。
- 不同的表现
1.值类型的表现
int a = 5;
int b = a;
上面这段代码中我们赋予a
一个常量值5,而赋予b为
a的值,这会在内存中两个不同的地方存储值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();
}
运行结果
Before:testA中A的值:
Before:testB中A的值:
After:testA中A的值:
After:testB中A的值:
可以看到testB改变了属性值之后,testA的属性值也随之改变,这是由于这两个对象只是一个指向堆内存的地址,实际指向的只有一份实际的值。
3. ref 引用和默认引用的关系,ref和out的关系
我们可以直接看如下代码的输出:
class Testref
{
public Testref() { }
public int Value;
} static void Main(string[] args)
{
Testref a = new Testref{Value = };
Console.WriteLine("Before TestRef, a.Value:{0}",a.Value); TestRef(a);
System.Diagnostics.Debug.Assert(a != null); Console.WriteLine("After TestRef, a.Value:{0}", a.Value); Console.WriteLine("Before TestRefEx, a.Value:{0}", a.Value);
TestRefEx(ref a);
System.Diagnostics.Debug.Assert(a != null);
Console.WriteLine("After TestRefEx, a.Value:{0}", a.Value); string str_a = "IIIIIIIIIIIII";
Console.WriteLine(str_a); string str_b = str_a;
//str_b[0] = 'H'; //error, string is const Console.WriteLine("Before b=a, a.Value:{0}", a.Value);
Testref b = a;
b.Value = ;
Console.WriteLine("After b=120, a.Value:{0}", a.Value);
b = null;
Console.WriteLine("After b=null, a.Value:{0}", a.Value);
} public static void TestRef(Testref a)
{
a.Value = ;
a = null; // will not trigger assert
} public static void TestRefEx(ref Testref a)
{
a.Value = ;
// a = null; // will trigger assert Testref b = new Testref { Value = };
a = b;
}
输出结果如下:
Before TestRef, a.Value:
After TestRef, a.Value:
Before TestRefEx, a.Value:
After TestRefEx, a.Value:
IIIIIIIIIIIII
Before b=a, a.Value:
After b=, a.Value:
After b=null, a.Value:
结论:默认情况下,除了13个预定义类型的值传递以外,其他对象默认情况下,传参和赋值都是默认引用形式。
但ref为显示但引用,如果在函数参数中使用了显示的ref引用,那修改函数内的引用或者引用的值,都会修改函数外的原对象。
修改引用相当于将原来对象丢弃,重新生成新的引用。(在C++中,没有直接的这样的情况,但是可以认为是指针的指针,**p,这种情况);如:TestRefEx,所示情况。
而默认引用,修改引用本身不影响函数外的对象,但修改引用所指的值,则会影响到函数外面的对象的值。如:TestRef,所示情况。
注意:ref和out是完全一样的行为,唯一不同的是,out的引用进入函数前可以不初始化,而ref的参数函数调用前必须初始化。
4.与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
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处
C#中的值类型和引用类型,深拷贝,浅拷贝的更多相关文章
- C# 中的值类型和引用类型
原文 C# 中的值类型和引用类型 值类型(value type):int,long,float,double,decimal,char,bool 和 struct 统称为值类型.值类型变量声明后,不管 ...
- C++ : 从栈和堆来理解C#中的值类型和引用类型
C++中并没有值类型和引用类型之说,标准变量或者自定义对象的存取默认是没有区别的.但如果深入地来看,就要了解C++中,管理数据的两大内存区域:栈和堆. 栈(stack)是类似于一个先进后出的抽屉.它的 ...
- 浅谈C#中的值类型和引用类型
在C#中,值类型和引用类型是相当重要的两个概念,必须在设计类型的时候就决定类型实例的行为.如果在编写代码时不能理解引用类型和值类型的区别,那么将会给代码带来不必要的异常.很多人就是因为没有弄清楚这两个 ...
- 【.Net】浅谈C#中的值类型和引用类型
在C#中,值类型和引用类型是相当重要的两个概念,必须在设计类型的时候就决定类型实例的行为.如果在编写代码时不能理解引用类型和值类型的区别,那么将会给代码带来不必要的异常.很多人就是因为没有弄清楚这两个 ...
- .NET中的值类型与引用类型
.NET中的值类型与引用类型 这是一个常见面试题,值类型(Value Type)和引用类型(Reference Type)有什么区别?他们性能方面有什么区别? TL;DR(先看结论) 值类型 引用类型 ...
- Windows Phone 开发起步之旅之二 C#中的值类型和引用类型
今天和大家分享下本人也说不清楚的一个C#基础知识,我说不清楚,所以我才想把它总结一下,以帮助我自己理解这个知识上的盲点,顺便也和同我一样不是很清楚的人一起学习下. 一说起来C#中的数据类型有哪些,大 ...
- C#中对值类型和引用类型的一点认识
区别值类型和引用类型的重要一点就是值类型赋值的时候是给出一块内存空间,空间里放下要赋给值类型的值.而引用类型是开辟一块内存空间,空间里放下的是要赋给引用类型值的指向地址. 就像一个是复制了银行卡里的现 ...
- js中的值类型和引用类型的区别
1.JavaScript中的变量类型有哪些? (1)值类型(基本类型):字符串(String).数值(Number).布尔值(Boolean).Undefined.Null (这5种基本数据类型是按 ...
- Swift 中的值类型与引用类型
顶级修饰 次级修饰 赋值类型 存储类型 值类型 值类型 深拷贝 栈 值类型 引用类型 浅拷贝 堆 引用类型 值类型 浅拷贝 堆 引用类型 引用类型 浅拷贝 堆 复合引用类型会改变内部值类型的存储行 ...
随机推荐
- 写给需要的Javaer-大数据学习路线篇
已经更新100+篇~ 关注公众号,BAT大神带你飞~ 听说你还在写Java,看Spring,看Dubbo,今天SpringCloud, 明天Dubbo3.X新版本... 10个开发9个半在写Java后 ...
- [树莓派]启用root账户
树莓派使用的linux是debian系统,所以树莓派启用root和debian是相同的. debian里root账户默认没有密码,但账户锁定. 当需要root权限时,由默认账户经由sudo执行,Ras ...
- 【mysql】mysql 调优之 ——执行计划 explain
1.what is explain(explain 是个什么东东) explain(解释),在 Mysql 中 作为一个关键词,用来解释 Mysql 是如何执行语句,可以连接 select .dele ...
- Python使用Xpath轻松爬虫(脑残式)
1.在PyCharm安装lxml. 2.找到源码 3.F12.copy源码的xpath 4.代码 from lxml import etree import requests wb_data = re ...
- 通过Python、BeautifulSoup爬取Gitee热门开源项目
一.安装 1.通过requests 对响应内容进行处理,requests.get()方法会返回一个Response对象 pip install requests 2.beautifulSoup对网页解 ...
- Mongo 用户管理
开启用户管理 auth = true 在配置文件或者参数中设置为改选项 开启认证服务,注意一点,很多人说在没有设置用户和配置用户之前,应该先不要开启,等设置完用户后再开启该参数,目前在win2008 ...
- 【c#】RabbitMQ学习文档(一)Hello World
一.简介 RabbitMQ是一个消息的代理器,用于接收和发送消息,你可以这样想,他就是一个邮局,当您把需要寄送的邮件投递到邮筒之时,你可以确定的是邮递员先生肯定会把邮件发送到需要接收邮件的人的手里,不 ...
- 探索Windows命令行系列(1):导航目录
探索Windows命令行系列(1):导航目录 探索Windows命令行系列(2):命令行工具入门 探索Windows命令行系列(3):命令行脚本基础 探索Windows命令行系列(4):通过命令操作文 ...
- 你真的会PHP吗?
Note: 1) PHP中的数据类型 PHP一共支持八种数据类型, 4种标量类型,boolean(布尔型),integer(整形),float/double(浮点型)和string(字符串类型), 2 ...
- linux下sh脚本/bin/bash^M问题解决
如果是在windows下编辑的脚本,到了linux下运行时会报出这样的错误/bin/bash^M:bad interpreter: No such file or directory这时因为编码的问题 ...