C# 值类型 / 引用类型 内存布局(栈、堆、托管堆)

这是 C# 最核心的底层知识点之一,直接决定了变量赋值、方法传参、GC 垃圾回收的行为

先记住 3 个核心概念

1.栈 (Stack)

  • 线程独有,自动分配、自动释放(离开作用域立刻销毁)
  • 速度极快,内存连续
  • 只存:值类型变量、引用类型的引用地址(指针)

2.托管堆 (Managed Heap)

  • 进程共享,由 GC(垃圾回收器)自动管理
  • 速度较慢,内存不连续
  • 只存:引用类型的实际对象数据

**3.**值类型 / 引用类型

  • 值类型structenum、基本类型(int/float/bool/DateTime 等)
  • 引用类型classstring、数组、委托、接口等

一、内存布局总规则

类型 变量本身存在哪里? 实际数据存在哪里?
值类型 栈(或嵌套在引用对象里) 变量自己身上(和变量同内存)
引用类型 栈(存一个内存地址 / 指针) 托管堆

一句话总结:

  • 值类型 = 数据直接存在栈上
  • 引用类型 = 栈上存地址,堆上存真实数据

二、值类型内存布局

代码示例

cs 复制代码
// 值类型
int a = 10;
DateTime now = DateTime.Now;

内存布局

cs 复制代码
【栈内存】
a: 10        ← 数据直接存在栈上
now: 时间值  ← 数据直接存在栈上

特点

1.变量离开作用域(如方法结束),栈自动弹出,内存立刻释放

**2.**赋值 = 完整拷贝一份数据

cs 复制代码
int b = a; // b 是全新独立的 10,改 b 不影响 a

三、引用类型内存布局

代码示例

cs 复制代码
// 引用类型
Person p = new Person();
p.Name = "小明";
p.Age = 20;

内存布局

cs 复制代码
【栈内存】          【托管堆】
p: 0x123456  →→→   Person 对象
                     Name: "小明"
                     Age: 20

:只存一个内存地址(指针)(64 位系统占 8 字节)

:存真正的对象数据

特点

1.赋值 = 只拷贝地址,不拷贝数据

cs 复制代码
Person p2 = p;
// p2 和 p 指向堆上同一个对象!改 p2.Age,p.Age 也会变

2.堆内存由 GC 自动回收(没有任何栈指针指向它时才会被回收)

四、混合场景:引用类型里包含值类型

cs 复制代码
public class Person  // 引用类型
{
    public string Name;  // 引用类型
    public int Age;      // 值类型
}

Person p = new Person();
p.Name = "小红";
p.Age = 18;

内存布局

cs 复制代码
【栈】        【堆】
p: 0x123 →→  Person 对象
               Name: 0x456 (指针) →→ 堆上字符串 "小红"
               Age: 18  ← 值类型直接存在堆里!

结论

  • 值类型如果被包在 class 里,它就存在堆上,不是栈!
  • 只有独立的、局部的值类型变量才在栈上

五、string 特殊说明(引用类型,但表现像值类型)

string引用类型,但它是 ** 不可变(immutable)** 的。

cs 复制代码
string s1 = "abc";
string s2 = s1;
s2 = "def";  // 不会修改 s1

内存:

  • s1s2 一开始指向同一个堆字符串
  • 重新赋值时,会在堆上创建新字符串,不再指向旧数据
  • 表现上像值类型,但底层依然是引用类型 + 托管堆

为什么字符串不能修改?

C# 设计 string 就是只读不可变的:

  • 你无法修改 "abc" 变成 "adc"

  • 任何 "修改" 字符串的操作(拼接、替换、赋值)都会创建新字符串,旧的不动

    cs 复制代码
    s1 = "a" + "b"; // 新建字符串
    s1 = s1.Replace("a","x"); // 新建字符串

    对比普通引用类型

  • 普通类(可修改)

    cs 复制代码
    Person p1 = new Person();
    p1.Name = "小明";
    
    Person p2 = p1;
    p2.Name = "小红"; // 会改 p1!因为指向同一个对象

    string(不可修改)

    cs 复制代码
    string s1 = "abc";
    string s2 = s1;
    s2 = "def"; // 不会改 s1!因为新建了对象
  • string 是引用类型

  • 存放在托管堆

  • 变量存的是地址

  • 但它不可变,不能修改内容

  • 赋值 = 换新地址,不是改旧内容

所以

cs 复制代码
s2 = "def";

不是修改 s2 指向的字符串,而是让 s2 指向了一个全新的字符串。

字符串 = 引用类型,但表现得像值类型 因为不可变,所以赋值不会互相影响。

六、快速判断内存位置

你可以直接用这个万能口诀:

  • 局部变量(方法内)

    • 值类型 →
    • 引用类型 → 栈存地址,堆存数据
  • 类的成员变量

    • 无论值 / 引用 → 全部存在堆上
  • 数组元素

    • 数组本身在堆 → 所有元素都在

七、值 / 引用类型核心区别

对比项 值类型 引用类型
存储位置 栈(或嵌入堆)
赋值行为 拷贝完整数据 只拷贝地址(指针)
内存管理 自动释放(栈弹出) GC 回收
空值 不能为 null 可以为 null
继承 继承自 System.ValueType 继承自 object

总结

  1. 栈 = 快、自动释放、存值类型 / 引用指针
  2. 托管堆 = 慢、GC 管理、存引用类型真实数据
  3. 值类型 = 数据自己存自己
  4. 引用类型 = 栈存地址,堆存数据
  5. class 里的值类型 → 存在堆上
相关推荐
chao1898444 小时前
完整MES系统实现 (C# 客户端服务器)
服务器·windows·c#
月昤昽4 小时前
autocad二次开发 2.旋转
c#·autocad·autocad二次开发
rockey6275 小时前
基于AScript的python3脚本语言发布啦!
python·c#·.net·script·python3·eval·expression·function·动态脚本
工程师0075 小时前
C# 字符串不可变性 + 字符串驻留池原理
c#·字符串拘留池
唐青枫10 小时前
内存为什么越来越高?C#.NET GC 详解:分代回收、LOH、终结器与性能优化实战
c#·.net
xiaohe0711 小时前
C#数据库操作系列---SqlSugar完结篇
网络·数据库·c#
yngsqq1 天前
平面图环 内轮廓
c#
rockey6271 天前
AScript之eval函数详解
c#·.net·script·eval·expression·动态脚本
He少年1 天前
【AI 辅助案例分享】
人工智能·c#·编辑器·ai编程