In this article, we’ll introduce pointers in C#, exploring their syntax, usage in unsafe code blocks, and practical applications with direct memory manipulation. We’ll also discuss considerations involved in using pointers in a managed language.
Let’s start!
What Is a Pointer in C#?
A pointer is simply a variable that holds the memory address of another variable. This allows direct access to that memory location.
By default, in the .NET environment, the Garbage Collector (or GC) handles memory management, to prevent issues such as memory leaks, invalid memory access, or data corruption. Pointers allow direct access manipulation of memory, bypassing the safety features provided by .NET runtime. This is the reason why the utilization of pointers in C# must be inside the unsafe
context.
Getting Started With Pointers in C#
To begin with, let’s create a new console app:
dotnet new console -n App
Next, let’s add the following line to the .csproj
file:
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
This allows our project to use the unsafe
context:
unsafe { int* p; }
Here we declare a pointer p
to an integer
inside an unsafe
block. The *
indicates that the variable p
is a pointer to a type. A pointer is an untracked reference, meaning that the GC does not account for pointers and they do not provide safety against GC operations like object relocation.
We must declare the pointer type to specify what data it is expected to point to. In C#, pointers can only point to unmanaged types, such as basic data types, enums, etc. Pointers cannot point to reference types like class, string, object, etc.
Example With Pointer to Int Variable
Let’s see a simple demonstration of how we can create a pointer to an int
variable:
unsafe { var num = 7; int* p = # Console.WriteLine($"Original variable value: {num}"); Console.WriteLine($"Pointer points to value: {*p}"); Console.WriteLine($"Pointer address: {(ulong)p:X}"); }
First, we declare an int
variable num
with a sample value. Then we declare and initialize a pointer to the num
variable with the unary &
operator, which returns the address of its operand. The pointer p
now holds the memory address of num
variable, which we can display:
Original variable value: 7 Pointer points to value: 7 Pointer address: E2D7F7ED1C
As we expect, the first two values are equal. We extract the raw memory address of the pointer with the expression (ulong)p
. This casts the pointer’s address to a 64-bit unsigned integer
from its native form. We use ulong
type instead of int
type in the case of a 64-bit system.
Use Pointers as Method Arguments
Now, we will work with multiple pointers and alter their values.
Let’s create an example with two char
variables and their respective pointers:
var char1 = 'A'; var char2 = 'B'; char* p1 = &char1; char* p2 = &char2;
Similarly to the previous example, we initialize two char
variables and assign them to two pointers. We will create a method that swaps the pointers’ values:
public static unsafe void SwapChars(char* p1, char* p2) => (*p2, *p1) = (*p1, *p2);
We must mark the method as unsafe
because we’re using pointers. This allows direct memory manipulation inside its scope.
The arguments are two pointers to char
variables. We swap the values using two ValueTuples.
Now let’s use the above method:
Console.WriteLine($"Value of char1: {char1}"); Console.WriteLine($"Value of char2: {char2}"); SwapChars(p1,p2); Console.WriteLine($"After swapping:"); Console.WriteLine($"Value of char1: {char1}"); Console.WriteLine($"Value of char2: {char2}");
Here we print the variables before and after their values change. Let’s inspect the results:
Value of char1: A Value of char2: B After swapping: Value of char1: B Value of char2: A
We can see that we have successfully swapped the variables’ data, verifying that we can change the values of variables directly through pointers to them.
Use of Fixed Statement in Unsafe Context
A keyword that is often useful when coding with pointers is the fixed
keyword. We use the fixed
keyword to pin an object in memory, which prevents the GC from relocating it. We can only use the fixed
keyword within an unsafe
context:
public static unsafe void DoubleOddValues(int[] numbers) { fixed (int* arrayPtr = numbers) { for (var i = 0; i < numbers.Length; i++) { if (*(arrayPtr + i) % 2 != 0) *(arrayPtr + i) *= 2; } } }
The DoubleOddValues()
method takes an int[]
array numbers
, and doubles any odd value within the array. Since we are using both the fixed
keyword along with pointers, we must declare the method as unsafe
.
First, we assign the int*
arrayPtr
to the first element of the numbers
. We must enclose this assignment inside a fixed
block.
We use the fixed statement to pin the memory location of the numbers
. This statement prevents the GC from moving the array in memory while we are working with pointers to its elements. If the array were moved to another memory location while we are working with a pointer to it, that pointer would become invalid which would lead to undefined behavior.
By pinning the location in memory we prevent the GC from moving the object. Because of this, we should ensure that we do not pin any objects in memory longer than necessary, as this can impact GC performance.
Next, we initiate a for
loop
to iterate through the collection. Since an array
is a collection stored in a contiguous block of memory, the expression *(arrayPtr + i)
is offsetting the pointer to point to the i
th element of the array.
With our offset pointer we check the value of the array location to see whether it is odd via the modulus operater, and if so we double its value.
Now, let’s see it in action:
var intArray = new[] {1, 2, 3, 4, 5}; Console.WriteLine("Before doubling odd values:"); Console.WriteLine(string.Join(", ", intArray)); Methods.DoubleOddValues(intArray); Console.WriteLine("\nAfter doubling odd values:"); Console.WriteLine(string.Join(", ", intArray));
Here, we initialize a simple int
array with five positive integers, and then pass it into our new method DoubleOddValues()
:
Before doubling odd values: 1, 2, 3, 4, 5 After doubling odd values: 2, 2, 6, 4, 10
Use Cases of Pointers and Risks
Generally, pointers are rarely used when coding in .NET. However there are cases when their utilization can be useful. For example, we might interface with unmanaged functions (e.g. from a C library) and we need to pass a data structure to the unmanaged code. This is common in scenarios that include cryptography or hardware interaction tasks.
Moreover, direct interaction with memory can offer optimization benefits. For example, pointers can provide faster access to texture or pixel data in performance-critical applications such as game development or image processing involving large arrays.
Undoubtedly, the introduction of pointers comes with significant risks. Incorrect memory access outside allocated bounds can result in buffer overflows, potentially corrupting memory and leading to application instability. Also, in cases where we prevent the Garbage Collector from managing objects in memory, it can lead to fragmentation of the managed memory heap. Lastly, the code becomes more difficult to understand without experience in unmanaged techniques.
Conclusion
In this article, we introduced the concept of pointers in C#. We presented their syntax, explored direct memory access to unmanaged objects, and considered their risks and benefits.