值类型与引用类型(上)

简介:

本文将介绍以下内容:

  • 类型的基本概念 
  • 值类型深入
  • 引用类型深入
  • 值类型与引用类型的比较及应用

 

1. 引言

买了新本本,忙了好几天系统,终于开始了对值类型和引用类型做个全面的讲述了,本系列开篇之时就是因为想写这个主题,才有了写个系列的想法。所以对值类型和引用类型的分析,是我最想成文的一篇,其原因是过去的学习过程中我就是从这个主题开始,喜欢以IL语言来分析执行,也喜好从底层的过程来深入了解。这对我来说,似乎是一件找到了有效提高的方法,所以想写的冲动就没有停过,旨在以有效的方式来分享所得。同时,我也认为,对值类型和引用类型的把握,是理解语言基础环节的关键主题,有必要花力气来了解和深入。  

2. 一切从内存开始

2.1 基本概念

从上回《第七回:品味类型---从通用类型系统开始》我们知道,CLR支持两种基本类型:值类型引用类型。因此,还是把MSDN这张经典视图拿出来做个铺垫。

 

值类型(Value Type),值类型实例通常分配在线程的堆栈(stack)上,并且不包含任何指向实例数据的指针,因为变量本身就包含了其实例数据。其在MSDN的定义为值类型直接包含它们的数据,值类型的实例要么在堆栈上,要么内联在结构中。我们由上图可知,值类型主要包括简单类型、结构体类型和枚举类型等。通常声明为以下类型:int、char、float、long、bool、double、struct、enum、short、byte、decimal、sbyte、uint、ulong、ushort等时,该变量即为值类型。  

引用类型(Reference Type),引用类型实例分配在托管堆(managed heap)上,变量保存了实例数据的内存引用。其在MSDN中的定义为引用类型存储对值的内存地址的引用,位于堆上。我们由上图可知,引用类型可以是自描述类型、指针类型或接口类型。而自描述类型进一步细分成数组和类类型。类类型是则可以是用户定义的类、装箱的值类型和委托。通常声明为以下类型:class、interface、delegate、object、string以及其他的自定义引用类型时,该变量即为引用类型。

下面简单的列出我们类型的进一步细分,数据来自MSDN,为的是给我们的概念中有清晰的类型概念,这是最基础也是最必须的内容。

  

2.2 内存深入

2.2.1. 内存机制

那么.NET的内存分配机制如何呢?

数据在内存中的分配位置,取决于该变量的数据类型。由上可知,值类型通常分配在线程的堆栈上,而引用类型通常分配在托管堆上,由GC来控制其回收。例如,现在有MyStruct和MyClass分别代表一个结构体和一个类,如下: 

using System;

public  class Test
{
     static  void Main()
    {
         // 定义值类型和引用类型,并完成初始化
        MyStruct myStruct =  new MyStruct();
        MyClass myClass =  new MyClass();
        
         // 定义另一个值类型和引用类型,
        
// 以便了解其内存区别
        MyStruct myStruct2 =  new MyStruct();
        myStruct2 = myStruct;
        
        MyClass myClass2 =  new MyClass();
        myClass2 = myClass;        
    }
}

在上述的过程中,我们分别定义了值类型变量myStruct和引用类型变量myClass,并使用new操作符完成内存分配和初始化操作,此处new的区别可以详见《第五回:深入浅出关键字---把new说透》  的论述,在此不做进一步描述。而我们在此强调的是myStruct和myClass两个变量在内存分配方面的区别,还是以一个简明的图来展示一下:

 

我们知道,每个变量或者程序都有其堆栈,不同的变量不能共有同一个堆栈地址,因此myStruct和myStruct2在堆栈中一定占用了不同的堆栈地址,尽管经过了变量的传递,实际的内存还是分配在不同的地址上,如果我们再对myStruct2变量改变时,显然不会影响到myStruct的数据。从图中我们还可以显而易见的看出,myStruct在堆栈中包含其实例数据,而myClass在堆栈中只是保存了其实例数据的引用地址,实际的数据保存在托管堆中。因此,就有可能不同的变量保存了同一地址的数据引用,当数据从一个引用类型变量传递到另一个相同类型的引用类型变量时,传递的是其引用地址而不是实际的数据,因此一个变量的改变会影响另一个变量的值。从上面的分析就可以明白的知道这样一个简单的道理:值类型和引用类型在内存中的分配区别是决定其应用不同的根本原因,由此我们就可以很容易的解释为什么参数传递时,按值传递不会改变形参值,而按址传递会改变行参的值,道理正在于此。 

对于内存分配的更详细位置,可以描述如下:

  • 值类型变量做为局部变量时,该实例将被创建在堆栈上;而如果值类型变量作为类型的成员变量时,它将作为类型实例数据的一部分,同该类型的其他字段都保存在托管堆上,这点我们将在接下来的嵌套结构部分来详细说明。
  • 引用类型变量数据保存在托管堆上,但是根据实例的大小有所区别,如下:如果实例的大小小于85000Byte时,则该实例将创建在GC堆上;而当实例大小大于等于85000byte时,则该实例创建在LOH(Large Object Heap)堆上。

更详细的分析,我推荐《类型实例的创建位置、托管对象在托管堆上的结构》。

2.2.2. 嵌套结构 

嵌套结构就是在值类型中嵌套定义了引用类型,或者在引用类型变量中嵌套定义了值类型,相信园子中关于这一话题的论述和关注都不是很多。因此我们很有必要发挥一下,在此就顺藤摸瓜,从上文对.NET的内存机制着手来理解会水到渠成。

  • 引用类型嵌套值类型

值类型如果嵌套在引用类型时,也就是值类型在内联的结构中时,其内存分配是什么样子呢? 其实很简单,例如类的私有字段如果为值类型,那它作为引用类型实例的一部分,也分配在托管堆上。例如: 

public  class NestedValueinRef

   // aInt做为引用类型的一部分将分配在托管堆上 
   private  int aInt;  
   public NestedValueinRef 
  { 
     // aChar则分配在该段代码的线程栈上 
      char achar = 'a'; 
  } 

其内存分配图可以表示为:

  

  •  值类型嵌套引用类型

引用类型嵌套在值类型时,内存的分配情况为:该引用类型将作为值类型的成员变量,堆栈上将保存该成员的引用,而成员的实际数据还是保存在托管堆中。例如:

public  struct NestedRefinValue
{
     public MyClass myClass;
     public NestedRefinValue
    {
        myClass.X = 1;
        myClass.Y = 2;
    }
}

其内存分配图可以表示为:

 

2.2.3. 一个简单的讨论

通过上面的分析,如果我们现在有如下的执行时:

AType[] myType = new AType[10];

试问:如果AType是值类型,则分配了多少内存;而如果AType是引用类型时,又分配了多少内存?

我们的分析如下:根据CRL的内存机制,我们知道如果ATpye为Int32类型,则表示其元素是值类型,而数组本身为引用类型,myType将保存指向托管堆中的一块大小为4×10byte的内存地址,并且将所有的元素赋值为0;而如果AType为自定义的引用类型,则会只做一次内存分配,在线程的堆栈创建了一个指向托管堆的引用,而所有的元素被设置为null值,表示为空。

--------引用地址:http://blog.csdn.net/NETZHOU/archive/2007/06/27/1668621.aspx


版权声明:本文原创发表于博客园,作者为路过秋天,原文链接:http://www.cnblogs.com/cyq1162/archive/2007/08/16/858023.html

目录
相关文章
|
消息中间件 Java 中间件
秒懂消息队列MQ,万字总结带你全面了解消息队列MQ
消息队列是大型分布式系统不可缺少的中间件,也是高并发系统的基石中间件,所以掌握好消息队列MQ就变得极其重要。接下来我就将从零开始介绍什么是消息队列?消息队列的应用场景?如何进行选型?如何在Spring Boot项目中整合集成消息队列。
23786 10
秒懂消息队列MQ,万字总结带你全面了解消息队列MQ
|
11月前
|
缓存 监控 固态存储
如何优化磁盘性能?
【10月更文挑战第4天】如何优化磁盘性能?
534 4
|
9月前
|
JSON 人工智能 Serverless
一键生成毛茸萌宠形象,基于函数计算极速部署ComfyUI生图系统
通过阿里云函数计算FC 和文件存储NAS,用户体验 ComfyUI 和预置工作流文件,用户可以快速生成毛茸茸萌宠等高质量图像。
一键生成毛茸萌宠形象,基于函数计算极速部署ComfyUI生图系统
|
12月前
|
存储 人工智能 并行计算
Pai-Megatron-Patch:围绕Megatron-Core打造大模型训练加速生态
Pai-Megatron-Patch(https://github.com/alibaba/Pai-Megatron-Patch)是阿里云人工智能平台PAI研发的围绕Nvidia MegatronLM的大模型开发配套工具,旨在帮助开发者快速上手大模型,完成大模型(LLM)相关的高效分布式训练,有监督指令微调,下游任务评估等大模型开发链路。最近一年来,我们持续打磨Pai-Megatron-Patch的性能和扩展功能,围绕Megatron-Core(以下简称MCore)进一步打造大模型训练加速技术生态,推出更多的的训练加速、显存优化特性。
|
存储 传感器 Java
格雷码(Gray Code)
格雷码(Gray Code)是一种二进制编码方式,它使用两种不同状态的信号(通常为 0 和 1)来表示二进制位。与普通的二进制编码不同,格雷码相邻的两个二进制位之间只相差一个比特。例如,对于 4 位二进制数,格雷码可以是 0000、0001、0011、0100、0101、0110、1000、1001、1010、1011、1100、1101、1110 和 1111。
2687 1
|
存储 Linux 调度
考研操作系统【1.5 操作系统引导与虚拟机】
操作系统是一种程序,程序以数据的形式存放于硬盘,硬盘分为多个区,一台计算机中有多个或多种外部存储设备;操作系统引导是计算机利用CPU运行特定程序,通过程序识别硬盘,识别硬盘分区,识别硬盘分区上的操作系统,最后通过程序启动操作系统。
565 0
|
算法 Java Android开发
Android 中使用 dlib+opencv 实现动态人脸检测
1 概述 完成 Android 相机预览功能以后,在此基础上我使用 dlib 与 opencv 库做了一个关于人脸检测的 demo。
1953 0