【20260820 学习笔记】栈与堆、值类型与引用类型综合
2026/9/2 8:04:51 网站建设 项目流程
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

  1. 栈 Stack:空间小、读写速度快。存放局部变量、方法参数。方法执行完毕自动回收栈内存。
    • 引用类型的栈变量:只存放堆对象的内存地址(引用编号),不存储对象真实字段数据
    • 值类型的栈变量:直接存放完整的全部字段数据。
  2. 堆 Heap:存储空间大。执行new class()时,在堆存放对象真实字段数据;由 GC 垃圾回收自动清理不再使用的堆对象。

struct 结构体属于值类型,默认数据全部保存在栈,不占用堆内存。

二、class 类(引用类型)内存行为

Person p1 = new Person("Tom", 7); Person p2 = p1; p2._name = "Jerry";
  1. new Person():在堆内存创建对象,保存_name_age真实数据,返回堆内存地址。
  2. 栈变量p1保存堆地址,栈里面不会存放_name_age
  3. Person p2 = p1;:栈上新变量 p2 复制 p1 里面的堆地址。堆不会创建新对象
  4. p2._name = "Jerry";
    • 栈上 p1、p2 存储的地址编号完全不变,栈变量本身没有改动
    • ✅ 修改的是堆内存里面对象的字段数据
    • ✅ p1、p2 拿着同一个堆地址读取数据,两者读取到修改之后的值。
  5. p2 = new Person("Alice", 10);new 在堆开辟全新对象;修改栈变量 p2 保存的堆地址编号。p1、p2 彻底分开,互不影响。

三、struct 结构体(值类型)内存行为

PointStruct s1 = new PointStruct(10, 20); PointStruct s2 = s1; s2.X = 999;
  1. struct 全部字段数据直接存储在栈内存,不使用堆。
  2. s2 = s1:栈内存完整拷贝全部字段,s1、s2 是两块互相独立的栈内存。
  3. 修改s2.X只会修改 s2 这份栈数据,s1 完全不受影响。
  4. 值类型不可以赋值 null,直接编译报错。

四、方法参数传递栈堆行为

  1. 值类型作为参数:传入栈数据的副本。方法内部修改副本,外部原始变量完全不受影响。
  2. 引用类型作为参数:传入堆地址的副本。形参和外部变量指向同一个堆对象。
    • 如果修改p._name:修改堆对象字段,外部变量可以看到变化。
    • ⚠️ 高频大坑:方法内部写p = new Person(),仅仅修改局部形参 p 栈上保存的地址,外部变量完全不受任何改变。仅仅局部参数换了指向。

五、null 空引用

Person pNull = null;
  1. null:栈上引用变量,不保存任何堆地址,不指向堆中任何对象。
  2. null 的变量调用对象方法/字段,运行抛出NullReferenceException空引用异常,属于运行时错误,不是编译错误。
  3. struct 值类型不能赋值 null,编译直接报错。

六、string 特殊引用类型

string 属于引用类型,对象存放在堆;但是字符串具备不可变性。修改字符串变量,会生成全新的堆字符串对象,表现行为看起来和值类型一致。

七、核心记忆对照表

代码对象存储位置赋值行为参数传递能否为 null
class 类(引用类型)栈存地址;堆存真实对象复制堆地址,多个变量共享同一个堆对象传地址副本;改字段会影响外部;方法内重新 new 只改变局部参数✅ 可以 null
struct 结构体(值类型)栈存完整全部数据完整拷贝数据副本,变量互相独立传数据副本;方法内修改副本不影响外部❌ 不可以 null

八、高频编译 & 运行错误点总结

  1. 修改引用类型对象字段,栈变量保存的地址不会发生变化;只有执行变量 = new Xxx(),栈变量的地址才会被替换。
  2. 方法内部对引用形参执行p = new Xxx(),不会改变外部变量,这是面试高频易错坑。
  3. null 引用变量调用成员,运行时报NullReferenceException,不是编译报错。
  4. struct 值类型写变量 = null直接编译报错。
  5. 区分概念:引用变量存的是门牌号(地址);堆才是真正的房子对象;修改房子内部物品,不会涂改纸条上门牌号。

九、通俗口诀

引用类型栈存门牌号,堆中才是真对象;
修改对象字段,只改堆,不动栈上门牌号;
只有重新 new 赋值,栈里面地址才换掉。
值类型全部放栈上,赋值拷贝全数据,两者互不干扰。
引用参数改字段,外部跟着变;参数内部做 new,外部看不见。
null 就是没有门牌号,调用成员直接报错。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询