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.

To download the source code for this article, you can visit our GitHub repository.

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.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

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 = &num;
  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 ith 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.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!