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

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

先记住 3 个核心概念

1.栈 (Stack)

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

2.托管堆 (Managed Heap)

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

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

  • 值类型 :struct、enum、基本类型(int/float/bool/DateTime 等)
  • 引用类型 :class、string、数组、委托、接口等

一、内存布局总规则

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

一句话总结:

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

二、值类型内存布局

代码示例

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

内存:

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

为什么字符串不能修改?

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 里的值类型 → 存在堆上
相关推荐
UIU1142 小时前
同一个变量在两个 .c 文件里类型不一致,程序会怎样?
c++·学习·c#
波力海苔夹心脆67513 小时前
C# 值类型与引用类型详解:存储位置、赋值机制、参数传递、相等比较、装箱拆箱与常见陷阱
开发语言·jvm·经验分享·笔记·c#·.net
估值探索者14 小时前
【Python量化策略实战 #02】多因子合成mom和vol两因子打分合成
开发语言·c++·python·数据挖掘·c#
淡海水1 天前
01-04-认知篇-Unity内存全景
unity·c#·游戏引擎·.net·gc
淡海水1 天前
01-03-认知篇-C#内存模型深度解析
unity·c#·游戏引擎·.net·gc
lzhdim2 天前
C#不为人知的10个魔法特性:资深开发者也会震惊的底层奥秘
java·前端·javascript·算法·c#
前端 贾公子2 天前
LangChain核心组件 == 提示词(Prompts)
开发语言·c#
码艺-Alimjan2 天前
Web 网站打包桌面应用的另一种方式,超级简单(C# exe 33Kb)
开发语言·前端·c#
海天鹰2 天前
C#入门:显示窗口
c#
淡海水2 天前
16-02-C#常用数据结构源码-附录B-源码索引速查
unity·c#·游戏引擎·il2cpp