Указатели


Если вы программировали на С/С++, то, возможно, вы знакомы с таким понятием как указатели. Указатели позволяют получить доступ к определенной ячейке памяти и произвести определенные манипуляции со значением, хранящимся в этой ячейке.

В языке C# указатели редко используются, однако в некоторых случаях можно прибегать к ним для оптимизации приложений. Код, применяющий указатели, еще называют небезопасным (unsafe) кодом. Однако это не значит, что он представляет какую-то опасность. Просто при работе с ним все действия по использованию памяти, в том числе по ее очистке, ложится целиком на нас, а не на среду CLR. И с точки зрения CLR такой код не безопасен, так как среда не может проверить данный код, поэтому повышается вероятность различного рода ошибок.

Чтобы использовать небезопасный код в C#, надо первым делом указать проекту, что он будет работать с небезопасным кодом. Для этого надо установить в настройках проекта соответствующий флаг - в меню Project (Проект) найти Свойства проекта. Затем в меню Build установить флажок Unsafe code (Небезопасный код):

установка флага unsafe в c#
Теперь мы можем приступать к работе с небезопасным кодом и указателями.

Ключевое слово unsafe
Блок кода или метод, в котором используются указатели, помечается ключевым словом unsafe:

// блок кода, использующий указатели
unsafe
{

}
Метод, использующий указатели:

unsafe void Test()
{

}
Также с помощью unsafe можно объявлять структуры и классы:

unsafe struct State
{

}

unsafe class Person
{

}
Операции * и &
Ключевой при работе с указателями является операция *, которую еще называют операцией разыменовывания. Операция разыменовывания позволяет получить или установить значение по адресу, на который указывает указатель. Для получения адреса переменной применяется операция &:

unsafe
{
int* x; // определение указателя
int y = 10; // определяем переменную

x = &y; // указатель x теперь указывает на адрес переменной y
Console.WriteLine(*x); // 10

y = y + 20; // меняем значение
Console.WriteLine(*x);// 30

*x = 50;
Console.WriteLine(y); // переменная y=50
}
При объявлении указателя указываем тип int* x; - в данном случае объявляется указатель на целое число. Но кроме типа int можно использовать и другие: sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal или bool. Также можно объявлять указатели на типы enum, структуры и другие указатели.

Выражение x = &y; позволяет нам получить адрес переменной y и установить на него указатель x. До этого указатель x не на что не указывал.

После этого все операции с y будут влиять на значение, получаемое через указатель x и наоборот, так как они указывают на одну и ту же область в памяти.

Для получения значения, которое хранится в области памяти, на которую указывает указатель x, используется выражение *x.

Получение адреса
Используя преобразование указателя к целочисленному типу, можно получить адрес памяти, на который указывает указатель:

int* x; // определение указателя
int y = 10; // определяем переменную

x = &y; // указатель x теперь указывает на адрес переменной y

// получим адрес переменной y
ulong addr = (ulong)x;
Console.WriteLine($"Адрес переменной y: {addr}");
Для получения адреса используется преобразование в тип uint, long или ulong. Так как значение адреса - это целое число, а на 32-разрядных системах диапазон адресов 0 до 4 000 000 000, а адрес можно получить в переменную uint/int. Соответственно на 64-разрядных системах диапазон доступных адресов гораздо больше, поэтому в данном случае лучше использовать ulong, чтобы избежать ошибки переполнения.

Указатель на другой указатель
Объявление и использование указателя на указатель:

unsafe
{
int* x; // определение указателя
int y = 10; // определяем переменную

x = &y; // указатель x теперь указывает на адрес переменной y
int** z = &x; // указатель z теперь указывает на адрес, указателя x
**z = **z + 40; // изменение указателя z повлечет изменение переменной y
Console.WriteLine(y); // переменная y=50
Console.WriteLine(**z); // переменная **z=50
}