Unsafe Code and Pointers in C#
Learn unsafe code and pointers in C# to work with memory addresses, low-level operations, and advanced performance scenarios.
Although the C# language provides safe memory management, in certain high-performance scenarios,
direct access to memory may be necessary. Unsafe code is used in such cases.
Unsafe blocks allow direct access to memory addresses using pointers.
While this approach provides low-level control, it must be used carefully, as it operates outside the .NET Garbage Collector (GC)’s protection.
What is Unsafe Code?
The unsafe keyword tells the C# compiler that the code works with unmanaged memory. In such code, the compiler does not perform memory safety checks. To use unsafe code, the project must have the “Allow unsafe code” option enabled in the build settings.
// Property to add to your .csproj file:
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
What is a Pointer?
A pointer is a variable that stores the memory address of another variable.
In C#, pointers can only be declared within unsafe blocks and are used on the stack.
The * symbol declares a pointer, and the & operator retrieves a variable’s address.
using System;
class Program
{
static unsafe void Main()
{
int x = 42;
int* ptr = &x; // get the memory address of x
Console.WriteLine($"Value of x: {*ptr}");
Console.WriteLine($"Address of x: {(ulong)ptr}");
}
}
The expression *ptr means “read the value at the pointed address.”
In this example, both the variable’s address and its value are accessed.
Modifying Values via Pointers
You can directly modify a variable’s value through a pointer. This works exactly like pointer manipulation in C or C++.
unsafe
{
int x = 10;
int* p = &x;
*p = 99; // change x's value via pointer
Console.WriteLine($"New value of x: {x}");
}
// Output:
// New value of x: 99
Pointer Arithmetic
Pointer arithmetic allows navigating through arrays by incrementing pointer addresses.
However, it is only allowed for primitive types such as int, byte, and double.
unsafe
{
int[] numbers = { 10, 20, 30, 40 };
fixed (int* p = numbers)
{
for (int i = 0; i < numbers.Length; i++)
Console.WriteLine($"Address: {(ulong)(p + i)}, Value: {*(p + i)}");
}
}
The fixed keyword pins the array in memory so the GC cannot move it.
This ensures that the pointer always references a valid address.
The fixed Keyword
fixed prevents a managed object (such as an array or string) from being relocated by the GC.
It allows safe access to memory addresses via pointers.
unsafe
{
string message = "Hello";
fixed (char* p = message)
{
for (int i = 0; i < message.Length; i++)
Console.Write(p[i]);
}
}
Note: When the fixed block ends, the object can be moved again,
so using the pointer outside this block is unsafe.
Accessing Memory with struct and Pointer
You can also access memory directly for structs using pointers. This is often used in performance-critical scenarios (e.g., game engines, graphics processing, or native interop).
using System;
struct Point
{
public int X;
public int Y;
}
class Program
{
static unsafe void Main()
{
Point p = new Point { X = 5, Y = 10 };
Point* ptr = &p;
ptr->X = 20;
Console.WriteLine($"Point: X={ptr->X}, Y={ptr->Y}");
}
}
The ptr->X syntax is the same as struct pointer access in C/C++.
Allocating Memory on the Stack with stackalloc
stackalloc allocates raw memory directly on the stack.
This memory is automatically released at the end of the scope.
unsafe
{
int* p = stackalloc int[5];
for (int i = 0; i < 5; i++)
p[i] = i * 2;
for (int i = 0; i < 5; i++)
Console.WriteLine(p[i]);
}
stackalloc is fast but should only be used for small data sizes.
For large data, prefer heap allocation (new).
When to Use Unsafe Code
- In performance-critical scenarios (e.g., mathematical computation, game engines, image processing)
- When interacting with unmanaged C/C++ code (P/Invoke, Interop)
- When direct memory manipulation of large arrays or byte streams is required
- In fixed-size buffer structures or custom memory layout scenarios
Performance Comparison
Unsafe code with direct memory access can offer a performance gain of 30–50% by bypassing the GC. However, this comes at the cost of increased risk. Accessing invalid memory addresses can cause crashes or memory corruption.
Important Considerations
- Accessing a null pointer or invalid address throws an
AccessViolationException. - Do not use pointers outside the fixed block (the GC might move the object).
- Unsafe code is considered unmanaged from a security standpoint and must be reviewed carefully.
- The entire application’s security model should be reassessed when using unsafe code.
- Unsafe code does not run under partial trust environments.
Example: Accessing Image Pixel Data
The following example demonstrates how to directly access pixels in a bitmap using an unsafe block. This method performs pixel manipulation (e.g., color inversion) much faster than traditional APIs.
using System;
using System.Drawing;
using System.Drawing.Imaging;
class Program
{
static unsafe void Main()
{
using Bitmap bmp = new Bitmap("image.png");
Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
BitmapData data = bmp.LockBits(rect, ImageLockMode.ReadWrite, bmp.PixelFormat);
int bytesPerPixel = Image.GetPixelFormatSize(bmp.PixelFormat) / 8;
byte* firstPixel = (byte*)data.Scan0;
for (int y = 0; y < bmp.Height; y++)
{
byte* row = firstPixel + (y * data.Stride);
for (int x = 0; x < bmp.Width; x++)
{
byte* pixel = row + (x * bytesPerPixel);
pixel[0] = (byte)(255 - pixel[0]); // invert blue
pixel[1] = (byte)(255 - pixel[1]); // invert green
pixel[2] = (byte)(255 - pixel[2]); // invert red
}
}
bmp.UnlockBits(data);
bmp.Save("output.png");
Console.WriteLine("Image colors have been inverted!");
}
}
This example demonstrates the power of unsafe code for high-performance pixel manipulation operations.
TL;DR
- unsafe blocks in C# allow direct memory access via pointers.
- The fixed keyword prevents the GC from moving objects in memory.
- stackalloc enables fast, temporary memory allocation on the stack.
- It offers performance gains but comes with significant security risks.
- Commonly used for interaction with unmanaged (C/C++) code or low-level memory manipulation.
Related Articles
C# Basic Data Types
Basic data types in C#: numeric, text, logical, object-based, and nullable types.
Memory Management and the Garbage Collector in C#
Learn memory management and the garbage collector in C# to understand object lifetime, allocation, and cleanup processes.
Performance Optimization with Span<T> and Memory<T> in C#
Learn performance optimization in C# using Span<T> and Memory<T> to manage memory efficiently and process data faster.
Structs in C# – Differences from Classes
Learn the key differences between structs and classes in C#, including memory model, inheritance, boxing, and performance.