C# Unsafe指针与Marshal内存操作详解
本文深入解析C#中的不安全代码(unsafe)、指针原理及Marshal内存操作方法。通过实例演示如何通过指针修改值类型、引用类型及数组数据,掌握stackalloc栈内存分配,并利用System.Runtime.InteropServices.Marshal类进行非托管内存的申请、读写、释放及结构体编组,实现高效的底层内存控制。
内存基础架构
计算机内存由地址(Key)和数据(Value)组成。32位系统内存寻址为32位二进制数(最大2GB),64位系统则为64位。数据以8位字节(Byte,最小存储单位)为单位存储。在程序开发中,内存地址通常使用 IntPtr 类型表示,并以十六进制呈现。
指针的核心机制
指针用于直接访问指定内存地址的数据。读取时,会根据指针指向的数据类型长度截取相应大小的连续字节。例如,int 类型的指针会自动读取连续的4个字节,并将其转换为整型数值。
编译与安全环境配置
C# 默认启用安全检查,若需进行内存控制或指针操作,必须执行以下两步:
- 在代码中使用
unsafe关键字包裹相关语句块。 - 在项目属性中启用不安全代码:进入项目 → 右键“属性” → “生成”选项卡 → 勾选“允许不安全代码”(Allow unsafe code)。
基础内存操作实战
第1步:修改值类型变量
目的:获取变量地址并通过指针修改其原始值。
核心代码:
int val = 10;
unsafe
{
int* p = &val; // 获取 val 的内存地址
*p *= *p; // 间接赋值,val 变为 100
}
第2步:锁定并修改引用类型
目的:防止垃圾回收器(GC)移动字符串等可变引用对象,通过 fixed 关键字将堆内存锁定并转为指针。
核心代码:
string val = "ABC";
unsafe
{
fixed (char* p = val) // 锁定字符串内存区域
{
*p = 'D'; // 修改首个字符,val 变为 "DBC"
p[2] = 'E'; // 修改第三个字符,val 变为 "DBE"
}
}
第3步:数组成员的精准修改
目的:跳过索引器开销,直接通过内存偏移修改数组特定元素。
核心代码:
double[] array = { 0.1, 1.5, 2.3 };
unsafe
{
fixed (double* p = &array[2]) // 锁定目标元素起始地址
{
*p = 0.2; // array 变为 { 0.1, 1.5, 0.2 }
}
}
第4步:操纵类对象的内部字段
目的:针对特定对象实例的字段地址进行操作。
核心代码:
User val = new User() { age = 25 };
unsafe
{
fixed (int* p = &val.age)
{
*p += 1; // val.age 变为 26
}
}
public class User { public string name; public int age; }
第5步:基于IntPtr自定义地址操作
目的:模拟极端的内存读写场景(如驱动交互、逆向分析)。
注意:写入不可读写的物理或受保护地址会引发安全异常。
unsafe
{
char val = 'A';
IntPtr address = (IntPtr)&val;
byte* p = (byte*)address;
*p = (byte)'B'; // 改变 val 的值
}
第6步:void* 通用指针的使用
说明:void* 是一种不绑定具体数据类型的任意指针,常用于接收外部传入的通用内存句柄。使用时必须进行显式强转(如转为 int*)方可按类型存取数据。
第7步:使用 stackalloc 分配栈空间
优势:stackalloc 直接在调用栈上分配未初始化内存,速度远快于堆分配,无需手动 GC 释放。适用于高频短生命周期的缓冲区。
unsafe
{
int* buffer = stackalloc int[100]; // 分配400字节的栈内存
}
Marshal 与非托管内存控制
涉及跨进程通信、大文件处理及原生 C/C++ 库交互时,必须依赖 System.Runtime.InteropServices.Marshal 类。
内存的申请与释放
使用 AllocHGlobal 从非托管堆申请大块内存,操作完成后必须使用 FreeHGlobal 释放,否则会造成严重的内存泄漏。
using System.Runtime.InteropServices;
try
{
// 申请 1GB 空间
IntPtr ptr = Marshal.AllocHGlobal(1024 * 1024 * 1024);
// 写入单个字节
Marshal.WriteByte(ptr, offset: 0, value: 111);
// 批量拷贝:将托管数组拷贝至非托管内存
byte[] source = new byte[] { 1, 2, 3 };
Marshal.Copy(source, startIndex: 0, destination: ptr, length: 3);
// 反向拷贝:读取至托管数组
byte[] dest = new byte[5];
Marshal.Copy(source: ptr, destination: dest, startIndex: 1, length: 3);
}
finally
{
Marshal.FreeHGlobal(ptr);
}
数组元素的精准寻址
当托管数组被 GCHandle 固定或处于非移动状态时,可使用 UnsafeAddrOfPinnedArrayElement 获取数组内特定索引的绝对地址,方便进行高性能的指针遍历。
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













