using System; namespace _3_StudyStackAndHeap { /// <summary> /// 【值类型演示】自定义结构体 PointStruct /// struct 是值类型,所有字段数据直接存储在栈内存,不使用堆 /// 赋值时完整拷贝全部字段数据,多个变量互相独立互不干扰 /// </summary> struct PointStruct { public int X; public int Y; /// <summary> /// 结构体构造函数:给字段初始化 /// </summary> /// <param name="x">X坐标</param> /// <param name="y">Y坐标</param> public PointStruct(int x, int y) { X = x; Y = y; } /// <summary> /// 打印结构体信息 /// </summary> public void ShowInfo() { Console.WriteLine($"struct结构体 Point:X={X}, Y={Y}"); } } /// <summary> /// 【引用类型演示】Person类 class /// class属于引用类型:new出来的真实对象存放在堆内存 /// 栈上的变量仅仅保存堆对象的内存地址(引用) /// </summary> class Person { public string _name; public int _age; /// <summary> /// 实例构造函数:new的时候自动调用,在堆上创建对象 /// </summary> /// <param name="name">姓名</param> /// <param name="age">年龄</param> public Person(string name, int age) { _name = name; _age = age; } /// <summary> /// 打印Person对象信息 /// </summary> public void ShowInfo() { Console.WriteLine($"Person类对象:姓名={_name},年龄={_age}"); } } class Program { static void Main(string[] args) { Console.WriteLine("====================【模块1:引用类型 class 栈堆基础演示】===================="); // new Person:在堆内存开辟空间,创建真实对象;栈上p1保存堆对象的内存地址 Person p1 = new Person("Tom", 7); Console.WriteLine("执行 Person p1 = new Person(\"Tom\", 7);"); p1.ShowInfo(); // ⭐引用类型赋值:仅仅复制栈上保存的堆地址,不会在堆新建对象! Person p2 = p1; Console.WriteLine("\n执行 Person p2 = p1; 【只复制地址,堆不会产生新对象】"); Console.WriteLine("执行 p2._name = \"Jerry\"; 修改堆对象内部字段"); // ⚠重点:修改的是堆里面对象的数据!栈上p1、p2存储的地址编号本身完全不变! p2._name = "Jerry"; Console.Write("p1 的输出:"); p1.ShowInfo(); Console.Write("p2 的输出:"); p2.ShowInfo(); Console.WriteLine("👉小结:p1、p2栈中地址不变,共用同一个堆对象;修改堆对象字段两边同时变化\n"); Console.WriteLine("====================【模块2:引用类型易错点:重新new,更换栈变量保存的地址】===================="); Console.WriteLine("执行 p2 = new Person(\"Alice\",10);"); // new 在堆创建全新对象;修改栈变量p2里面存储的地址,p1与p2彻底分家 p2 = new Person("Alice", 10); Console.Write("p1 的输出:"); p1.ShowInfo(); Console.Write("p2 的输出:"); p2.ShowInfo(); Console.WriteLine("👉小结:只有重新new赋值,才会修改栈变量保存的堆地址,两者不再关联\n"); Console.WriteLine("====================【模块3:值类型 struct 栈内存演示】===================="); // struct 值类型:全部数据直接存栈,不碰堆内存 PointStruct s1 = new PointStruct(10, 20); Console.WriteLine("执行 PointStruct s1 = new PointStruct(10, 20);"); s1.ShowInfo(); // ⭐值类型赋值:栈内存完整拷贝全部字段,生成一份独立副本 PointStruct s2 = s1; Console.WriteLine("\n执行 PointStruct s2 = s1; 栈完整拷贝全部数据"); Console.WriteLine("执行 s2.X = 999; 修改副本s2的数据"); s2.X = 999; Console.Write("s1 的输出:"); s1.ShowInfo(); Console.Write("s2 的输出:"); s2.ShowInfo(); Console.WriteLine("👉小结:s1、s2是栈上两块独立数据;修改s2,完全不会影响s1\n"); Console.WriteLine("====================【模块4:方法参数传递 栈堆行为演示】===================="); Console.WriteLine("-------4.1 值类型作为参数:传入数据副本,方法内部修改不影响外部-------"); PointStruct st = new PointStruct(1, 2); Console.WriteLine("调用方法前外部st:"); st.ShowInfo(); ModifyStruct(st); Console.WriteLine("调用方法结束后外部st:"); st.ShowInfo(); Console.WriteLine("👉小结:传入的是栈副本,方法改副本,外部原始数据不受影响\n"); Console.WriteLine("-------4.2 引用类型作为参数:传入地址副本,修改堆对象字段会影响外部-------"); Person per = new Person("张三", 18); Console.WriteLine("调用方法前外部per:"); per.ShowInfo(); ModifyPersonField(per); Console.WriteLine("调用方法结束后外部per:"); per.ShowInfo(); Console.WriteLine("👉小结:形参拿到堆地址副本,修改堆对象字段,外部可以看到变化\n"); Console.WriteLine("-------4.3【高频大坑】方法内部给形参new对象,不会改变外部变量!-------"); Person pTest = new Person("原始数据", 20); Console.WriteLine("调用方法前外部pTest:"); pTest.ShowInfo(); ModifyPersonNew(pTest); Console.WriteLine("调用方法结束后外部pTest:"); pTest.ShowInfo(); Console.WriteLine("👉小结:p = new Person()仅仅修改局部形参p栈上的地址,外面变量完全不受影响!\n"); Console.WriteLine("====================【模块5:null 空引用演示,引用类型专属】===================="); Person pNull = null; Console.WriteLine($"Person pNull = null; pNull == null → {pNull == null}"); Console.WriteLine("null含义:栈上引用变量,不保存任何堆地址,不指向堆的任何对象"); // pNull.ShowInfo(); // ⚠取消注释运行:报运行时异常 NullReferenceException 空引用异常 // ❌值类型不允许赋值null PointStruct sNull = null; 直接编译报错 Console.WriteLine("\n====================【模块6:string特殊引用类型演示】===================="); string strA = "你好"; string strB = strA; strB = "世界"; Console.WriteLine($"strA={strA} strB={strB}"); Console.WriteLine("👉string是引用类型,但是字符串不可变;赋值会生成新字符串对象,表现类似值类型"); Console.ReadKey(); } /// <summary> /// 修改值类型结构体参数 /// 值类型传入:栈数据副本,方法内部修改只改动副本 /// </summary> /// <param name="s">结构体参数副本</param> static void ModifyStruct(PointStruct s) { s.X = 888; Console.WriteLine("----方法内部,修改之后的参数s----"); s.ShowInfo(); } /// <summary> /// 修改引用类型对象的字段 /// 传入堆地址副本,形参与外部变量指向同一个堆对象 /// 修改堆对象字段,外部可以感知变化 /// </summary> /// <param name="p">Person引用参数</param> static void ModifyPersonField(Person p) { p._name = "被方法修改"; Console.WriteLine("----方法内部,修改对象字段之后p----"); p.ShowInfo(); } /// <summary> /// ⚠易错坑演示:方法内部给引用形参new新对象 /// 仅仅修改局部形参p栈上存储的地址,外部变量完全不受影响 /// </summary> /// <param name="p">Person引用参数</param> static void ModifyPersonNew(Person p) { // 局部p指向堆上新对象,仅仅改变局部栈变量p p = new Person("方法内部全新对象", 99); Console.WriteLine("----方法内部new之后局部参数p----"); p.ShowInfo(); } } }栈与堆、值类型 & 引用类型综合知识点总结 > 基于上面示例代码整理,逐条记录
一、内存两块区域:栈 Stack、堆 Heap
- 栈 Stack:空间小、读写速度快。存放局部变量、方法参数。方法执行完毕自动回收栈内存。
- 引用类型的栈变量:只存放堆对象的内存地址(引用编号),不存储对象真实字段数据。
- 值类型的栈变量:直接存放完整的全部字段数据。
- 堆 Heap:存储空间大。执行
new class()时,在堆存放对象真实字段数据;由 GC 垃圾回收自动清理不再使用的堆对象。
struct 结构体属于值类型,默认数据全部保存在栈,不占用堆内存。
二、class 类(引用类型)内存行为
Person p1 = new Person("Tom", 7); Person p2 = p1; p2._name = "Jerry";new Person():在堆内存创建对象,保存_name、_age真实数据,返回堆内存地址。- 栈变量
p1保存堆地址,栈里面不会存放_name、_age。 Person p2 = p1;:栈上新变量 p2 复制 p1 里面的堆地址。堆不会创建新对象。p2._name = "Jerry";- ✅栈上 p1、p2 存储的地址编号完全不变,栈变量本身没有改动
- ✅ 修改的是堆内存里面对象的字段数据
- ✅ p1、p2 拿着同一个堆地址读取数据,两者读取到修改之后的值。
p2 = new Person("Alice", 10);new 在堆开辟全新对象;修改栈变量 p2 保存的堆地址编号。p1、p2 彻底分开,互不影响。
三、struct 结构体(值类型)内存行为
PointStruct s1 = new PointStruct(10, 20); PointStruct s2 = s1; s2.X = 999;- struct 全部字段数据直接存储在栈内存,不使用堆。
s2 = s1:栈内存完整拷贝全部字段,s1、s2 是两块互相独立的栈内存。- 修改
s2.X只会修改 s2 这份栈数据,s1 完全不受影响。 - 值类型不可以赋值 null,直接编译报错。
四、方法参数传递栈堆行为
- 值类型作为参数:传入栈数据的副本。方法内部修改副本,外部原始变量完全不受影响。
- 引用类型作为参数:传入堆地址的副本。形参和外部变量指向同一个堆对象。
- 如果修改
p._name:修改堆对象字段,外部变量可以看到变化。 - ⚠️ 高频大坑:方法内部写
p = new Person(),仅仅修改局部形参 p 栈上保存的地址,外部变量完全不受任何改变。仅仅局部参数换了指向。
- 如果修改
五、null 空引用
Person pNull = null;null:栈上引用变量,不保存任何堆地址,不指向堆中任何对象。- null 的变量调用对象方法/字段,运行抛出
NullReferenceException空引用异常,属于运行时错误,不是编译错误。 - struct 值类型不能赋值 null,编译直接报错。
六、string 特殊引用类型
string 属于引用类型,对象存放在堆;但是字符串具备不可变性。修改字符串变量,会生成全新的堆字符串对象,表现行为看起来和值类型一致。
七、核心记忆对照表
| 代码对象 | 存储位置 | 赋值行为 | 参数传递 | 能否为 null |
|---|---|---|---|---|
| class 类(引用类型) | 栈存地址;堆存真实对象 | 复制堆地址,多个变量共享同一个堆对象 | 传地址副本;改字段会影响外部;方法内重新 new 只改变局部参数 | ✅ 可以 null |
| struct 结构体(值类型) | 栈存完整全部数据 | 完整拷贝数据副本,变量互相独立 | 传数据副本;方法内修改副本不影响外部 | ❌ 不可以 null |
八、高频编译 & 运行错误点总结
- 修改引用类型对象字段,栈变量保存的地址不会发生变化;只有执行
变量 = new Xxx(),栈变量的地址才会被替换。 - 方法内部对引用形参执行
p = new Xxx(),不会改变外部变量,这是面试高频易错坑。 - null 引用变量调用成员,运行时报
NullReferenceException,不是编译报错。 - struct 值类型写
变量 = null直接编译报错。 - 区分概念:引用变量存的是门牌号(地址);堆才是真正的房子对象;修改房子内部物品,不会涂改纸条上门牌号。
九、通俗口诀
引用类型栈存门牌号,堆中才是真对象;
修改对象字段,只改堆,不动栈上门牌号;
只有重新 new 赋值,栈里面地址才换掉。
值类型全部放栈上,赋值拷贝全数据,两者互不干扰。
引用参数改字段,外部跟着变;参数内部做 new,外部看不见。
null 就是没有门牌号,调用成员直接报错。