Поняття покажчика (указатель)
Покажчики дозволяють отримати доступ до певної комірки пам'яті і зробити певні маніпуляції зі значенням, що зберігається в цій комірці.
У мові 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;