.Net Framemwork 之 值类型和引用类型的存储
C#把数据类型分为两种:值类型 和 引用类型。值类型存储在堆栈中,而引用类型存储在托管堆上。
一、值类型和引用类型变量的存储
首先,变量是存储信息的基本单元,而对于计算机内部来说,变量就相当于一块内存空间。
C#中的变量数据类型有两种:
[1] 值类型:简单类型、结构类型、枚举类型
[2] 引用类型:类、代表、数组、接口。
1、值类型和引用类型内存分配
值类型是在栈中操作,而引用类型则在堆中分配存储单元。
栈在编译的时候就分配好内存空间,在代码中有栈的明确定义,而堆是程序运行中动态分配的内存空间,可以根据程序的运行情况动态地分配内存的大小。因此,值类型总是在内存中占用一个预定义的字节数(比如,int占用4个字节,即32位)。当声明一个值类型变量时,会在栈中自动分配此变量类型占用的内存空间,并存储这个变量所包含的值。.NET会自动维护一个栈指针,它包含栈中下一个可用内存空间的地址。栈是先入后出的,栈中最上面的变量总是比下面的变量先离开作用域。当一个变量离开作用域时,栈指针向下移动被释放变量所占用的字节数,仍指向下一个可用地址。
注意,值类型的变量在使用时必须初始化。
引用类型的变量则在栈中分配一个内存空间,这个内存空间包含的是对另一个内存位置的引用,这个位置是托管堆中的一个地址,即存放此变量实际值的地方。.NET也自动维护一个堆指针,它包含堆中下一个可用内存空间的地址,但堆不是先入后出的,堆中的对象不会在程序的一个预定义点离开作用域,为了在不使用堆中分存的内存时将它释放,.NET将定期执行垃圾收集。垃圾收集器递归地检查应用程序中所有的对象引用,当发现引用不再有效的对象使用的内存无法从程序中访问时,该内存就可以回收(除了fixed关键字固定在内存中的对象外)。
但值类型在栈上分配内存,而引用类型在托管堆上分配内存,却只是一种笼统的说法。更详细准确地描述是:
[1] 对于值类型的实例,如果做为方法中的局部变量,则被创建在线程栈上;如果该实例做为类型的成员,则作为类型成员的一部分,连同其他类型字段存放在托管堆上,
[2] 引用类型的实例创建在托管堆上,如果其字节小于85000byte,则直接创建在托管堆上,否则创建在LOH(Large Objet Heap)上。
比如一下代码段:
public class Test
{
private int i; //作为Test实例的一部分,与Test的实例一起被创建在GC堆上
public Test()
{
int j = ; //作为局部实量,j的实例被创建在执行这段代码的线程栈上
}
}
2、嵌套结构的内存分配
所谓嵌套结构,就是引用类型中嵌套有值类型,或值类型中嵌套有引用类型。
引用类型嵌套值类型是最常见的,上面的例子就是典型的例子,此时值类型是内联在引用类型中。
值类型嵌套引用类型时,该引用类型作为值类型成员的变量,将在堆栈上保留关引用类型的引用,但引用类型还是要在堆中分配内存的。
3、关于数组内存的分配
考虑当数组成员是值类型和引用类型时的情形:
成员是值类型:比如int[] arr = new int[5]。arr将保存一个指向托管堆中4*5byte(int占用4字节)的地址的引用,同时将所有元素赋值为0;
引用类型:myClass[] arr = new myClass[5]。arr在线程的堆栈中创建一个指向托管堆的引用。所有元素被置为null。
二、值类型和引用类型在传递参数时的影响
由于值类型直接将它们的数据存放在栈中,当一个值类型的参数传递给一个方法时,该值的一个新的拷贝被创建并被传递,对参数所做的任何修改都不会导致传递给方法的变量被修改。而引用类型它只是包含引用,不包含实际的值,所以当方法体内参数所做的任何修改都将影响传递给方法调用的引用类型的变量。
下面程序证明了这一点:
class Program
{
/// <summary>
/// 应用程序的主入口点。
/// </summary>
[STAThread]
static void Main(string[] args)
{
int i = ;
int[] intArr = new int[]; Class1.SetValues(i,intArr);
//输出的结果将是:i=0,intArr[0]=10
Console.WriteLine("i={0},intArr[0]={1}",i,intArr[]);
Console.Read(); } public static void SetValues(int i,int[] intArr)
{
i = ;
for (int j = ; j < intArr.Length; j++)
{
intArr[j] = i;
}
}
}
三、装箱和拆箱
装箱:将一个值类型转换为一个对象类型(object);
拆箱:将一个对象类型显式转换为一个值类型。
对于装箱而言,它是将被装箱的值类型复制一个副本来转换,而对于拆箱而言,需要注意类型的兼容性,比如,不能将一个值为“a”的object类型转换为int的类型。
可以用以下程序来说明:
static void Main(string[] args)
{
int i = ; //装箱
object o = i; //对象类型 if (o is int)
{
//说明已经被装箱
Console.WriteLine("i已经被装箱");
} i = ; //改变i的值 //拆箱
int j = (int)o; //输出的结果是20,10,10
Console.WriteLine("i={0};o={1};j={2}",i,o,j);
Console.ReadLine();
}
四、关于string
string是引用类型,但却与其他引用类型有着一点差别。可以从以下两个方面来看:
[1] String类继承自object类。而不是System.ValueType。
[2] String本质上是一个char[],而Array是引用类型,同样是在托管的堆中分配内存。
但String作为参数传递时,却有值类型的特点,当传一个string类型的变量进入方法内部进行处理后,当离开方法后此变量的值并不会改变。原因是每次修改string的值时,都会创建一个新的对象。比如下面这段程序:
class Program
{
/// <summary>
/// 应用程序的主入口点。
/// </summary>
[STAThread]
static void Main(string[] args)
{
string a = ""; //a是一个引用,指向string类的一个实例
string b = a; //b与a都是同一个对象 //这时候b与a指向的并不是同一样对象,因为给b赋值后,已经创建了一个新的对象,并将这个新的string对象的引用赋给了b。
b = ""; //所以a的值不变,输出a=111.
Console.WriteLine("a={0}",a);
Console.ReadLine();
}
}
注意:如果按引用传值时,则会与引用类型的参数一样,值会发生改变,比如以下代码:
class Program
{
/// <summary>
/// 应用程序的主入口点。
/// </summary>
[STAThread]
static void Main(string[] args)
{
string a = "";
TestByValue(a); //输出a=111.
Console.WriteLine("a={0}",a); TestByReference(ref a); //按引用传值时则会改变,输出a="".
Console.WriteLine("a={0}",a);
Console.ReadLine();
} static void TestByValue(string s)
{
//设置值
s = "";
} static void TestByReference(ref string s)
{
//设置值
s = "";
}
}
五、关于C#中的堆和栈
C#中存储数据的地方有两种:线程堆栈 和 托管堆。
在传统的C/C++语言中,栈是机器操作系统提供的数据结构,而堆则是C/C++函数提供的。所以机器有专门的寄存器来指向栈所在的地址,有专门的机器指令实现数据的入栈/出栈动作。其执行效率高,但不过也正因为此,栈一般只支持整数、指针、浮点数等系统直接支持的类型。堆是由C/C++语言提供函数库来维护的,其内存是动态分配的。相对于堆来说,栈的分配速度快,不会有内存碎片,但支持的数据有限。
在C#中,值变量由系统分配在栈上。用来分配固定长度的数据(值类型大都有固定长度)。每一个程序都有单独的堆栈,其他程序不能访问。在调用函数时,调用函数的本地变量都被推入程序的栈中。与C/C++类似,堆用来存放可变长度的数据,不过与C/C++不同的是,C#中数据是存放在托管堆中。
由于值变量在栈中分配,所以把一个值变量赋给另一个值变量,会在栈中复制两个相同数据的副本;相反,把一个引用变量赋给另一个引用变量时,会在内存中创建对同一个位置的引用。
在栈中分配相对于堆中分配,有以下特点:
(1)分配速度快;
(2)用完以后自动解除分配;
(3)可以用等号的方式把一个值类型的变量赋给另一个值类型。
使用基于CLR的语言编译器开发的代码都成为托管代码。
托管堆是CLR中自动内存管理的基础。初始化新进程时,运行会为进程保留一个连续的地址空间区域。这个保留的地址空间被称为托管堆。托管堆维护着一个指针,用它指向将在堆中分配的下一个对象的地址。最初,该指针设置为指向托管堆的基址。
以下代码说明的很形象:
//引用类型(‘class’类类型)
class SomeRef
{
public int32 x;
} //值类型(‘struct’)
struct SomeVa
{
public Int32 x;
} static void Value TypeDemo()
{
SomeRef r1=new SomeRef();//分配在托管堆
SomeVal v1=new SomeVal();//分配堆栈上
r1.x=;//解析指针
v1.x=;//在堆栈上修改
SomeRef r2=r1;//仅拷贝引用(指针)
SomerVal v2=v1;//先在堆栈上分配,然后拷贝成员
r1.x=;//改变了r1,r2的值
v1.x=;//改变了v1,没有改变v2
}
栈是内存中完全用于存储局部变量或成员(值类型数据)的高效区域,但其大小有限制。
托管堆占内存比栈大得多,当访问速度较慢。托管堆只用于分配内存,一般有CLR来出来释放问题。
当创建值类型数据时,在栈上分配内存;
当创建引用型数据时,在托管堆上分配内存并返回对象的引用。注意这个对象的引用,像其他局部变量一样也是保存在栈中,该引用指向的值则位于托管堆中。
如果创建一个包含值类型的引用类型,比如数组,其元素的值也是存放在托管堆中的某个地方,由使用该实体的变量引用;而值类型存储在使用它们的地方,有几处在使用,就有几个副本存在。
对于引用类型,如果在声明变量的时候没有使用new运算符,运行时不会给它分配托管堆的内存空间,而是在栈伤给她分配一个包含null值的引用。对于值类型,运行时会给它分配栈上的空间,并且调用构造函数来初始化对象的状态。
一、栈和托管堆
通用类型系统(CTS)区分两种基本类型:值类型和引用类型。它们之间的根本区别在于它们在内存中的存储方式。.NET使用两种不同的物理内存快来存储数据------栈和托管堆:
值类型总是在内存中占用一个预定义的字节数(例如,int类型占4个字节,而string类型占用的字节数会根据字符串的长度而不同)。当生命一个值类型变量时,会在栈中分配适当大小的内存(除了引用类型的值类型成员外,如类的int字段)。内存中的这个空间用来存储变量所包含的值。.NET维护一个栈指针,它包含栈中下一个可用的内存空间的地址。当一个变量离开作用域时,栈指针指向下移动被释放变量所占有的字节数。所以它仍指向下一个可用地址。
引用变量也利用栈,但这时候栈包含的只是对另一个内存位置的引用,而不是实际值。这个位置是托管堆中的一个地址,和栈一样,他也维护一个指针,包含堆中下一个可用的内存地址。但是,堆不是先入后出的,因为对对象的引用可在我们的程序中传递(例如,作为参数传递给方法调用)。堆中的对象不会在程序的一个预定点离开作用域。为了在不适用在堆中分配的内存时将它释放。.NET定期执行垃圾回收集,垃圾收集递归检查应用程序中所有对象的引用。引用不再有效的对象使用的内存无法从程序中访问,该内存就可以回收。
二、类型层次结构
CTS定义了一种类型层次结构,该结构不仅仅描述了不同预定义类型,还指出了用户定义类型的层次结构中的
三、引用类型
引用类型包含一个指针,指向堆中存储对象本身的位置。因为引用类型只包含实际的值,对方法体内参数所做的任何修改都将影响传递给方法调用的引用类型的变量。
下图显示了声明一个字符串变量并把它作为参数传递给一个方法时所发生的事情。
string s1="something";
DoSomething(s1);
//.....
DoSomething(string s2)
{
//....
}
当声明一个字符变量s1时,一个值被压入栈中,它指向栈中的一个位置,在上图中,引用存放在地址1243044中,而实际的字符串存放在堆地址12662032中,当该字符串传递给一个方法中,在栈伤对应输入参数声明了一个新的变量(这次是在地址1243032上),保存在引用变量,即堆中内存位置中的值被传递给这个新的变量。
委托是引用方法的一种引用类型,类似于c++中的函数指针(两者的主要区别于委托包括调用其方法的对象)。
四、预定义的引用类型
有两种引用类型在c#中受到了特别的重视,他们的c#别名和预定义值类型的c#别名很相像。第一种是object(c#别名是object,o小写)。这是所有值类型和引用类型的最终基类。因为所有的类型派生object,所以可以把任何类型转换成Object,甚至值类型也可以转换。这个把值类型转换为Object的过程称为装箱。所有的值类型都派生自引用类型,在这件事看似矛盾的事情背后,装箱的作用不可或缺。
第二种是String类,字符串代表一个固定不变的Unicode字符序列,这种不变性意味着,一旦在堆中分配了一个字符串,他的值将永远不会改变,如果值该改变了,.NET就创建一个全新的String对象,并把它赋值给该变量,这意味着,字符串在很多方面都像值类型,而不像引用类型。如果把一个字符串传递给方法。然后在方法体内改变参数的值,这不会影响最初的字符串(当然,除非参数按引用传递的)。c#提供了别名String(s小写)来代表Ststem.String类,如果代码中使用String,必须在代码一开始添加Using System;这一行。使用内建的别名string则不需要using System;
.Net Framemwork 之 值类型和引用类型的存储的更多相关文章
- JavaScript高级 面向对象(6)--值类型与引用类型的存储特征
说明(2017.3.31): 1. 画图: var num = 123; var num2 = num; 值类型赋值的存储特点:将变量num内的数据全部拷贝一份,存储给新的变量num2,内存中有2个数 ...
- C# - 值类型、引用类型&走出误区,容易错误的说法
1. 值类型与引用类型小总结 1)对于引用类型的表达式(如一个变量),它的值是一个引用,而非对象. 2)引用就像URL,是允许你访问真实信息的一小片数据. 3)对于值类型的表达式,它的值是实际的数据. ...
- 【译】.NET中六个重要的概念:栈、堆、值类型、引用类型、装箱和拆箱
为何要翻译 一来是为了感受国外优秀技术社区知名博主的高质量文章,二来是为了复习对.NET技术的基础拾遗达到温故知新的效果,最后也是为了锻炼一下自己的英文读写能力.因为是首次翻译英文文章(哎,原谅我这个 ...
- 图解C#的值类型,引用类型,栈,堆,ref,out
C# 的类型系统可分为两种类型,一是值类型,一是引用类型,这个每个C#程序员都了解.还有托管堆,栈,ref,out等等概念也是每个C#程序员都会接触到的概念,也是C#程序员面试经常考到的知识,随便搜搜 ...
- c# 我所理解的 值类型 and 引用类型
一直以来对于值类型和引用类型都只是一个模糊的概念,趁最近有空深入理解了下. 先说说值类型,在msdn上是这样介绍值类型的. 意思就是值类型直接包含值. 变量引用的位置就是值所在内存中实际存储的位置,所 ...
- JAVA初学(1):值类型和引用类型的区别
JAVA值类型和引用类型的区别(转) [定义] 引用类型表示你操作的数据是同一个,也就 ...
- C#类和接口、虚方法和抽象方法及值类型和引用类型的区别
1.C#类和接口的区别接口是负责功能的定义,项目中通过接口来规范类,操作类以及抽象类的概念!而类是负责功能的具体实现!在类中也有抽象类的定义,抽象类与接口的区别在于:抽象类是一个不完全的类,类里面有抽 ...
- .NET面试题解析(01)-值类型与引用类型
系列文章目录地址: .NET面试题解析(00)-开篇来谈谈面试 & 系列文章索引 常见面试题目: 1. 值类型和引用类型的区别? 2. 结构和类的区别? 3. delegate是引用类型还 ...
- Emit学习(2) - IL - 值类型和引用类型(补)
上周末回家去享受生活了, 工作是为了更好的生活嘛, 所以我把生活, 工作分的比较开. 这几天不是很忙, 在学习工作技能的同时, 发点博文, 也算是做一个学习笔记 上篇中, 贴出的地址里面那位哥, 也有 ...
随机推荐
- jQuery插件开发,jquery插件
关于jQuery插件的开发自己也做了少许研究,自己也写过多个插件,在自己的团队了也分享过一次关于插件的课.开始的时候整觉的很复杂的代码,现在再次看的时候就清晰了许多.这里我把我自己总结出来的东西分享出 ...
- 【招聘需求】前端开发/PHP工程师【往全栈发展】
1.熟悉html.css,了解javascript2.熟悉任何一种服务端编程语言(php.python.java.asp.jsp.c.c++.c#,go等等)3.热爱工作,接受加班者优先 我们是公司内 ...
- 转:攻击JavaWeb应用[5]-MVC安全
转:http://static.hx99.net/static/drops/tips-347.html 攻击JavaWeb应用[5]-MVC安全 园长 · 2013/07/25 13:31 注:这一节 ...
- 最全python面试题
Python语言特性 1 Python的函数参数传递 看两个例子: a = 1 def fun(a): a = 2 fun(a) print a # 1 a = [] def fun(a): a.ap ...
- 第3天:YAML语法
YAML是一种可读性很强的数据格式语言.正是由于YAML良好的可读性,其广泛引用于软件配置中. 语法规则 YAML文件中的第一行为"---",表示这是一个YAML文件: YAML中 ...
- 【BZOJ 2118】 2118: 墨墨的等式 (最短路)
2118: 墨墨的等式 Description 墨墨突然对等式很感兴趣,他正在研究a1x1+a2y2+…+anxn=B存在非负整数解的条件,他要求你编写一个程序,给定N.{an}.以及B的取值范围,求 ...
- [BZOJ4003][JLOI2015]城池攻占(左偏树)
这题有多种做法,一种是倍增预处理出每个点往上走2^i步最少需要的初始战斗力,一种是裸的启发式合并带标记splay. 每个点合并能攻占其儿子的所有骑士,删去所有无法攻占这个城市的骑士并记录答案. 注意到 ...
- [Codeforces #210] Tutorial
Link: Codeforces #210 传送门 A: 贪心,对每个值都取最大值,不会有其他解使答案变优 #include <bits/stdc++.h> using namespace ...
- BZOJ 1202 [HNOI2005]狡猾的商人(并查集)
[题目链接] http://www.lydsy.com/JudgeOnline/problem.php?id=1202 [题目大意] 给出一些区间和的数值,问是否存在矛盾 [题解] 用并查集维护前缀和 ...
- 【优先队列】POJ1442-Black Box
[思路] 建立一个小堆和一个大堆.大堆用来存放第1..index-1大的数,其余数存放在大堆,小堆的堆顶元素便是我们要求出的第index大的数.每次插入一个A(n),必须保证大堆中数字数目不变,故先插 ...