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

来源:互联网 发布:服务器硬盘恢复数据 编辑:程序博客网 时间:2024/05/01 23:00

        一、值类型和引用类型变量的存储

    首先,变量是存储信息的基本单元,而对于计算机内部来说,变量就相当于一块内存空间。
    C#中的变量可以划分为值类型和引用类型两种:
    值类型:简单类型、结构类型、枚举类型
    引用类型:类、代表、数组、接口。

    (一)值类型和引用类型内存分配

    值类型是在栈中操作,而引用类型则在堆中分配存储单元。栈在编译的时候就分配好内存空间,在代码中有栈的明确定义,而堆是程序运行中动态分配的内存空间,可以根据程序的运行情况动态地分配内存的大小。因此,值类型总是在内存中占用一个预定义的字节数(比如,int占用4个字节,即32位)。当声明一个值类型变量时,会在栈中自动分配此变量类型占用的内存空间,并存储这个变量所包含的值。.NET会自动维护一个栈指针,它包含栈中下一个可用内存空间的地址。栈是先入后出的,栈中最上面的变量总是比下面的变量先离开作用域。当一个变量离开作用域时,栈指针向下移动被释放变量所占用的字节数,仍指向下一个可用地址。注意,值类型的变量在使用时必须初始化.

    而引用类型的变量则在栈中分配一个内存空间,这个内存空间包含的是对另一个内存位置的引用,这个位置是托管堆中的一个地址,即存放此变量实际值的地方。.NET也自动维护一个堆指针,它包含堆中下一个可用内存空间的地址,但堆不是先入后出的,堆中的对象不人在程序的一个预定义点离开作用域,为了在不使用堆中分存的内存时将它释放,.NET将定期执行垃圾收集。垃圾收集器递归地检查应用程序中所有的对象引用,当发现引用不再有效的对象使用的内存无法从程序中访问时,该内存就可以回收(除了fixed关键字固定在内存中的对象外)。(垃圾收集器原理?

    但值类型在栈上分配内存,而引用类型在托管堆上分配内存,却只是一种笼统的说法。更详细准确地描述是:

    1、对于值类型的实例,如果做为方法中的局部变量,则被创建在线程栈上;如果该实例做为类型的成员,则作为类型成员的一部分,连同其他类型字段存放在托管堆上,

    2、引用类型的实例创建在托管堆上,如果其字节小于85000byte,则直接创建在托管堆上,否则创建在LOH(Large Objet Heal)上。

    比如一下代码段:

        
public class Test
    {
        private int i;    //作为Test实例的一部分,与Test的实例一起被创建在GC堆上
        public Test()
        {
            int j = 0;     //作为局部实量,j的实例被创建在执行这段代码的线程栈上
        }
    }

    当执行语句:Test T = new T(); T.j=0;时,其内存分配过程如下:

        

    (二)嵌套结构的内存分配 

    所谓嵌套结构,就是引用类型中嵌套有值类型,或值类型中嵌套有引用类型。

    引用类型嵌套值类型是最常见的,上面的例子就是典型的例子,此时值类型是内联在引用类型中。

    值类型嵌套引用类型时,该引用类型作为值类型成员的变量,将在堆栈上保留关引用类型的引用,但引用类型还是要在堆中分配内存的。

    (三)关于数组内存的分配

    考虑当数组成员是值类型和引用类型时的情形:

    成员是值类型:比如int[] arr = new int[5]。arr将保存一个指向托管堆中4*5byte(int占用4字节)的地址的引用,同时将所有元素赋值为0;

    引用类型:myClass[] arr = new myClass[5]。arr在线程的堆栈中创建一个指向托管堆的引用。所有元素被置为null。

   

 

二、值类型和引用类型在传递参数时的影响

    由于值类型直接将它们的数据存放在栈中,当一个值类型的参数传递给一个方法时,该值的一个新的拷贝被创建并被传递,对参数所做的任何修改都不会导致传递给方法的变量被修改。而引用类型它只是包含引用,不包含实际的值,所以当方法体内参数所做的任何修改都将影响传递给方法调用的引用类型的变量。可以用下面图来表示:

    下面程序证明了这一点: 

        
class Class1
    {
        /// <summary>
        /// 应用程序的主入口点。
        /// </summary>
        [STAThread]
        static void Main(string[] args)
        {
            int i = 0;
            int[] intArr = new int[5];

            Class1.SetValues(i,intArr);
            //输出的结果将是:i=0,intArr[0]=10
            Console.WriteLine("i={0},intArr[0]={1}",i,intArr[0]);
            Console.Read();

        }

        public static void SetValues(int i,int[] intArr)
        {
            i = 10;
            for (int j = 0; j < intArr.Length; j++)
            {
                intArr[j] = i;
            }
        }
    }

三、装箱和拆箱

    装箱是将一个值类型转换为一个对象类型(object),而拆箱则是将一个对象类型显式转换为一个值类型。对于装箱而言,它是将被装箱的值类型复制一个副本来转换,而对于拆箱而言,需要注意类型的兼容性,比如,不能将一个值为“a”的object类型转换为int的类型。

    可以用以下程序来说明:

        
static void Main(string[] args)
        {
            int i = 10;

            //装箱
            object o = i;  //对象类型

            if (o is int)
            {
                //说明已经被装箱
                Console.WriteLine("i已经被装箱");
            }

            i = 20;  //改变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 Class1
    {
        /// <summary>
        /// 应用程序的主入口点。
        /// </summary>
        [STAThread]
        static void Main(string[] args)
        {
            string a = "1111";        //a是一个引用,指向string类的一个实例
            string b = a;             //b与a都是同一个对象
            
            //这时候b与a指向的并不是同一样对象,因为给b赋值后,已经创建了一个新的对象,并将这个新的string对象的引用赋给了b。
            b = "2222"; 

            //所以a的值不变,输出a=111.
            Console.WriteLine("a={0}",a);
            Console.ReadLine();
        }
    }

    但要注意,如果按引用传值时,则会与引用类型的参数一样,值会发生改变,比如以下代码:

        
class Class1
    {
        /// <summary>
        /// 应用程序的主入口点。
        /// </summary>
        [STAThread]
        static void Main(string[] args)
        {
            string a = "1111";        
            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)可以用等号的方式把一个值类型的变量赋给另一个值类型。

 

     

原创粉丝点击