Указатели на структуры, члены классов и массивы
Указатели на типы и операция ->
Кроме указателей на простые типы можно использовать указатели на структуры. А для доступа к полям структуры, на которую указывает указатель, используется операция ->:
unsafe
{
Point point = new Point(0, 0);
Console.WriteLine(point); // X: 0 Y: 0
Point* p = &point;
p->X = 30;
Console.WriteLine(p->X); // 30
// разыменовывание указателя
(*p).Y = 180;
Console.WriteLine((*p).Y); // 180
Console.WriteLine(point); // X: 30 Y: 180
}
struct Point
{
public int X { get; set; }
public int Y { get; set; }
public Point(int x, int y)
{
X = x; Y = y;
}
public override string ToString() => $"X: {X} Y: {Y}";
}
Обращаясь к указателю p->X = 30; мы можем получить или установить значение свойства структуры, на которую указывает указатель. Обратите внимание, что просто написать p.X=30 мы не можем, так как p - это не структура Point, а указатель на структуру.
Альтернативой служит операция разыменования: (*p).X = 30;
Стоит отметить, что указатель может указывать только на те структуры, которые не имеют полей ссылочных типов (в том числе полей, которые генерируются компилятором автоматически для автосвойств).
Указатели на массивы и stackalloc
С помощью ключевого слова stackalloc можно выделить память под массив в стеке. Смысл выделения памяти в стеке в повышении быстродействия кода. Посмотрим на примере вычисления квадратов чисел:
unsafe
{
const int size = 7;
int* square = stackalloc int[size]; // выделяем память в стеке под семь объектов int
int* p = square;
// вычисляем квадраты чисел от 1 до 7 включая
for (int i = 1; i <= size; i++, p++)
{
// считаем квадрат числа
*p = i * i;
}
for (int i = 0; i < size; i++)
{
Console.WriteLine(square[i]);
}
}
Оператор stackalloc принимает после себя массив, на который будет указывать указатель. int* square = stackalloc int[size];.
Для манипуляций с массивом создаем указатель p: int* p = square;, который указывает на первый элемент массива, в котором всего 7 элементов. То есть с помощью указателя p мы сможем перемещаться по массиву square.
Далее в цикле происходит подсчет квадратов чисел от 1 до 7. В цикле для установки значения (квадрата числа - i * i) по адресу, который хранит указатель, выполняется выражение:
*p = i * i;
Затем происходит инкремент указателя p++, и указатель p смещается вперед на следующий элемент в массиве square.
Чуть более сложный пример - вычисление факториала:
unsafe
{
const int size = 7;
int* factorial = stackalloc int[size]; // выделяем память в стеке под семь объектов int
int* p = factorial;
*(p++) = 1; // присваиваем первой ячейке значение 1 и
// увеличиваем указатель на 1
for (int i = 2; i <= size; i++, p++)
{
// считаем факториал числа
*p = p[-1] * i;
}
for (int i = 0; i < size; i++)
{
Console.WriteLine(factorial[i]);
}
}
Также с помощью оператора stackalloc выделяется память для 7 элементов массива. И также для манипуляций с массивом создаем указатель p: int* p = factorial;, который указывает на первый элемент массива, в котором всего 7 элементов
Далее начинаются уже сами операции с указателем и подсчет факториала. Так как факториал 1 равен 1, то присваиваем первому элементу, на который указывает указатель p, единицу с помощью операции разыменования: *(p++)= 1;
Для установки некоторого значения по адресу указателя применяется выражение: *p=1. Но кроме этого тут происходит также инкремент указателя p++. То есть сначала первому элементу массива присваивается единица, потом указатель p смещается и начинает указывать уже на второй элемент. Мы могли бы написать это так:
*p= 1;
p++;
Чтобы получить предыдущий элемент и сместиться назад, можно использовать операцию декремента: Console.WriteLine(*(--p));. Обратите внимание, что операции *(--p) и *(p--) различаются, так как в первом случае сначала идет смещение указателя, а затем его разыменовывание. А во втором случае - наоборот.
Затем вычисляем факториал всех остальных шести чисел: *p = p[-1] *i;. Обращение к указателям как к массивам представляет альтернативу операции разыменовывания для получения значения. В данном случае мы получаем значение предыдущего элемента.
И в заключении, используя указатель factorial, выводим факториалы всех семи чисел.
Оператор fixed и закрепление указателей
Ранее мы посмотрели, как создавать указатели на типы значений, например, int или структуры. Однако кроме структур в C# есть еще и классы, которые в отличие от типов значений, помещают все связанные значения в куче. И в работу данных классов может в любой момент вмешаться сборщик мусора, периодически очищающий кучу. Чтобы фиксировать на все время работы указатели на объекты классов используется оператор fixed.
Допустим, у нас есть класс Person:
class Point
{
public int x;
public int y;
public override string ToString() => $"x: {x} y: {y}";
}
Зафиксируем указатель с помощью оператора fixed:
unsafe
{
Point point = new Point();
// блок фиксации указателя
fixed (int* pX = &point.x)
{
*pX = 30;
}
fixed (int* pY = &point.y)
{
*pY = 150;
}
// можно совместить оба блока
/*fixed (int* pX = &point.x, pY = &point.y)
{
*pX = 30;
*pY = 150;
}*/
Console.WriteLine(point); // x: 30 y: 150
}
Оператор fixed создает блок, в котором фиксируется указатель на поле объекта person. После завершения блока fixed закрепление с переменных снимается, и они могут быть подвержены сборке мусора.
Кроме адреса переменной можно также инициализировать указатель, используя массив, строку или буфер фиксированного размера:
unsafe
{
int[] nums = { 0, 1, 2, 3, 7, 88 };
string str = "Привет мир";
fixed(int* p = nums)
{
int third = *(p+2); // получим третий элемент
Console.WriteLine(third); // 2
}
fixed(char* p = str)
{
char forth = *(p + 3); // получим четвертый элемент
Console.WriteLine(forth); // в
}
}
При инициализации указателей на строку следует учитывать, что указатель должен иметь тип char*.