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.