Поняття покажчика (указатель)

Покажчики дозволяють отримати доступ до певної комірки пам'яті і зробити певні маніпуляції зі значенням, що зберігається в цій комірці.

У мові C # покажчики дуже рідко використовуються, проте в деяких випадках можна вдаватися до них для оптимізації додатків. Код, що застосовує покажчики, ще називають небезпечним кодом. Однак це не означає, що він представляє якусь небезпеку. Просто при роботі з ним всі дії по використанню пам'яті, в тому числі по її очищенню, лягають цілком на нас, а не на середовище CLR. І з точки зору CLR такий код не безпечний, тому що середовище не може перевірити даний код, тому підвищується ймовірність різного роду помилок.

CLR - «загальномовне виконуюче середовище» — це компонент пакету Microsoft .NET Framework, віртуальна машина, на якій виконуються всі мови платформи .NET Framework.

Щоб використовувати небезпечний код в C #, треба насамперед вказати проекту, що він буде працювати з небезпечним кодом. Для цього треба встановити в налаштуваннях проекту відповідний прапорець - в меню Project (Проект) знайти Властивості проекту. Потім в меню Build встановити прапорець Allow unsafe code (Дозволити небезпечний код):

 

Теперь ми можемо приступати до роботи з небезпечним кодом і індикаторами.

Ключове слово unsafe

Блок коду або метод, в якому використовуються покажчики, відмічається ключовим словом unsafe:

static void Main(string[] args)
{
     // блок коду, що використовує покажчики
     unsafe
     {
     
     }
}
Метод, що використовує покажчики:

unsafe private static void PointerMethod()
{
 
}
Так само з допомогою unsafe можна анонсувати структури:


unsafe struct State
{
 
}

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

static void Main(string[] args)
{
    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
    }
    Console.ReadLine();
}

При оголошенні покажчика вказуємо тип 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
uint addr = (uint) x;
Console.WriteLine ( "Адреса змінної y: {0}", addr);
Так як значення адреси - це ціле число, а на 32-розрядних системах діапазон адрес 0 до 4 000 000 000, то для отримання адреси використовується перетворення в тип uint, long або ulong. Відповідно на 64-розрядних системах діапазон доступних адрес набагато більше, тому в даному випадку краще використовувати ulong, щоб уникнути помилки переповнення.

Операції з покажчиками
Крім операції розіменування до покажчиків застосовні ще й деякі арифметичні операції (+, ++, -, -, + =, - =) і перетворення. Наприклад, ми можемо перетворити число в покажчик:

int * x; // визначення покажчика
int y = 10; // визначаємо змінну
x = & y; // покажчик x тепер вказує на адресу змінної y 
// отримаємо адреса змінної y
uint addr = (uint) x;
Console.WriteLine ( "Адреса змінної y: {0}", addr); 
byte * bytePointer = (byte *) (addr + 4); // отримати покажчик на наступний байт після addr
Console.WriteLine ( "Значення byte за адресою {0}: {1}", addr + 4, * bytePointer); 
// зворотна операція
uint oldAddr = (uint) bytePointer - 4; // віднімаємо чотири байти, так як bytePointer - покажчик на байт
int * intPointer = (int *) oldAddr;
Console.WriteLine ( "Значення int за адресою {0}: {1}", oldAddr, * intPointer); 
// перетворення в тип double
double * doublePointer = (double *) (addr + 4);
Console.WriteLine ( "Значення double за адресою {0}: {1}", addr + 4, * doublePointer);

Оскільки у нас x - покажчик на об'єкт int, який займає 4 байта, то ми можемо отримати наступний за ним байт за допомогою виразу byte * chp = (byte *) addr + 4 ;. Тепер покажчик bytePointer вказує на наступний байт. Так само ми можемо створити і інший покажчик double * doublePointer = (double *) addr + 4 ;, тільки цей покажчик вже буде вказувати на наступні 8 байт, так як тип double займає 8 байт.

Щоб назад отримати вихідну адресу, викликаємо вираз bytePointer - 4. Тут bytePointer - це покажчик, а не число, і операції віднімання та додавання відбуватимуться відповідно до правил арифметики покажчиків. наприклад:

char* charPointer = (char*)123400;

charPointer += 4; // 123408

Console.WriteLine("Адрес {0}", (uint)charPointer);

Хоча ми до покажчика додаємо число 4, але підсумкова адреса збільшиться на 8, оскільки розмір об'єкта char - 2 байта, а 2 * 4 = 8. Подібним чином діє складання з іншими типу покажчиків:

double* doublePointer = (double*)123000;

doublePointer = doublePointer+3; // 123024

Console.WriteLine("Адрес {0}", (uint)doublePointer);

Аналогічно працює віднімання: doublePointer - = 2 встановить в покажчику doublePointer в якості адреси число 123008.

Покажчики на типи і операція ->

Крім покажчиків на прості типи можна використовувати покажчики на структури. А для доступу до полів структури, на яку вказує покажчик, використовується операція ->:

class Program
{
    static void Main(string[] args)
    {
        unsafe
        {
            Person person;
            person.age = 29;
            person.height = 176;
            Person* p = &person;
            p->age = 30;
            Console.WriteLine(p->age);
 
            // розіменування покажчика
            (*p).height = 180;
            Console.WriteLine((*p).height);
        }
    }
}
 
public struct Person
{
    public int age;
    public int height;
}

Звертаючись до покажчика p-> age = 30; ми можемо отримати або встановити значення властивості структури, на яку вказує покажчик. Зверніть увагу, що просто написати p.age = 30 ми не можемо, оскільки p - це не структура Person, а покажчик на структуру.

Альтернативою служить операція розіменування: (* p) .height = 180;

Покажчики на масиви і stackalloc
За допомогою ключового слова stackalloc можна виділити пам'ять під масив в стеці. Сенс виділення пам'яті в стеку в підвищенні швидкодії коду. Подивимося на прикладі обчислення факторіала:

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 = 1; i <= size; ++ i)
     {
         Console.WriteLine (factorial [i-1]);
     }
}

Оператор stackalloc приймає після себе масив, на який буде вказувати покажчик. int * factorial = stackalloc int [size];.

Для маніпуляцій з масивом створюємо покажчик p: int * p = factorial;, який вказує на перший елемент масиву, в якому всього 7 елементів

Далі починаються вже самі операції з покажчиком і підрахунок факторіала. Так як факторіал 1 дорівнює 1, то присвоюємо першому елементу, на який вказує покажчик p, одиницю за допомогою операції розіменування: * (p ++) = 1;