Unsafe Code and Pointers in C#

Vaibhav • September 11, 2025

Up until now, everything we’ve written in C# has been safe - meaning the compiler and runtime automatically manage memory, prevent buffer overflows, and protect you from accessing invalid memory. This safety is one of the reasons C# is such a reliable and beginner-friendly language. But sometimes, especially in performance-critical scenarios like graphics, game engines, or interop with native code, you may need to bypass these protections and work directly with memory. That’s where unsafe code and pointers come in.

In this article, we’ll explore what unsafe code is, how pointers work in C#, when and why you might use them, and how to do so responsibly. We’ll build on your understanding of value types, arrays, and memory layout - and introduce new syntax and concepts that give you low-level control, while still staying within the C# ecosystem.

What Is Unsafe Code?

Unsafe code is a special mode in C# that allows you to use pointers and perform operations that the compiler normally disallows. It’s called “unsafe” because it bypasses the safety checks of the .NET runtime - meaning you can access memory directly, manipulate addresses, and potentially cause crashes or security issues if used incorrectly.

To write unsafe code, you must:

  • Use the unsafe keyword
  • Enable unsafe compilation in your project settings
  • Understand how memory works at a low level

Here’s a simple example:

unsafe
{
    int x = 10;
    int* p = &x;
    Console.WriteLine(*p); // Output: 10
}

This code declares a pointer to an integer, assigns it the address of x, and then dereferences the pointer to read the value. This is similar to how pointers work in C or C++, but with C# syntax and restrictions.

Unsafe code is not enabled by default. You must explicitly allow it in your project settings or use the /unsafe compiler option.

Declaring and Using Pointers

A pointer is a variable that holds the memory address of another variable. In C#, you declare a pointer using the * symbol:

int* ptr; // pointer to an int
char* cptr; // pointer to a char

You can assign a pointer using the address-of operator &:

int number = 42;
int* p = &number;

To read or write the value at the pointer’s address, use the dereference operator *:

*p = 100;
Console.WriteLine(number); // Output: 100

This modifies the original variable through the pointer. You’re working directly with memory - which is powerful, but also risky.

Pointer Arithmetic

C# supports limited pointer arithmetic - you can increment or decrement pointers, or add/subtract integers to move across memory:

int[] arr = { 10, 20, 30 };

unsafe
{
    fixed (int* p = arr)
    {
        Console.WriteLine(*(p + 1)); // Output: 20
    }
}

The fixed statement pins the array in memory so the garbage collector doesn’t move it. Then we use pointer arithmetic to access the second element. This is useful for high-performance scenarios like image processing or custom memory layouts.

Note: Pointer arithmetic is only allowed in unsafe contexts and only with unmanaged types (like int, float, char).

Fixed Keyword and Pinning Memory

In managed environments like .NET, the garbage collector can move objects around in memory. This is great for performance - but dangerous if you’re using pointers. To prevent this, you use the fixed keyword to pin an object in memory:

byte[] buffer = new byte[100];

unsafe
{
    fixed (byte* ptr = buffer)
    {
        ptr[0] = 255;
    }
}

This ensures that buffer stays in the same memory location while you’re using the pointer. Without fixed, the garbage collector might move it - causing your pointer to point to invalid memory.

Working with Structs and Pointers

You can use pointers with structs to access fields directly. This is useful for performance-critical code where you want to avoid copying or boxing:

struct Point
{
    public int X;
    public int Y;
}

unsafe
{
    Point p = new Point { X = 1, Y = 2 };
    Point* ptr = &p;
    ptr->X = 10;
    Console.WriteLine(p.X); // Output: 10
}

The -> operator lets you access fields through a pointer. This is similar to dereferencing and accessing members in C++.

Stackalloc for Stack-Based Memory

C# provides a special keyword stackalloc to allocate memory on the stack - instead of the heap. This is fast and temporary, ideal for short-lived buffers:

unsafe
{
    int* buffer = stackalloc int[5];
    buffer[0] = 42;
    Console.WriteLine(buffer[0]); // Output: 42
}

Stack-allocated memory is automatically freed when the method returns. You don’t need to worry about garbage collection - but you must use it carefully to avoid stack overflows.

Span<T> and stackalloc are often used together in modern C# to write high-performance, safe code without using unsafe pointers.

Interop with Native Code

Unsafe code is often used when interacting with native libraries - such as C or C++ DLLs. You can pass pointers to unmanaged functions using DllImport and unsafe blocks:

[DllImport("native.dll")]
public static extern void ProcessData(byte* data, int length);

unsafe
{
    byte[] buffer = new byte[100];
    fixed (byte* ptr = buffer)
    {
        ProcessData(ptr, buffer.Length);
    }
}

This lets you call native functions and pass raw memory buffers - essential for graphics, audio, or hardware integration.

When to Use Unsafe Code

Unsafe code is not for everyday use. Most C# programs never need it. But it’s valuable when:

  • You need maximum performance
  • You’re working with hardware or native libraries
  • You’re building custom memory structures
  • You’re writing low-level frameworks or game engines

Even then, use it sparingly and isolate it from the rest of your code. Unsafe code is harder to debug, test, and maintain - so treat it like a sharp tool: powerful, but dangerous.

Wrap unsafe code in small, well-tested methods. Keep the rest of your code safe and managed. Use Span<T> and Memory<T> when possible - they offer many of the same benefits without the risks.

Common Mistakes and How to Avoid Them

Unsafe code can lead to serious bugs if misused. Here are some common mistakes:

  • Dereferencing null or invalid pointers - causes crashes
  • Accessing memory out of bounds - leads to corruption
  • Forgetting to pin memory - causes garbage collector issues
  • Using unsafe code when safe alternatives exist - adds unnecessary risk

Always validate pointers, use fixed when needed, and prefer safe constructs unless performance demands otherwise.

Summary

Unsafe code and pointers give you low-level control over memory in C#. You’ve learned how to declare pointers, use unsafe blocks, perform pointer arithmetic, pin memory with fixed, allocate stack memory with stackalloc, and interact with native code. You’ve also seen when and why to use unsafe code - and how to do so responsibly.

By mastering unsafe code, you unlock a powerful tool for performance-critical scenarios. But with great power comes great responsibility - so use it wisely, test thoroughly, and keep the rest of your code safe. In the next article, we’ll explore Dynamic Types - a feature that lets you write flexible, runtime-bound code using the dynamic keyword.