
Лекція 8. Вказівники
Розуміння і правильне використання вказівників дуже важливо для створення гарних програм на мові С. Вказівники необхідні для успішнго використання функцій і динамічного розподілу пам'яті. Крім того, багато конструкцій мови C++ вимагають застосування вказівників. Однак із вказівниками варто працювати обережно. Використання в програмі неіціалізованого, або "дикого" (wild), вказівника може привести до "зависання" комп'ютера. При неакуратному використанні вказівників у програмі може виникнути помилка, яку дуже важко знайти. Але й обійтися без вказівників у програмах на мові С не можна.
Оголошення вказівників
Вказівник - це змінна, що містить адресу деякого об'єкта. Тут мається на увазі адреса в пам'яті комп'ютера. Взагалі це просто ціле число. Але не можна трактувати покажчик як змінну або константу цілого типу. Якщо перемінна буде вказівнииком, то вона повинна б відповідним чином бути оголошена. Вказівник оголошується в такий спосіб:
тип *<ім'я змінної>;
У цьому оголошенні тип - деякий тип мови С, що визначає тип об'єкта, на який вказує вказівник (адресу якого містить); * - означає, що наступна за ном змінна є вказівником.
Наприклад:
char *ch;
int *temp, i, *j;
float *pf, f;
Тут оголошені покажчики ch, temp, j, pf, змінна i типу int і змінна f типу float.
Операції над вказівниками
З покажчиками зв'язані дві спеціальні операції: & і *.
Обидві ці операції є унарними, тобто мають один операнд, перед якими вони ставляться. Операція & відповідає операції "взяти адресу". Операція * відповідає словам "значення, розташоване по зазначеній адресі".
Особливість мови С перебуває в тому, що знак * відповідає двом операціям, що не мають одна до іншої ніякого відношення: арифметичної операції множення й операції взяти значення. У той же час сплутати їх у контексті програми не можна, тому що одна з операцій унарна (містить один операнд), інша - множення - бінарна (містить два операнда). Унарні операції & і * мають найвищий пріоритет.
У оголошенні зміннної, що є вказівником, дуже важливий базовий тип. Звідкіля компілятор знає, скільки байтів пам'яті займає перемінна, на якій вказує даний покажчик? Відповідь проста: із базового типу вказівника. Якщо вказівник має базовий тип int, то змінна займає 2 байти, char - 1 байт і т.д.
Найпростіші дії з покажчиками ілюструються наступною програмою:
# include <stdio.h>
/* Приклад 27. */
/* Робота з покажчиками */
main()
{
float x=10.1, у;
float *pf;
pf=&x;
y=*pf;
printf(“x=%f у=%f”, х, у);
*pf++;
printf(“x-%f у=%f”, х, у);
y=l+*pf*y;
printf(“x=%f у=%f”, х, у);
return 0;
}
До вказівників можна застосувати операцію присвоювання. Вказівники того самого типу можуть використовуватися в операції присвоювання, як і будь-які інші змінні.
Розглянемо приклад.
# include <stdio.h>
/* Приклад 28. */
main()
{
int x=10;
int *p, *g;
p=&x;
g=p;
printf("%p", p); /* друк вмісту р */
printf("%p", g); /* друк вмісту g */
printf("%d %d", х, *g); /* друк розміру х і розміри за адресою g*/
}
У цьому прикладі приведена ще одна специфікація формату функції printf() %р.Цей формат використовується для друку адреси пам'яті в шістнадцятковій формі.
Не можна створити змінну типу void, але можна створити вказівник на тип void. Вказівнику на void можна привласнити вказівник будь-якого іншого типу. Однак при зворотному присвоюванні необхідно використовувати явне перетворення вказівника на void;
void *pv;
float f, *pf;
pf=&f;
pv=pf;
pf=(float*)pv;
У мові С припустимо привласнюється вказівнику будь-якої адреси пам'яті. Однак, якщо оголошений покажчик на ціле
int *pi;
а за адресою, що привласнений даному вказівнику, знаходиться змінна х типу float, то при компіляції програми буде видане повідомлення про помилку в рядку
р=&х;
Цю помилку можна виправити перетворюючи вказівник на int до типу вказівника на float явним перетворенням типу:
p=(inf*)&x;
Але при цьому втрачається інформація про те, на який тип вказував вихідний покажчик:
# include <stdio.h>
/* Приклад 29. */
/* Неправильні дії з вказівниками */
main()
{
float х=10.1, у;
int *p; р=&х; /* Потім замінимо на p=(int*)&x; */
y=*p;
printf("x=%f y%f \n, x, у);
}
У результаті роботи цієї програми не буде отримана та відповідь, що очікувалася. Змінній у не буде привласнене значення змінній х, тому що будуть оброблятися не 4 байти, як покладено для змінної типу float, а тільки 2 байти, тому що базовий тип вказівника - int.
Як і над іншими типами змінних, над вказівниками можна робити арифметичні операції: додавання і віднімання. (Операції ++ і -- є окремими випадками операцій додавання і віднімання.) Арифметичні дії над покажчиками мають свої особливості. Виконаємо найпростішу программу
# include <stdio.h>
/* Приклад 30. */
main()
{
int *p;
int х;
p=&x;
рrintf("%р %р", p, ++р);
}
Після виконання цієї програми ми побачимо, що при операції ++р значення покажчика р збільшилося не на 1, а на 2. І це правильно, тому що нове значення вказівника повинно вказувати не на наступну адресу пам'яті, а на адресу наступного цілого. А ціле, як ми пам'ятаємо, займає 2 байти. Якби базовий тип вказівника був не int, a double, то були б надруковані адреси, що відрізняються на 8, саме стільки байт пам'яті займає змінна типу double, тобто при кожній операції ++р значення покажчика буде збільшуватись на кількість байтів, що займають змінну базового типу вказівника.
Операції над вказівниками не обмежуються тільки операціями ++ і --. До вказівників можна додавати деяке ціле або віднімати ціле. Нехай вказівник р має значення 2000 і вказує на ціле. Тоді в результаті виконання оператора
р=р+3;
значення вказівника р буде 2006. Якщо ж вказівник р1=2000 був би вказівником на float, то після застосування оператора
р1=р1+10;
значення р1 було б 2040.
Загальна формула для обчислення значення вказівника після виконання операції р=р+n; буде мати вигляд
<р>=<р>+n*<кільк. байт пам'яті базового типу вказівника>
Можна також віднімати один вказівник від іншого. Так, якщо р і p1 - вказівники та елементи того самого масиву, то операція р - р1 дає такий же результат, як і віднімання індексів відповідних елементів масиву.
Інші арифметичні операції над вказівниками заборонені, наприклад не можна скласти два вказівники, помножити вказівник на число і т.д.
Вказівники можна порівнювати. Застосовнао всі 6 операцій:
<, >, <=, >=, = , == і !=.
Порівняння р < g означає, що адреса, що знаходиться в р, менше адреси, що знаходиться в g.
Якщо р и g вказують на елементи одного масиву, то індекс елемента, на який вказує р, менше індексу масиву, на який указує g.