Pointers
Remarks#
Pointers and unsafe
Due to their nature, pointers produce unverifiable code. Thus, usage of any pointer type requires an unsafe
context.
The type System.IntPtr
is a safe wrapper around a void*
. It is intended as a more convenient alternative to void*
when an unsafe context isn’t otherwise required to perform the task at hand.
Undefined behavior
Like in C and C++, incorrect usage of pointers can invoke undefined behavior, with possible side-effects being memory corruption and execution of unintended code. Due to the unverifiable nature of most pointer operations, correct usage of pointers is entirely a responsibility of the programmer.
Types that support pointers
Unlike C and C++, not all C# types have corresponding pointer types. A type T
may have a corresponding pointer type if both of the following criteria apply:
T
is a struct type or a pointer type.T
contains only members that satisfy both of these criteria recursively.
Pointers for array access
This example demonstrates how pointers can be used for C-like access to C# arrays.
unsafe
{
var buffer = new int[1024];
fixed (int* p = &buffer[0])
{
for (var i = 0; i < buffer.Length; i++)
{
*(p + i) = i;
}
}
}
The unsafe
keyword is required because pointer access will not emit any bounds checks that are normally emitted when accessing C# arrays the regular way.
The fixed
keyword tells the C# compiler to emit instructions to pin the object in an exception-safe way. Pinning is required to ensure that the garbage collector will not move the array in memory, as that would invalidate any pointers pointing within the array.
Pointer arithmetic
Addition and subtraction in pointers works differently from integers. When a pointer is incremented or decremented, the address it points to is increased or decreased by the size of the referent type.
For example, the type int
(alias for System.Int32
) has a size of 4. If an int
can be stored in address 0, the subsequent int
can be stored in address 4, and so on. In code:
var ptr = (int*)IntPtr.Zero;
Console.WriteLine(new IntPtr(ptr)); // prints 0
ptr++;
Console.WriteLine(new IntPtr(ptr)); // prints 4
ptr++;
Console.WriteLine(new IntPtr(ptr)); // prints 8
Similarly, the type long
(alias for System.Int64
) has a size of 8. If a long
can be stored in address 0, the subsequent long
can be stored in address 8, and so on. In code:
var ptr = (long*)IntPtr.Zero;
Console.WriteLine(new IntPtr(ptr)); // prints 0
ptr++;
Console.WriteLine(new IntPtr(ptr)); // prints 8
ptr++;
Console.WriteLine(new IntPtr(ptr)); // prints 16
The type void
is special and void
pointers are also special and they are used as catch-all pointers when the type isn’t known or doesn’t matter. Due to their size-agnostic nature, void
pointers cannot be incremented or decremented:
var ptr = (void*)IntPtr.Zero;
Console.WriteLine(new IntPtr(ptr));
ptr++; // compile-time error
Console.WriteLine(new IntPtr(ptr));
ptr++; // compile-time error
Console.WriteLine(new IntPtr(ptr));
The asterisk is part of the type
In C and C++, the asterisk in the declaration of a pointer variable is part of the expression being declared. In C#, the asterisk in the declaration is part of the type.
In C, C++ and C#, the following snippet declares an int
pointer:
int* a;
In C and C++, the following snippet declares an int
pointer and an int
variable. In C#, it declares two int
pointers:
int* a, b;
In C and C++, the following snippet declares two int
pointers. In C#, it is invalid:
int *a, *b;
void*
C# inherits from C and C++ the usage of void*
as a type-agnostic and size-agnostic pointer.
void* ptr;
Any pointer type can be assigned to void*
using an implicit conversion:
int* p1 = (int*)IntPtr.Zero;
void* ptr = p1;
The reverse requires an explicit conversion:
int* p1 = (int*)IntPtr.Zero;
void* ptr = p1;
int* p2 = (int*)ptr;
Member access using ->
C# inherits from C and C++ the usage of the symbol ->
as a means of accessing the members of an instance through a typed pointer.
Consider the following struct:
struct Vector2
{
public int X;
public int Y;
}
This is an example of the usage of ->
to access its members:
Vector2 v;
v.X = 5;
v.Y = 10;
Vector2* ptr = &v;
int x = ptr->X;
int y = ptr->Y;
string s = ptr->ToString();
Console.WriteLine(x); // prints 5
Console.WriteLine(y); // prints 10
Console.WriteLine(s); // prints Vector2
Generic pointers
The criteria that a type must satisfy in order to support pointers (see Remarks) cannot be expressed in terms of generic constraints. Therefore, any attempt to declare a pointer to a type provided through a generic type parameter will fail.
void P<T>(T obj)
where T : struct
{
T* ptr = &obj; // compile-time error
}