堆内存、栈内存、内存地址
一、堆(Heap)
用途:存储动态分配的内存,比如通过
new
创建的对象。特点:
生命周期由程序控制(在 C# 中由垃圾回收器 GC 管理)。
内存较大,但分配/释放效率比栈慢。
所有引用类型的实例(如类)都在堆上创建。
示例(C#):
class Person { public string Name; }var p = new Person(); // p 是引用,指向堆上的对象
二、栈(Stack)
用途:用于存储函数调用过程中的局部变量、参数等。
特点:
生命周期短:函数执行完毕后自动释放。
分配速度快,先进后出(FILO)结构。
值类型变量(如
int
,bool
,struct
)如果是局部变量,通常存在栈上。
示例(C#):
void Test() {int a = 10; // a 是值类型,存在栈上
}
三、常见误解:“堆栈(Heap Stack)”
有时“堆栈”这个词在中文环境下被混用或误称,其实它应该具体说是“堆(Heap)”或“栈(Stack)”,不要把它们合成一个词理解为一个东西。
四、对比总结表:
对比项 | 堆(Heap) | 栈(Stack) |
---|---|---|
管理方式 | 手动或自动(GC) | 自动释放 |
存储内容 | 引用类型对象、数组 | 值类型变量、函数调用上下文 |
分配速度 | 慢 | 快 |
生命周期 | 程序控制/GC回收 | 方法调用期间 |
内存大小 | 较大 | 较小 |
如你用 C# 写代码,可以记住一句话:
值类型在栈,引用类型在堆(值在栈上,引用在堆上)(有些结构体情况略复杂,但大体如此)。
五、拓展:数组是值类型还是引用类型?
char[] a = new char[] { 'a', 'b', 'c' };
char[]
是引用类型(即使它内部装的是值类型char
)。数组本身存储在堆上。
变量
a
是一个引用(指针),它本身存在栈上,指向堆中的数组数据。
🧩 内存结构分析
对于这段代码:
char[] a = new char[] { 'a', 'b', 'c' };
内存中大概是这样:
栈(Stack)
a -> [ 堆上的数组地址 ]
堆(Heap)
数组对象 header(元数据)
['a', 'b', 'c'] ← 连续的 char 值(每个是 2 字节)
再看看这个例子
void Test()
{int[] nums = new int[] { 1, 2, 3 }; // 数组对象在堆上,nums 是引用,在栈上int x = nums[0]; // x 是值类型,值拷贝存在栈上
}
nums
是引用类型,在栈上;nums
指向堆上的int[]
数组;x
是值类型,直接在栈上存放拷贝的1
。
📝 总结
类型 | 值/引用类型 | 存储位置 |
---|---|---|
int | 值类型 | 栈 |
char | 值类型 | 栈 |
int[] | 引用类型 | 数组本体在堆上 |
char[] | 引用类型 | 数组本体在堆上 |
string | 引用类型 | 堆(字符串是不可变对象) |
补充:
一、C# 数组对象在堆上的结构大致如下:
以 char[] a = new char[] { 'a', 'b', 'c' }; 为例
注意:char 是 Unicode,每个占 2 字节
[ 栈 Stack ]
-----------------------
a (char[] 引用)
↓
0x0012F3A0 ← 地址,指向堆[ 堆 Heap ]
--------------------------------------
地址:0x0012F3A0
+------------------------+
| 对象头(Object Header)| ← 8 字节(64 位系统)
+------------------------+
| 方法表指针(MethodTbl)| ← 8 字节(指向类型元数据)
+------------------------+
| 数组长度:3 | ← 4 字节(int 类型)
+------------------------+
| 元素1:'a' | ← 2 字节(char = 2B)
+------------------------+
| 元素2:'b' | ← 2 字节
+------------------------+
| 元素3:'c' | ← 2 字节
+------------------------+
🖼️ 二、图示(ASCII 简化图)
[ Stack ]
+-------------------+
| a (变量名) |
| 0x0012F3A0 | → 指向堆
+-------------------+[ Heap ]
地址: 0x0012F3A0
+-----------------------------+
| Object Header (8 bytes) | ← CLR 控制的同步/GC 等信息
+-----------------------------+
| Method Table Ptr (8 bytes) | ← 指向类型描述,比如 System.Char[]
+-----------------------------+
| Array Length = 3 (4 bytes) |
+-----------------------------+
| 'a' (0x0061) (2 bytes) |
| 'b' (0x0062) (2 bytes) |
| 'c' (0x0063) (2 bytes) |
+-----------------------------+
📌 三、说明:
Object Header:CLR 内部用来记录同步锁、GC 状态等,不对用户可见;
Method Table Pointer:指向数组的类型信息,用于调用虚方法或进行类型检查;
Length 字段:数组的长度,是
System.Array
的字段;数组元素:紧随长度字段后,按顺序存储,内存上是连续的;
所以你可以用
fixed
获取数组首元素地址并按偏移读取。
四、在 Visual Studio 中查看变量地址的方法
方法 1:在“监视”窗口中使用 &变量
或 fixed
⚠️ 注意:只有值类型变量(如
int
,char
)才能直接取地址,
引用类型要通过间接方式(如GCHandle
或fixed
)
unsafe
{char[] arr = new char[] { 'a', 'b', 'c' };fixed (char* p = arr){Console.WriteLine((ulong)p); // 设置断点这里}
}
如图:
Name | Value | Type | |
---|---|---|---|
▶ | p | 0x000001a5828fe8b0 | char* |
▶ | &arr3 | 0x000000789477eb70 | System.Char[]* |
疑问:p和arr3的地址怎么不一样?
你的观察非常敏锐,这是一个很多人调试 C# 时的疑惑点。
你看到类似这样的情况:
&arr3 = 0x000000789477eb70 // arr3 的引用变量地址(在栈上)
p = 0x000001a5828fe8b0 // p 是 fixed 后数组元素的地址(在堆上)
✅ 为什么两个地址不一样?
因为:
&arr3
是 引用变量的地址(在栈上)
👉 它表示“arr3
这个变量本身存在的位置”,也就是栈中的地址p
是 数组首元素(arr3[0])的地址(在堆上)
👉 它是char[]
数组本体中第一个字符的实际地址,也就是堆中的地址
类比解释(图像化):
[栈 stack]
+----------------------+
| arr3 (变量名) | ← &arr3(变量自身的地址)
| 值:0x000001a5828fe8b0 ← 这是堆上数组的地址
+----------------------+[堆 heap]
0x000001a5828fe8b0 → 数组对象头 + 长度字段 + 'a' 'b' 'c'↑p (指向 arr3[0])
所以:
&arr3
:是栈中arr3
这个引用变量本身的地址(存储在栈上)arr3
的值:是一个指向堆上数组对象的地址p
:是fixed (char* p = arr3)
后,固定在堆上的数组首元素地址
方法 2、使用 “内存窗口” 查看内存中的内容
步骤:
设置断点后运行程序
命中断点后,点击菜单:
调试 > Windows > 内存 > 内存1
(也可以按Ctrl+Alt+M, 1
)
在地址栏输入:
p
或者0x02F3A210
(你从监视窗口看到的地址)回车,就能看到数组的内存内容:
0x000001A5828FE8B0 61 00 62 00 63 00 00 00 00 00 00 00 00 00 00 00 28 66 27 e9 fa 7f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 a.b.c...........(f'??...........................
如图:
⚙️ 五、如何启用 unsafe
支持?
如果你的项目不能编译,可以这样做:
✅ 方法 1:修改项目属性(推荐)
右键项目 → 属性
选择“生成”选项卡
勾选 “允许不安全代码”
保存后重新编译
✅ 方法 2:手动编辑 .csproj
文件:
<PropertyGroup><AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
✅ 总结
关键词 | 含义 |
---|---|
unsafe | 开启不安全上下文,允许指针操作 |
fixed | 固定托管对象在内存中,防止被 GC 移动 |
char* p | 获取数组首元素(arr[0] )的地址 |
仅供学习参考,如有侵权联系我删除