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

本文详细介绍了C#中的值类型和引用类型在内存中的存储方式,强调了值类型作为类的成员时会存储在堆中,而引用类型变量存储在栈中,实例在堆中。数组作为引用类型,其元素无论值类型或引用类型都存储在堆中。此外,还讨论了类型嵌套情况下的存储规则,并通过实例解释了引用类型的赋值行为。文章最后总结了值类型和引用类型的主要区别。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


C# 的变量类型可以分为值类型和引用类型。
常见的值类型和引用类型可参考下面这个表格:
在这里插入图片描述
注:C# 的 struct 是值类型,这个很容易被忽略。

二者在内存中的存储方式

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

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

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

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

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

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

按照方法的执行顺序,第一步:
在这里插入图片描述
a 是局部变量,类型是 int,所以它会存储在栈内存。

第二步:
在这里插入图片描述
b 在这里同样是值类型的局部变量。因为栈是先进后出的数据结构,所以 b 会“压”在 a 的上面。

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

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

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

2)数组

int[] arr=new int[10];

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

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

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

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

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

二者区别的补充与总结

在这里插入图片描述
这里再提一下引用类型的赋值,比如:

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 引用的还是原本那块内存。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

YY-nb

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值