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 里的值类型 → 存在堆上
相关推荐
2501_930707784 小时前
使用C#代码在 PowerPoint 中组合或取消组合形状
开发语言·c#
baivfhpwxf202312 小时前
c# 中对像之间频繁的转换会慢吗?
开发语言·c#
加号314 小时前
【C#】 实现 XRC 异或冗余校验:原理与实践
c#·xrc
小钻风336614 小时前
Java + Spring Boot 操作 Kafka 完整学习指南
c#·linq
叫我少年14 小时前
C# 程序的常规结构 — 命名空间、类型、入口点与表达式
c#
步步为营DotNet19 小时前
探秘.NET 11:C# 14 特性在后端性能优化中的深度应用
性能优化·c#·.net
Chris _data19 小时前
C# 与 PLC Modbus RTU 通信实践:从单例到线程安全的连接监控
开发语言·安全·c#
Chris _data19 小时前
C# WinForms 后台轮询 Modbus 数据与 UI 更新实践
开发语言·ui·c#
魔法阵维护师20 小时前
从零开发游戏需要学习的c#模块,第二十四章(场景管理 —— 标题、游戏、结束画面)
学习·游戏·c#
唐青枫20 小时前
别把登录写散了:C#.NET IdentityServer4 统一认证与 JWT 授权实战
c#·.net