C# 值类型和引用类型的区别

文章目录

  • 二者在内存中的存储方式
  • 二者区别的补充与总结

C# 的变量类型可以分为值类型和引用类型。
常见的值类型和引用类型可参考下面这个表格:
C# 值类型和引用类型的区别_第1张图片
注:C# 的 struct 是值类型,这个很容易被忽略。

二者在内存中的存储方式

值类型和引用类型最大的区别,就是它们在内存中的存储方式不同。
也许在很多地方你会看到这么一句话:值类型存储在栈中,引用类型存储在堆中。
实际上,这并不是严谨的说法。比较完整的说法应该是:

引用类型的变量在栈中分配,引用类型的实例在堆中分配(二段式)。相当于栈中存的是一个引用,这个引用指向了堆中具体的数据。

值类型总是分配在它声明的地方:作为类的字段(成员变量)时,跟随其所属的实例存储,也就是存储在了堆中;作为方法中的局部变量时,存储在栈上

数组本身是引用类型,但是不管数组存储的元素是值类型还是引用类型,都是存储在堆中

举几个例子:
1)一般的情况

 public void func()
 {
      int a = 1;
      double b = 2.5;
      Student s = new Student();//Student是自定义的类
 }

按照方法的执行顺序,第一步:
C# 值类型和引用类型的区别_第2张图片
a 是局部变量,类型是 int,所以它会存储在栈内存。

第二步:
C# 值类型和引用类型的区别_第3张图片
b 在这里同样是值类型的局部变量。因为栈是先进后出的数据结构,所以 b 会“压”在 a 的上面。

第三步:
C# 值类型和引用类型的区别_第4张图片
Student 类型的变量 s 作为引用类型的变量,本身只是个引用,它存储在栈内存中;Student 类的实例,也就是 new 出来的东西,存储在堆内存中。栈中的引用记录了堆内存中对应的实例的地址,可以理解为这个引用指向了堆中的实例。
假如 Student 类中有个成员变量 int age,虽然 int 是值类型,但它是声明在了类中,此时作为类的实例的一部分数据,会跟随类的实例存储在堆内存中。

注:使用引用类型的变量,实际上是去使用堆内存中分配的内存所对应的实例(或者说对象),因为这个变量记录了堆内存中数据的地址,那么就能通过地址找到对应的数据。只定义了引用类型的变量但是没去实例化,这个时候堆内存中并没有这个引用类型包含的数据,相当于我们只在栈中定义了一个指针(即引用类型的变量),但是这个指针不知道自己应该指向堆内存中哪一块空间(指向的是“空”)。因此如果这个时候去使用引用类型的变量,会出现空指针异常。

第四步(退出方法):
C# 值类型和引用类型的区别_第5张图片
这里需要注意的是局部变量占用的内存在退出方法后会自动被释放,而引用类型的实例仍然会存在于堆内存中,等待被垃圾回收。也就是栈的内存会被自动释放,堆的内存要借助 GC 自动释放。

2)数组

int[] arr=new int[10];

数组是引用类型,虽然这个数组中存储的每个元素是 int 类型(值类型),但是引用类型的数组中的值类型元素仍然被存储在堆内存中

3)类型嵌套
比如一个类(引用类型)里有个值类型的成员变量,或者一个结构体(值类型)中声明了一个引用类型的变量。

class Student{
	int age;
}
struct MyStruct{
	Student student;
}

关于前者,之前说过了,即使引用类型中嵌套了值类型,值类型仍然会跟随引用类型一起存储,即类中值类型的成员变量会跟随类的实例一起存储在堆内存中。

关于后者,首先要明确值类型中嵌套的引用类型不存在跟随问题。也就是结构体中的引用类型字段仍然按引用类型的存储方式进行存储,即字段本身被存储在栈内存,该引用类型的实例存储在堆内存。而结构体变量的存储方式要看结构体变量具体的声明位置,如果是作为局部变量,则存在于栈中,如果是嵌套在引用类型内,则跟随引用类型的实例一起存储在堆中。

二者区别的补充与总结

C# 值类型和引用类型的区别_第6张图片
这里再提一下引用类型的赋值,比如:

class Student{
	public int age;
}
Student s1=new Student();
Student s2=s1;
s2.age=5;
Console.WriteLine(s1.age);

此时会输出 5,虽然改变的是 s2 的 age,但是 s2 和 s1 引用的是同一块堆内存中的数据,改变的是堆内存中 Student 类的实例数据,所以 s1.age 也等于 5

注:string 会比较特殊

string s1="xx";
string s2=s1;
s2="hh";

此时 s1 仍然为 xx
实际上,这是运算符重载的结果。当 s2 被改变时,.NET在堆上为 s2 重新分配了内存,而 s1 引用的还是原本那块内存。

你可能感兴趣的:(#,C#知识,c#,开发语言)