0Pricing
C# Academy · 课时

不安全代码、指针与固定缓冲区

使用 unsafe 关键字处理指针,使用 fixed 固定托管内存,并访问结构体中的固定大小缓冲区。

不安全代码、指针与固定缓冲区 是 CoddyKit 上的免费 C# Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 C# Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 C# Academy 课程共包含 4 节课。

什么是不安全代码

C# 的 unsafe 关键字支持直接操作内存,包括原始指针、指针运算和固定大小的缓冲区。它会绕过垃圾回收器的安全保证。请在项目文件中添加 <AllowUnsafeBlocks>true</AllowUnsafeBlocks> 来启用它。

// Must add to .csproj:
// <AllowUnsafeBlocks>true</AllowUnsafeBlocks>

// Unsafe blocks can appear inside methods:
unsafe
{
    int x = 42;
    int* ptr = &x;          // get address of x
    Console.WriteLine(*ptr); // dereference — prints 42
    *ptr = 100;
    Console.WriteLine(x);    // 100 — x was mutated via pointer
}

C# 中的指针类型

指针语法与 C/C++ 类似:T* 表示指向 T 的指针。您可以声明指向非托管值类型的指针,例如 int、double,以及不含引用字段的结构体。不允许使用指向托管类型的指针。

unsafe
{
    int    i = 10;
    double d = 3.14;

    int*    ip = &i;
    double* dp = &d;

    // Dereference with *
    Console.WriteLine(*ip); // 10

    // Pointer arithmetic — move to next int in memory
    int[] arr = { 1, 2, 3 };
    fixed (int* p = arr)
    {
        Console.WriteLine(*(p + 0)); // 1
        Console.WriteLine(*(p + 1)); // 2
        Console.WriteLine(*(p + 2)); // 3
    }
}

fixed 语句

托管对象可能会被垃圾回收器重新定位。要获取托管对象的地址,必须使用 fixed 将其固定。在 fixed 代码块持续期间,垃圾回收器不会移动该对象。

byte[] buffer = new byte[256];

unsafe
{
    fixed (byte* pBuf = buffer)
    {
        // pBuf is valid only inside this block
        // GC won't relocate 'buffer' here
        for (int i = 0; i < buffer.Length; i++)
            pBuf[i] = (byte)i;
    }
    // After 'fixed', GC can move 'buffer' again
}

Console.WriteLine(buffer[5]); // 5

指针运算

您可以对指针加上或减去整数。给 int* 加 1 会使其前进 4 个字节(sizeof int)。这是高速批量内存操作的基础。

unsafe
{
    int[] data = { 10, 20, 30, 40, 50 };
    fixed (int* start = data)
    {
        int* p = start;
        long sum = 0;
        for (int i = 0; i < data.Length; i++)
        {
            sum += *p;
            p++;    // advance by sizeof(int) = 4 bytes
        }
        Console.WriteLine(sum); // 150

        // Or with index syntax:
        Console.WriteLine(start[2]); // 30
    }
}

stackalloc:栈分配

stackalloc 会在栈上而不是堆上分配一块内存。不增加垃圾回收压力,也不需要固定。它仅限于当前方法的栈帧,方法返回时会自动释放这块内存。

// Stack-allocated buffer — zero heap allocation
Span<int> buffer = stackalloc int[128]; // safe Span wrapper
buffer.Fill(0);
buffer[0] = 42;
Console.WriteLine(buffer[0]); // 42

// Or raw pointer form (requires unsafe):
unsafe
{
    int* raw = stackalloc int[128];
    raw[0] = 99;
    Console.WriteLine(raw[0]); // 99
}
// Stack frame popped — memory gone

结构体中的固定大小缓冲区

fixed-size 缓冲区会将固定长度的数组内嵌在结构体中,不进行堆分配,也不需要额外的指针。请在 unsafe 上下文中声明的结构体内使用 fixed 修饰符。

unsafe struct NetworkPacket
{
    public int  Length;
    public byte Command;
    public fixed byte Payload[256]; // 256 bytes inline in the struct
}

unsafe
{
    NetworkPacket pkt;
    pkt.Length  = 10;
    pkt.Command = 0x01;

    // Fill payload inline — no heap allocation
    for (int i = 0; i < 10; i++)
        pkt.Payload[i] = (byte)i;

    Console.WriteLine(pkt.Payload[5]); // 5
}
// sizeof(NetworkPacket) = 4 + 1 + 256 = ~261 bytes on stack

void* 与类型转换

void* 是一种无类型指针,适用于通用内存操作。在取消引用之前,必须将其转换为带类型的指针。在不安全上下文中,sizeof(T) 运算符可用于非托管类型。

unsafe
{
    int value = 42;
    void* vp = &value;      // typeless pointer
    int*  ip = (int*)vp;    // cast back to int*
    Console.WriteLine(*ip); // 42

    // sizeof works on unmanaged types
    Console.WriteLine(sizeof(int));    // 4
    Console.WriteLine(sizeof(double)); // 8
    Console.WriteLine(sizeof(long));   // 8
}

MemoryMarshal 与 Unsafe 类

System.Runtime.CompilerServices.Unsafe 和 System.Runtime.InteropServices.MemoryMarshal 为许多底层操作提供了安全的非关键字替代方案,与 Span 兼容,并且无需使用 unsafe 关键字。

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

// Reinterpret a Span<byte> as Span<int> — no copy, no unsafe keyword
byte[] raw = new byte[16];
Span<int> ints = MemoryMarshal.Cast<byte, int>(raw);
ints[0] = 12345678;
Console.WriteLine(ints[0]); // 12345678

// Unsafe.As<,> — reinterpret reference type (advanced)
ref int first = ref MemoryMarshal.GetReference(ints);
Unsafe.Add(ref first, 1) = 99999;
Console.WriteLine(ints[1]); // 99999

何时不应使用不安全代码

只有在确实有明确的性能需求且不存在安全替代方案时,才应使用不安全代码。请将不安全代码的范围保持在最小程度,将其隔离到私有辅助方法或专用类型中。请编写全面的测试并记录相关假设。

// GOOD: narrow unsafe scope
public static unsafe int SumBytes(ReadOnlySpan<byte> data)
{
    int total = 0;
    fixed (byte* p = data)
    {
        byte* end = p + data.Length;
        for (byte* cur = p; cur < end; cur++)
            total += *cur;
    }
    return total;
}

// BETTER for modern code: use SIMD via Vector<T> or hardware intrinsics
// which stay in managed code but still achieve native-level perf

实际应用:解析二进制协议

解析二进制协议时,如果结构体布局必须与线路格式完全匹配,不安全指针和固定大小缓冲区就能发挥优势。结构体可以 directly 从字节缓冲区转换而来,无需复制。

[StructLayout(LayoutKind.Sequential, Pack = 1)]
unsafe struct MessageHeader
{
    public uint  Magic;        // 4 bytes
    public ushort Version;    // 2 bytes
    public ushort PayloadLen; // 2 bytes
    public fixed byte Id[16]; // 16 bytes GUID inline
}

unsafe ReadOnlySpan<byte> SerializeHeader(MessageHeader hdr)
{
    // Cast struct directly to bytes — zero copy
    return new ReadOnlySpan<byte>(&hdr, sizeof(MessageHeader));
}

unsafe MessageHeader ParseHeader(ReadOnlySpan<byte> buf)
{
    fixed (byte* p = buf)
        return *(MessageHeader*)p; // reinterpret cast
}

快速检查

获取托管对象地址时,为什么需要使用 fixed 语句?

回顾:不安全代码、指针与固定缓冲区

要点:

  • unsafe 关键字支持使用原始指针,需要在项目中启用 AllowUnsafeBlocks
  • fixed 语句会固定托管对象,使垃圾回收器不会重新定位它们
  • 指针运算按照元素大小前进,而不是按照字节数前进
  • stackalloc 在栈上分配内存,不增加垃圾回收压力,并在方法退出时释放
  • 固定大小缓冲区(fixed byte Payload[N])会将数组直接内嵌在结构体中
  • 在大多数场景中,优先使用 Span<T>、MemoryMarshal 和 Unsafe 类

常见问题解答

「不安全代码、指针与固定缓冲区」课时是免费的吗?

是的 — 「不安全代码、指针与固定缓冲区」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C# Academy 课程的其余内容,请升级到 CoddyKit PRO。 C# Academy 课程共包含 4 节课。

「不安全代码、指针与固定缓冲区」这节课中我会学到什么?

使用 unsafe 关键字处理指针,使用 fixed 固定托管内存,并访问结构体中的固定大小缓冲区。 你通过在浏览器中直接运行的动手代码来练习 C# Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 C# Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 C# Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。

「不安全代码、指针与固定缓冲区」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 C# Academy 课中编写并运行代码吗?

能。每节 C# Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. P/Invoke 基础
  2. LibraryImport 与源代码生成的 P/Invoke
  3. 不安全代码、指针与固定缓冲区
  4. COM 互操作与运行时可调用包装器
← 返回 C# Academy