Кроме указателей на простые типы можно использовать указатели на структуры. А для доступа к полям структуры, на которую указывает указатель, используется операция ->:
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 можно выделить память под массив в стеке. Смысл выделения памяти в стеке в повышении быстродействия кода. Посмотрим на примере вычисления квадратов чисел:
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, выводим факториалы всех семи чисел.
Ранее мы посмотрели, как создавать указатели на типы значений, например, int или структуры. Однако кроме структур в C# есть еще и классы,
которые в отличие от типов значений, помещают все связанные значения в куче. И в работу данных классов может в любой момент вмешаться сборщик мусора,
периодически очищающий кучу. Чтобы фиксировать на все время работы указатели на объекты классов используется оператор fixed
.
Допустим, у нас есть класс Point:
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*
.