Урок №84. Вказівники

  Юрій  | 

  Оновл. 24 Лис 2020  | 

 568

На уроці №13 ми дізналися, що змінна — це назва шматочка пам’яті, який містить значення.

Оператор адресу &

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

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

Хороша новина: нам не потрібно турбуватися про те, які конкретно адреси в пам’яті виділені для певних змінних. Ми просто посилаємося на змінну через присвоєний їй ідентифікатор, а компілятор конвертує цей ідентифікатор у відповідну адресу в пам’яті. Однак цей підхід має деякі обмеження, які ми обговоримо на цьому і на наступних уроках.

Оператор адресу & дозволяє дізнатися, яку адресу в пам’яті присвоєно певній змінній. Все дуже просто:

Результат на моєму комп’ютері:

7
0046FCF0

Примітка: Хоча оператор адресу виглядає так само, як оператор побітового І, відрізнити їх можна по тому, що оператор адресу є унарним оператором, а оператор побітового І — бінарним оператором.

Оператор розіменування *

Оператор розіменування * дозволяє отримати значення по вказаному адресу:

Результат на моєму комп’ютері:

7
0046FCF0
7

Примітка: Хоча оператор розіменування виглядає так само, як і оператор множення, відрізнити їх можна по тому, що оператор розіменування — унарний, а оператор множення — бінарний.

Вказівники

Тепер, коли ми вже знаємо про операторів адресу і розіменування, ми можемо поговорити про вказівники.

Вказівник (або “покажчик”) — це змінна, значенням якої є адрес комірки в пам’яті. Вказівники оголошуються так само, як і звичайні змінні, тільки із зірочкою між типом даних і ідентифікатором:

Синтаксично мова C++ приймає оголошення вказівника, коли зірочка знаходиться поруч з типом даних, з ідентифікатором або навіть посередині. Зверніть увагу, ця зірочка НЕ є оператором розіменування. Це всього лише частина синтаксису оголошення вказівника.

Однак, при оголошенні кількох вказівників, зірочка повинна знаходитися біля кожного ідентифікатора. Це легко забути, якщо ви звикли вказувати зірочку біля типу даних, а не біля імені змінної. Наприклад:

З цієї причини, при оголошенні вказівника, рекомендується вказувати зірочку біля імені змінної. Як і звичайні змінні, вказівники не ініціалізуються при оголошенні. Вмістом неініціалізованого вказівника є звичайне сміття.

Присвоювання значень вказівнику

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

Це також можна проілюструвати наступним чином:

Ось чому вказівники мають таку назву: ptr містить адресу значення змінної value, і, можна сказати, ptr вказує на це значення.

Ще дуже часто можна побачити наступне:

Результат на моєму комп’ютері:

003AFCD4
003AFCD4

Тип вказівника повинен відповідати типу змінної, на яку він вказує:

Наступне не є допустимим:

Це пов’язано з тим, що вказівники можуть містити тільки адреси, а цілочисельний літерал 7 не має адресу в пам’яті. Якщо ви все ж зробите це, то компілятор повідомить вам, що він не може перетворити цілочисельне значення в цілочисельний вказівник.

Мова C++ також не дозволить вам напряму присвоювати адреси в пам’яті вказівнику:

Оператор адресу повертає вказівник

Варто зазначити, що оператор адресу & не повертає адресу свого операнда в якості літералу. Замість цього він повертає вказівник, що містить адресу операнда, тип якого отримано з аргументу (наприклад, адреса змінної типу int передається як адреса вказівника на значення типу int):

Результат виконання програми:

int *

Розіменування вказівників

Як тільки у нас є вказівник, який вказує на що-небудь, ми можемо його розіменувати, щоб отримати значення, на яке він вказує. Розіменований вказівник — це вміст комірки в пам’яті, на яку він вказує:

Результат:

0034FD90
5
0034FD90
5

Ось чому вказівники повинні мати тип даних. Без типу вказівник не знав би, як інтерпретувати вміст, на який він вказує (при розіменуванні). Також, тому і повинні збігатися тип вказівника з типом змінної. Якщо вони не будуть збігатися, то вказівник при розіменуванні може неправильно інтерпретувати біти (наприклад, замість типу double використати тип int).

Одному вказівнику можна присвоювати різні значення:

Коли адреса значення змінної присвоєна вказівнику, то виконується наступне:

   ptr — це те ж саме, що і &value;

   *ptr опрацьовується так само, як і value.

Оскільки *ptr опрацьовується так само, як і value, то ми можемо присвоювати йому значення так, як би це була звичайна змінна. Наприклад:

Розіменування некоректних вказівників

Вказівники в мові С++ по своїй суті є небезпечними, а їх неправильне використання — один з кращих способів отримати збій в програмі.

При розіменуванні вказівника, програма намагається перейти в комірку в пам’яті, яка зберігається в вказівнику і витягти вміст цієї комірки. З міркувань безпеки сучасні операційні системи (скор. “ОС”) запускають програми в пісочниці для запобігання їх неправильної взаємодії з іншими програмами і для захисту стабільності самої операційної системи. Якщо програма спробує отримати доступ до комірки в пам’яті, не виділеної для неї операційною системою, то ОС відразу завершить виконання цієї програми.

Наступна програма добре ілюструє вищесказане. При запуску ви отримаєте збій (спробуйте, нічого страшного з вашим комп’ютером не відбудеться):

Розмір вказівників

Розмір вказівника залежить від архітектури, на якій скомпільовано виконуваний файл: 32-бітний виконуваний файл використовує 32-бітні адреси в пам’яті. Відповідно, вказівник на 32-бітному пристрої займає 32 біти (4 байти). З 64-бітним виконуваним файлом вказівник буде займати 64 біти (8 байтів). І це незалежно від того, на що вказує вказівник:

Як ви можете бачити, розмір вказівника завжди один і той самий. Це пов’язано з тим, що вказівник — це всього лише адреса в пам’яті, а кількість біт, необхідна для доступу до адресу в пам’яті на певному пристрої, — завжди постійна.

Чим корисні вказівники?

Зараз ви можете подумати, що вказівники є непрактичними і взагалі непотрібними. Навіщо використовувати вказівник, якщо ми можемо використати вихідну змінну?

Однак, виявляється, вказівники корисні в наступних випадках:

   Випадок №1: Масиви реалізовані за допомогою вказівників. Вказівники можуть використовуватися для ітерації по масиву.

   Випадок №2: Вони є єдиним способом динамічного виділення пам’яті в С++. Це, безумовно, найбільш поширений варіант використання вказівників.

   Випадок №3: Вони можуть використовуватися для передачі великої кількості даних в функцію без копіювання цих даних.

   Випадок №4: Вони можуть використовуватися для передачі однієї функції в якості параметру іншій функції.

   Випадок №5: Вони використовуються для досягнення поліморфізму при роботі з наслідуванням.

   Випадок №6: Вони можуть використовуватися для представлення однієї структури/класу в іншій структурі/класі, формуючи, таким чином, цілі “ланцюжки”.

Вказівники застосовуються в багатьох випадках. Не хвилюйтеся, якщо ви багато чого не розумієте з вищесказаного. Тепер, коли ми розібралися зі вказівниками на базовому рівні, ми можемо почати заглиблюватися в окремі випадки, в яких вони корисні, що ми і зробимо на наступних уроках.

Висновки

Вказівники — це змінні, які містять адреси в пам’яті. Їх можна розіменувати за допомогою оператора розіменування * для вилучення значень, які містяться за адресою в пам’яті. Розіменування вказівника, значенням якого є сміття, призведе до збою в вашій програмі.

Порада: При оголошенні вказівника вказуйте зірочку біля імені змінної.

Тест

Завдання №1

Які значення ми отримаємо в результаті виконання наступної програми (припустимо, що це 32-бітний пристрій, і тип short займає 2 байти):

Відповідь №1

Значення:

0012FF60
7
0012FF60
7

0012FF60
9
0012FF60
9

0012FF54
3
0012FF54
3

4
2

Коротке пояснення щодо останньої пари: 4 і 2. 32-бітний пристрій означає, що розмір вказівника складає 32 біти, але оператор sizeof завжди виводить розмір в байтах: 32 біти = 4 байти. Таким чином, sizeof(ptr) дорівнює 4. Оскільки ptr є вказівником на значення типу short, то *ptr є типу short. Розмір short в цьому прикладі становить 2 байти. Таким чином, sizeof(*ptr) дорівнює 2.

Завдання №2

Що не так з наступним фрагментом коду?

Відповідь №2

Останній рядок не скомпілюється. Розглянемо цю програму детально.

У першому рядку знаходиться стандартне визначення змінної разом з ініціалізованим значенням. Тут нічого особливого.

У другому рядку ми визначаємо новий вказівник з ім’ям ptr і присвоюємо йому адресу змінної value. Пам’ятаємо, що в цьому контексті зірочка є частиною синтаксису оголошення вказівника, а не оператором розіменування. Так що і в цьому рядку все нормально.

У третьому рядку зірочка вже є оператором розіменування, і використовується для витягування значення, на яке вказує вказівник. Таким чином, цей рядок говорить: «Витягуємо значення, на яке вказує ptr (цілочисельне значення), і перезаписуємо його на адресу цього ж значення”. А це вже якась нісенітниця — ви не можете присвоїти адресу цілочисельному значенню!

Третій рядок повинен бути:

У вищенаведеному рядку ми коректно присвоюємо вказівнику адресу значення змінної.

Оцінити статтю:

1 Зірка2 Зірки3 Зірки4 Зірки5 Зірок (4 оцінок, середня: 5,00 з 5)
Loading...

Залишити відповідь

Ваш E-mail не буде опублікований. Обов'язкові поля відмічені *