unsafe 代码与指针操作
大约 10 分钟约 2893 字
unsafe 代码与指针操作
简介
C# 在安全托管环境下运行,但通过 unsafe 上下文可以直接操作指针和内存。unsafe 代码绕过 CLR 的安全检查,提供类似 C/C++ 的底层内存操作能力,是高性能计算、互操作和系统编程的基础。
特点
指针基础
指针类型与操作
// 启用 unsafe:csproj 中 <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
unsafe
{
// 基本指针操作
int value = 42;
int* ptr = &value; // 取地址
Console.WriteLine((int)ptr); // 地址值
Console.WriteLine(*ptr); // 解引用 → 42
*ptr = 100; // 通过指针修改
Console.WriteLine(value); // 100
// 指针算术
int[] array = { 10, 20, 30, 40, 50 };
fixed (int* p = array) // 固定数组,防止 GC 移动
{
int* current = p;
for (int i = 0; i < 5; i++)
{
Console.WriteLine(*current); // 10, 20, 30, 40, 50
current++; // 移动到下一个 int(+4 字节)
}
// 指针下标访问
Console.WriteLine(p[2]); // 30
p[2] = 99; // 修改 array[2]
}
// 不同类型的指针
byte* bytePtr = (byte*)ptr; // 类型转换
double* dblPtr = (double*)Marshal.AllocHGlobal(sizeof(double));
*dblPtr = 3.14;
Marshal.FreeHGlobal((IntPtr)dblPtr); // 释放
}指针与数组操作
// 高性能数组操作(避免边界检查)
unsafe void ProcessArray(int[] data)
{
fixed (int* ptr = data)
{
int* end = ptr + data.Length;
for (int* p = ptr; p < end; p++)
{
*p = *p * 2 + 1; // 无边界检查,极致性能
}
}
}
// 对比:安全代码(有边界检查)
void ProcessArraySafe(int[] data)
{
for (int i = 0; i < data.Length; i++)
{
data[i] = data[i] * 2 + 1; // 每次访问都有边界检查
}
}
// 指针与多维数组
unsafe void Process2D(int[,] matrix)
{
fixed (int* ptr = matrix)
{
int length = matrix.Length;
for (int i = 0; i < length; i++)
{
ptr[i] = ptr[i] + 1; // 按一维连续内存访问
}
}
}
// 结构体指针
public struct Pixel
{
public byte R, G, B, A;
}
unsafe void InvertColors(Pixel[] pixels)
{
fixed (Pixel* ptr = pixels)
{
Pixel* end = ptr + pixels.Length;
for (Pixel* p = ptr; p < end; p++)
{
p->R = (byte)(255 - p->R); // -> 访问结构体成员
p->G = (byte)(255 - p->G);
p->B = (byte)(255 - p->B);
}
}
}stackalloc 栈分配
栈上分配内存
// stackalloc:在栈上分配内存,方法返回时自动释放
unsafe int SumStackAlloc(int count)
{
// 栈上分配 count 个 int
int* buffer = stackalloc int[count];
for (int i = 0; i < count; i++)
buffer[i] = i + 1;
int sum = 0;
for (int i = 0; i < count; i++)
sum += buffer[i];
return sum; // buffer 自动释放
}
// stackalloc 与 Span 配合(不需要 unsafe 上下文)
Span<int> buffer = stackalloc int[100]; // 安全使用
buffer.Fill(0);
buffer[0] = 42;
// 在热路径中使用 stackalloc 避免堆分配
unsafe string ProcessString(string input)
{
// 栈上分配 UTF8 缓冲
Span<byte> utf8Buffer = stackalloc byte[256];
int written = System.Text.Encoding.UTF8.GetBytes(input, utf8Buffer);
// 处理后返回
return System.Text.Encoding.UTF8.GetString(utf8Buffer[..written]);
}
// 注意:stackalloc 大小应有限制(通常 < 1MB)
// 过大导致 StackOverflow
// 对于大型分配,使用 ArrayPool 或 NativeMemoryfixed 语句深入
固定托管对象
// fixed 防止 GC 移动对象
unsafe void FixedExample()
{
string text = "Hello, World!";
// 固定字符串,获取字符指针
fixed (char* p = text)
{
for (int i = 0; i < text.Length; i++)
{
if (p[i] >= 'a' && p[i] <= 'z')
p[i] = (char)(p[i] - 32); // 转大写
}
}
// fixed 多个变量
int[] a = { 1, 2, 3 };
int[] b = { 4, 5, 6 };
fixed (int* pa = a, pb = b)
{
// 同时使用两个指针
for (int i = 0; i < 3; i++)
pa[i] += pb[i];
}
// fixed 与 fixed-size buffer
fixed (int* p = &a[0]) // 固定第一个元素的地址
{
*p = 100;
}
// 使用 ref 固定(C# 7.3+)
// fixed 语句可以用于任何支持 GetPinnableReference 的类型
Span<int> span = stackalloc int[10];
fixed (int* p = span) // Span 支持 fixed
{
p[0] = 42;
}
}
// 固定大小缓冲区(struct 内嵌数组)
unsafe struct NetworkHeader
{
public int Version;
public fixed byte MacAddress[6]; // 固定 6 字节
public fixed char Name[32]; // 固定 32 * 2 字节
public int Length;
}函数指针
delegate 的零开销替代
// C# 9 函数指针(delegate*)
// 比 delegate 更高效:无装箱、无分配、直接调用
unsafe
{
// 定义函数指针类型
delegate*<int, int, int> addPtr = &Add;
int result = addPtr(3, 4); // 直接调用,无委托分配
// 托管函数指针
delegate* managed<int, int, int> managedPtr = &Add;
// 非托管函数指针(用于 P/Invoke 回调)
delegate* unmanaged<int, int, int> unmanagedPtr = ...;
// StdCall 约定(Windows API)
delegate* unmanaged[Stdcall]<int, int, int> stdCallPtr = ...;
// Cdecl 约定(C 默认)
delegate* unmanaged[Cdecl]<int, IntPtr, IntPtr> cdeclPtr = ...;
}
static int Add(int a, int b) => a + b;
// 函数指针作为参数(高性能回调)
unsafe void ProcessWithCallback(int[] data, delegate*<int, bool> filter)
{
foreach (var item in data)
{
if (filter(item))
Console.WriteLine(item);
}
}
// 从 native 库加载函数指针
[DllImport("libcurl")] static extern IntPtr curl_easy_init();
[DllImport("libcurl")] static extern delegate* unmanaged[Cdecl]<IntPtr, IntPtr, IntPtr> curl_easy_setopt;NativeMemory 手动管理
非托管内存操作
using System.Runtime.InteropServices;
// .NET 6+ NativeMemory API
unsafe void NativeMemoryExample()
{
// 分配未初始化内存
int* buffer = (int*)NativeMemory.Alloc(100 * sizeof(int));
// 分配并清零
int* zeroed = (int*)NativeMemory.AllocZeroed(100 * sizeof(int));
// 重新分配
buffer = (int*)NativeMemory.Realloc(buffer, 200 * sizeof(int));
// 释放
NativeMemory.Free(buffer);
NativeMemory.Free(zeroed);
// 对齐分配(SIMD 等)
byte* aligned = (byte*)NativeMemory.AlignedAlloc(1024, alignment: 64);
NativeMemory.AlignedFree(aligned);
}
// 使用 SafeHandle 封装非托管内存
public class NativeBuffer : SafeHandle
{
public NativeBuffer(int size) : base(IntPtr.Zero, true)
{
SetHandle((IntPtr)NativeMemory.Alloc((nuint)size));
Size = size;
}
public int Size { get; }
public unsafe byte* Ptr => (byte*)handle;
protected override bool ReleaseHandle()
{
NativeMemory.Free(handle.ToPointer());
return true;
}
public override bool IsInvalid => handle == IntPtr.Zero;
}
// 使用
using var buf = new NativeBuffer(1024);
unsafe { buf.Ptr[0] = 42; }优点
缺点
实战:高性能内存操作
零拷贝数据传输
/// <summary>
/// 使用指针实现零拷贝的数据传输
/// </summary>
// 场景:将 byte[] 数据传递给 native 库
// ❌ 传统方式 — 拷贝数据
[DllImport("native.dll")]
static extern void ProcessData(byte[] data, int length);
// ✅ 指针方式 — 零拷贝
[DllImport("native.dll")]
static extern void ProcessDataPtr(IntPtr data, int length);
unsafe void ProcessWithZeroCopy(byte[] data)
{
fixed (byte* ptr = data)
{
ProcessDataPtr((IntPtr)ptr, data.Length);
}
}
// 场景:跨缓冲区数据复制
unsafe void FastCopy(byte[] source, byte[] dest, int count)
{
fixed (byte* src = source)
fixed (byte* dst = dest)
{
Buffer.MemoryCopy(src, dst, count, count);
// 或者使用指针逐字节复制
for (int i = 0; i < count; i++)
dst[i] = src[i];
}
}SIMD 与指针优化
/// <summary>
/// 使用 SIMD 指令加速数据处理
/// </summary>
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
unsafe void ProcessWithSIMD(byte[] data)
{
fixed (byte* ptr = data)
{
int length = data.Length;
int i = 0;
// 使用 SSE2 处理 16 字节块
if (Sse2.IsSupported)
{
Vector128<byte> mask = Vector128.Create((byte)0xFF);
for (; i + 16 <= length; i += 16)
{
Vector128<byte> chunk = Sse2.LoadVector128(ptr + i);
Vector128<byte> result = Sse2.AndNot(mask, chunk); // 清除高位
Sse2.Store(ptr + i, result);
}
}
// 处理剩余字节
for (; i < length; i++)
{
ptr[i] = (byte)(ptr[i] & 0x0F);
}
}
}
// 数组求和的 SIMD 优化
unsafe int SumArray(int[] array)
{
fixed (int* ptr = array)
{
int sum = 0;
int length = array.Length;
int i = 0;
if (Avx2.IsSupported)
{
Vector256<int> acc = Vector256<int>.Zero;
for (; i + 8 <= length; i += 8)
{
Vector256<int> chunk = Avx2.LoadVector256(ptr + i);
acc = Avx2.Add(acc, chunk);
}
// 水平求和
sum = acc.GetElement(0) + acc.GetElement(1) +
acc.GetElement(2) + acc.GetElement(3) +
acc.GetElement(4) + acc.GetElement(5) +
acc.GetElement(6) + acc.GetElement(7);
}
for (; i < length; i++)
sum += ptr[i];
return sum;
}
}内存对齐与结构体布局
/// <summary>
/// 内存对齐对性能的影响
/// </summary>
// 默认情况下,C# 编译器会对结构体进行对齐
// 每个字段按其自然对齐边界排列
// ❌ 不好的布局 — 有填充字节
public struct BadLayout
{
public byte A; // offset 0, 1 byte
// 3 bytes padding
public int B; // offset 4, 4 bytes
public byte C; // offset 8, 1 byte
// 3 bytes padding
public int D; // offset 12, 4 bytes
} // 总大小: 16 bytes(实际数据只有 10 bytes)
// ✅ 好的布局 — 无填充
public struct GoodLayout
{
public int B; // offset 0, 4 bytes
public int D; // offset 4, 4 bytes
public byte A; // offset 8, 1 byte
public byte C; // offset 9, 1 byte
} // 总大小: 12 bytes(节省 25%)
// 显式控制布局
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public struct PackedStruct
{
[FieldOffset(0)] public byte A;
[FieldOffset(1)] public int B; // 未对齐访问!可能较慢
[FieldOffset(5)] public byte C;
} // 总大小: 6 bytes(但 B 的访问可能需要多次内存操作)
// Size 与 Pack 的使用
[StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct NetworkPacket
{
public uint Header; // 4 bytes
public ushort Length; // 2 bytes + 2 bytes padding
public byte Flags; // 1 byte + 3 bytes padding
public int Payload; // 4 bytes
} // 总大小: 16 bytes总结
unsafe 代码提供 C/C++ 级别的内存操作能力。fixed 语句固定托管对象防止 GC 移动,是操作托管数组的前提。stackalloc 在栈上分配内存,方法返回自动释放,配合 Span<T> 实现安全使用。函数指针(delegate*)是 delegate 的零开销替代,适合热路径回调。NativeMemory 提供 malloc/free 级别的手动内存管理。unsafe 适用场景:高性能计算、图像处理、互操作、SIMD 优化。使用时务必注意内存安全和生命周期管理。
关键知识点
- 先明确这个主题影响的是语法层、运行时层,还是性能与可维护性层。
- 学习时要同时关注语言表面写法和编译器、JIT、GC 等底层行为。
- 真正有价值的是知道“为什么这样写”和“在什么边界下不能这样写”。
项目落地视角
- 把示例改成最小可运行样例,并观察编译输出、运行结果和异常行为。
- 如果它会进入团队代码规范,最好同步补充命名约定、禁用场景和替代方案。
- 涉及性能结论时,优先用 Benchmark 或实际热点链路验证,而不是凭感觉判断。
常见误区
- 只记语法糖,不知道底层成本。
- 把适用于小样例的写法直接搬到高并发或大对象场景里。
- 忽略框架版本、语言版本和运行时差异,导致结论失真。
进阶路线
- 继续向源码、IL、JIT 行为和 BCL 实现层深入。
- 把知识点和代码评审、性能诊断、面试复盘结合起来。
- 把同类主题做横向对比,例如值类型与引用类型、迭代器与 async 状态机、反射与 Source Generator。
适用场景
- 当你准备把《unsafe 代码与指针操作》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合在需要理解语言特性、运行时行为或 API 边界时阅读。
- 当代码开始出现性能瓶颈、可维护性问题或语义歧义时,这类主题会直接影响实现质量。
落地建议
- 先写最小可运行样例,再把结论迁移到真实业务代码。
- 同时记录这个特性的收益、限制和替代方案,避免为了“高级”而使用。
- 涉及内存、并发或序列化时,最好配合调试器或基准测试验证。
排错清单
- 先确认问题属于编译期、运行期还是语义误用。
- 检查是否存在隐式转换、装箱拆箱、闭包捕获或上下文切换等隐藏成本。
- 查看异常栈、日志和最小复现代码,优先排除使用姿势问题。
复盘问题
- 如果把《unsafe 代码与指针操作》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《unsafe 代码与指针操作》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《unsafe 代码与指针操作》最大的收益和代价分别是什么?
