C# 堆与栈 超清晰笔记

一、核心一句话总结

栈(Stack)自动分配、自动释放,存值类型、引用类型的引用地址,读写速度快,空间小。
堆(Heap)手动管理(GC回收),存引用类型的实际数据,空间大,读写稍慢。


二、基础概念区分

1. 栈(线程栈)

  1. 归属:每个线程独立拥有一块栈内存,线程私有
  2. 存储内容
    • 所有值类型intdoubleboolstruct、枚举
    • 引用类型变量的内存地址(引用)
    • 函数局部变量、形参、返回地址
  3. 内存特点
    • 大小固定、容量小
    • 先进后出(栈结构)
    • 生命周期随方法/代码块结束自动销毁,无需手动释放
    • 分配速度极快

2. 堆(托管堆)

  1. 归属:整个进程共享,全局公共内存,由 CLR 托管。
  2. 存储内容
    • 引用类型的真实对象数据classstring、数组、委托
  3. 内存特点
    • 空间巨大,可动态扩容
    • 内存分配零散
    • 不会随方法结束立刻释放,由 .NET GC(垃圾回收器) 定时回收
    • 分配速度慢于栈

三、C# 类型与堆/栈对应关系(重点)

1. 值类型 → 主要存栈

值类型关键字:struct
常见类型:
byteshortintlongfloatdoubledecimalcharbool自定义结构体

规则

  • 局部值类型变量:直接存在栈上
  • 如果值类型嵌套在引用类型内部:会跟着引用类型一起存到

2. 引用类型 → 地址在栈,数据在堆

引用类型关键字:class
常见类型:
objectstring、数组、自定义类、委托、接口

固定模型(必记)

栈内存:引用变量(存对象地址)
↓ 指向
堆内存:对象真实数据

四、代码示例 + 内存图解(最直观)

示例1:纯值类型(全在栈)

// 局部变量,int 是值类型
int a = 10;
int b = a;
b = 20;

内存分布

  1. 栈上开辟空间存 a = 10
  2. b = a值拷贝,栈上新空间存 b = 10
  3. 修改 b互不影响 a

结论:值类型赋值 = 拷贝数据本身


示例2:引用类型(栈存地址,堆存数据)

// 自定义类(引用类型)
class Person
{
    public string Name;
}

// 1. 在栈创建 p1 引用变量
Person p1 = new Person();
// 2. new 触发:在堆创建 Person 对象
p1.Name = "张三";

// 3. 引用赋值:只拷贝【内存地址】
Person p2 = p1;
p2.Name = "李四";

内存分布

  1. 栈:p1 → 保存堆中对象的地址
  2. 堆:存放 Person 实例 + Name 字符串数据
  3. p2 = p1栈上拷贝地址p1p2 指向堆中同一个对象
  4. 修改 p2.Namep1.Name 同步改变

结论:引用类型赋值 = 拷贝内存地址,多个变量指向同一堆对象


示例3:值类型嵌套在类中(值类型进堆)

struct Point // 结构体(值类型)
{
    public int X;
}

class Test // 类(引用类型)
{
    public Point pt; // 值类型成员
}

Test t = new Test();

内存:

  • 栈:变量 t(地址)
  • 堆:Test 对象 + 内部 Point 结构体

规则:值类型在引用类型内部 → 跟随宿主进入堆


五、栈 vs 堆 对比表(笔记整理用)

对比项 栈(Stack) 堆(Heap)
所有者 单个线程私有 整个进程共享
存储内容 值类型、引用地址 引用类型真实对象
分配速度 极快 较慢
空间大小 小、固定 大、动态
释放方式 方法结束自动释放 由 GC 垃圾回收
溢出问题 易出现栈溢出(StackOverflow) 易出现内存泄漏
生命周期 短(随代码块) 长(等待GC)

六、常见面试/笔试题考点(CSDN 笔记加分项)

  1. string 是什么类型?存在哪里?
    string引用类型,地址在栈,字符串数据在堆。
  2. 结构体(struct) 和 类(class) 本质区别?
    struct = 值类型(默认栈);class = 引用类型(栈地址+堆数据)。
  3. 为什么递归太深会报 StackOverflowException?
    每一次递归调用都会在栈上开辟栈帧,栈空间有限,超出容量就栈溢出。
  4. GC 会回收栈吗?
    不会。GC 只负责托管堆,栈由运行时自动清理。
  5. 数组存在哪里?
    数组是引用类型:数组引用在栈,数组整体数据存在堆

七、总结(精简版,放笔记末尾)

  1. 栈:快、自动、存地址+值类型,线程私有,用完即销毁。
  2. 堆:大、GC管理、存引用对象,全局共享,等待垃圾回收。
  3. 赋值区别:值类型拷贝值,引用类型拷贝地址
  4. 特殊规则:值类型嵌套在类中,会存入堆。

堆和栈的内存流向示意图如下

结合上文内容,我分文字结构化示意图

补充:C# 堆&栈 内存流向示意图

一、通用内存模型总览(全局架构图)

==================== 线程栈 (Stack) ====================
[局部变量、形参、返回地址、引用类型的地址指针]
        ↓ 指针指向
==================== 托管堆 (Heap) ======================
[引用类型实例对象、对象内部所有成员数据]
========================================================
规则:
1. 栈:线程私有,方法执行完自动释放
2. 堆:进程共享,由 .NET GC 回收

二、场景1:纯值类型(int)内存流向

对应代码:

int a = 10;
int b = a;
b = 20;

内存示意图

【线程栈】
┌──────────┐
│   a: 10  │  变量a,直接存储数值
├──────────┤
│   b: 10  │  执行 b=a,完整拷贝一份数值
└──────────┘

无堆内存参与

流向说明

  1. 声明 int a = 10:栈中开辟空间,存入数值 10
  2. int b = a:栈中新开辟空间,拷贝数值,两个变量相互独立
  3. 修改 b=20:仅修改栈中 b 的值,a 不受影响

三、场景2:引用类型(自定义类)核心流向(重点)

对应代码:

class Person
{
    public string Name;
}
Person p1 = new Person();
p1.Name = "张三";
Person p2 = p1;
p2.Name = "李四";

内存示意图

【线程栈】                【托管堆】
┌────────────┐          ┌────────────────────────┐
│ p1: 0x0001 │ ────────►│ Person 对象实例        │
├────────────┤          │  Name: "张三" → "李四" │
│ p2: 0x0001 │ ────────►└────────────────────────┘
└────────────┘

流向说明

  1. Person p1:栈上创建引用变量 p1,此时无指向
  2. new Person()在堆中创建实体对象,CLR 返回对象内存地址 0x0001,赋值给栈中的 p1
  3. p1.Name = "张三":通过栈上地址,修改堆中对象的成员
  4. Person p2 = p1:栈上创建 p2仅拷贝地址 0x0001,两个变量指向堆中同一个对象
  5. 修改 p2.Name:本质修改堆内数据,因此 p1.Name 同步变化

四、场景3:值类型嵌套在引用类型中(结构体+类)

对应代码:

struct Point { public int X; }
class Test { public Point pt; }

Test t = new Test();
t.pt.X = 100;

内存示意图

【线程栈】                【托管堆】
┌────────────┐          ┌────────────────────────┐
│  t: 0x0002 │ ────────►│ Test 对象              │
└────────────┘          │  ┌────────────────┐    │
                        │  │ Point 结构体   │    │
                        │  │   X: 100       │    │
                        │  └────────────────┘    │
                        └────────────────────────┘

流向说明

  1. 栈中创建引用变量 t,存储堆对象地址 0x0002
  2. new Test() 在堆中创建 Test 对象
  3. 结构体 Point 是值类型,但作为类的成员,会直接内嵌在堆的宿主对象中,不再单独存在于栈

五、场景4:数组(引用类型)内存流向

对应代码:

int[] arr = new int[3];
arr[0] = 1;

内存示意图

【线程栈】                【托管堆】
┌────────────┐          ┌────────────┬────────────┬────────────┐
│ arr:0x0003 │ ────────►│  1         │  0         │  0         │
└────────────┘          │ 数组元素1  │ 数组元素2  │ 数组元素3  │
                        └────────────┴────────────┴────────────┘

流向说明

数组属于引用类型:

  • 数组引用变量 arr 存于栈,记录堆地址
  • 数组的所有元素(哪怕是值类型 int)全部存储在托管堆中

补充:栈溢出场景示意图(面试考点)

递归调用会持续往栈中新增栈帧,栈空间固定,超出上限则报错 StackOverflowException

【线程栈 空间上限】
┌────────────┐  第1层递归
├────────────┤  第2层递归
├────────────┤  第3层递归
├────────────┤  ... 持续叠加
├────────────┤  栈空间已满 → StackOverflow 栈溢出
└────────────┘

更多推荐