堆内存、栈内存、内存地址

一、堆(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)才能直接取地址
引用类型要通过间接方式(如 GCHandlefixed

unsafe
{
    char[] arr = new char[] { 'a', 'b', 'c' };

    fixed (char* p = arr)
    {
        Console.WriteLine((ulong)p); // 设置断点这里
    }
}

 如图:

NameValueType
p0x000001a5828fe8b0char*
&arr30x000000789477eb70System.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、使用 “内存窗口” 查看内存中的内容

步骤:

  1. 设置断点后运行程序

  2. 命中断点后,点击菜单:

    • 调试 > Windows > 内存 > 内存1(也可以按 Ctrl+Alt+M, 1

  3. 在地址栏输入:p 或者 0x02F3A210(你从监视窗口看到的地址)

  4. 回车,就能看到数组的内存内容:

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:修改项目属性(推荐)

  1. 右键项目 → 属性

  2. 选择“生成”选项卡

  3. 勾选 “允许不安全代码”

  4. 保存后重新编译

✅ 方法 2:手动编辑 .csproj 文件:

<PropertyGroup>
  <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

 


✅ 总结

关键词含义
unsafe开启不安全上下文,允许指针操作
fixed固定托管对象在内存中,防止被 GC 移动
char* p获取数组首元素(arr[0])的地址

仅供学习参考,如有侵权联系我删除

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值