值类型与引用类型

数据项的类型决定了存储它所需的内存大小(多少位),以及该类型包含的数据成员;同时,类型也规定了对象在内存中的存放位置 是栈(Stack)还是堆(Heap)。

类型分为两大类:值类型(Value Types)引用类型(Reference Types),两类对象在内存中的存储方式完全不同。

1、值类型仅占用一段内存,这段内存直接存储真实数据。

值类型的数据直接存储在栈Stack中,Data直接存储数据,例如int age = 12; 12就直接存储在这个stack中

2、引用类型需要两段内存:

  • 第一段存放真实数据,且永远分配在堆中
  • 第二段是一个引用地址,指向堆里数据所在的位置。(栈中存储堆中的地址(C语言时代叫指针))
引用类型数据存储在堆,数据存储区域会有一个地址,比如000001,然后栈中会存储这个地址Reference

以上两张图,仅仅是表示一个对象不属于其他对象的数据成员的时候,才符合

注意

引用类型对象的实体数据部分,永远存放在堆中,如上图。

但是值类型实例或者引用类型的引用地址,存储位置可能是堆也可能是栈,要看代码是如何。

例如:

假设有引用类型 Person,它包含两个成员 —— 一个值类型成员、一个引用类型成员。

很多人会误以为:值类型成员放栈、引用地址放栈,实体数据放堆(如上图),但这个结论是错误的。

记住一点:引用类型实例的全部实体数据永远在堆中。两个成员都属于这个对象的实体数据,因此无论成员本身是值类型还是引用类型,都会一并存放在堆内。

例子:

class Program
{
    static void Main(string[] args)
    {
        int a = 10;
        Person bill = new Person("Bill", 18);
    }

}

class Person
{
    private string name;

    private int age;

    private Phone myPhone;

    public Person(string name, int age)
    {
        this.name = name;
        this.age = age;
        this.myPhone = new Phone();
        this.myPhone.number = 12345;
    }
}   

class Phone
{
    public long number;
}Code language: C# (cs)

看上面例子,Person里面的数据成员,除了内置的类型age,name,还有一个Phone,这个Phone本身又是一个自定义类型。这个自定义类型Phone里面又有一个内置类型long,用来存储手机号码。

读者可能看到Person里面的int age,或者Phone 里面的long number ,以为它们是值类型,就存储在栈里面,其实不是的,虽然这些是内置的基本类型,但是由于它们是属于一个class自定义类型里面的数据成员,它们会作为一个实例的数据存储在堆里面的。最终数据存储会是这样:

上图可用看到,int age 和long number 看着都是内置类型,是值类型,但是它们属于自定义类型的数据成员,是存储在堆里面的,另外myPhone存储的是一个引用(地址),所以就算是引用类型的引用部分也可能是存储在堆的。

记住一点:自定义类型的数据成员都是存在在堆的,不管它是值类型还是引用类型,如果一个引用类型里面的数据成员例如上面的myPhone,对于Person里面存在的又是另外一个对象的地址。

如果上面Phone里面又有一个其他的自定义类型,那么Phone的数据里面又会有一个地方存在了另外一个自定义类型的地址(引用)。

整个程序就是从Main函数里面开始,第一个最顶层的引用类型的地址存在在栈里面,然后下面可能就像一条链一样,一层层地链接下去。

引用就像一条链一样

另外一个例子

// 引用类型 class
class MyType
{
    public int A;       // 值类型成员,存放在堆中MyType实例内部
    public Person B;    // 引用类型成员,引用指针存堆,实体数据另开堆空间
}

class Person
{
    public string Name;
}

void Test()
{
    // stack 存储 myObj 的引用指针
    MyType myObj = new MyType(); 
    /*
    Heap:
    MyType对象 {
        A: int(直接内嵌)
        B: 指向Person的引用指针
    }
    另一块Heap: Person{ Name: ... }
    */

    // 局部值类型变量,直接存在栈
    int localNum = 10;
}Code language: C# (cs)

以上说的数据,是自定义类型里面的数据成员,不是函数成员,如果一个类class,里面的函数成员又用到了局部变量或者引用,那么这些局部变量和引用类型实例的地址,还是会存在在栈的。例如下面代码:

再看下面的例子:

using System.ComponentModel.DataAnnotations;

class Program
{
    static void Main(string[] args)
    {
        int a = 10;
        Person bill = new Person("Bill", 18);
        bill.Calculate();
    }

}

class Person
{
    private string name;

    private int age;

    private Phone myPhone;

    public Person(string name, int age)
    {
        this.name = name;
        this.age = age;
        this.myPhone = new Phone();
        this.myPhone.number = 12345;
    }

    public int Calculate()
    {
        int one = 12;
        int two = 23;
        Phone myNewPhone = new Phone();
        return one + two;
    }
}   

class Phone
{
    public long number;
}Code language: C# (cs)

我在之前的代码中,给Person增加了一个Calculate的函数,然后函数里面有局部变量,有值类型也有引用类型,然后main函数中调用。这些函数里面的数据,它们不属于Person的数据成员,它们属于函数里面的局部变量,对于这些局部变量,如果是值类型,就存储在栈中,如果是引用类型,那么它的地址(引用)也是会存在在栈的。

当执行bill.Calculate();这句代码的时候,内存是这样的

上图中,蓝色部分,就算执行函数的时候,这些函数内部的变量的存储方式了。

一旦函数执行完毕,这几个变量就会出栈的。 就会变成这样

函数Calculate执行完毕后,回到Main函数,那么在函数里面创建的变量,就会出栈销毁了,但是对于myNewPhone的数据部分,也就是上图中的蓝色的,它在堆里面,栈中出栈的只是地址(引用),真正的数据部分是不会马上销毁的,它还会保留在堆中,等待垃圾回收器GC,在某个时间回收它。

Previous:
Next:

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注