Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Вказівники та посилання.docx
Скачиваний:
0
Добавлен:
01.04.2025
Размер:
857.42 Кб
Скачать

6.3. Вказівники і масиви

Будь-яку операцію, яку можна виконати за допомогою індексів масиву, можна виконати і за допомогою вказівників. Варіант із вказівниками більш швидкий, але й більш складний для розуміння. Опис

int a[10];

визначає масив розміром 10, тобто набір з 10 послідовних об’єктів a[0], a[1], ..., a[9]. Запис a[i] відповідає елементу масиву через i позицій від початку. Якщо ра – вказівник цілого, описаний як int *pa, то присвоєння pa = &a[0] означає, що pa вказує на нульовий елемент масиву a, тобто ра містить адресу елемента a[0]. Присвоєння x = *pa буде копіювати вміст a[0] у x. Якщо pa вказує на деякий певний елемент масиву a, то pa + 1 вказує на наступний елемент. Взагалі pa-і вказує на елемент, який розміщено на i позицій до елемента, що вказується pa, а pa + i на елемент, розміщений на i позицій після елемента. Отже, якщо ра вказує на а[0], то *(ра + 1) посилається на вміст а[1], ра + і – адреса а[і], а *(ра + і) – вміст а[і]. Ці зауваження справедливі незалежно від типу змінних у масиві a.

Очевидно існує дуже тісна відповідність між індексацією і арифметикою вказівників. Насправді компілятор перетворить посилання на масив у вказівник на початок масиву. В результаті цього ім’я масиву є вказівником виразу, тобто ім’я масиву є синонімом місця розташування його нульового елемента. Тому pa = &a[0] можна записати як pa = a.

Посилання на a[i] можна записати у вигляді *(a+i), тобто ці дві форми еквівалентні. Якщо застосувати операцію & до обох частин такого співвідношення еквівалентності, то &a[i] і a+i теж будуть ідентичними: a+i – адреса i-го елемент від початку a. Проте, якщо pa є вказівником, то у виразах його можна використати з індексом pa[i], що ідентично *(pa+i). Отже, будь-який вираз, який містить масиви й індекси, можна записати через вказівники та зсуви і навпаки.

Слід мати на увазі, що вказівник є змінною, тому операції pa = a і pa++ мають сенс. Але ім’я масиву є константою, а не змінною, тому конструкції типу a = pa або a++, або p = &a будуть помилковими.

Коли ім’я масиву передається функції, то насправді їй передається місце розташування початку цього масиву. Отже ім’я масиву як аргумент дійсно є вказівником, тобто змінною, яка містить адресу. Розглянемо функцію StrLen (див. ПП6.4 на СD), яка обчислює довжину рядка, де описи формальних параметрів

char s[];

char *s;

еквівалентні, і який вид опису варто використати, визначається тим, які вирази будуть використані для написання функції.

Можна передати функції частину масиву, якщо задати як аргумент вказівник початку підмасиву. Наприклад, якщо a – масив, то як F(&a[2]), так і F(a+2) передають функції F адресу елемента a[2], оскільки і &a[2], і a+2 є вказівними виразами, що посилаються на третій елемент a.

6.4. Адресна арифметика

Якщо p є вказівником, то який би не був тип об’єкта, на який він вказує, операція p++ збільшує p так, що він вказує на наступний елемент набору цих об’єктів, а операція p += i збільшує p так, щоб він вказував на елемент, віддалений на i елементів від поточного елемента. Ці та аналогічні конструкції формами арифметики вказівників або адресної арифметики мови C++.

Проілюструємо можливості мови на прикладі програми розподілу пам’яті. Є дві функції: функція Alloc(n) повертає як своє значення вказівник p, що вказує на першу з n послідовних символьних позицій, які можуть бути використані програмою для зберігання символів; функція Free(p) звільняє зайняту в такий спосіб пам’ять. Отже, керована функціями Alloc і Free пам’ять є стеком або списком, у якому елемент, що вводиться останнім, вилучається першим.

Реалізація полягає в тому, щоб функція виділяла відрізки великого символьного масиву (надамо ім’я allocbuf), який є власністю функцій Alloc і Free. Вони працюють із вказівниками, а не з індексами масиву, тому ніякій іншій функції не потрібно знати імен цього масиву. Його можна описати як зовнішній статичний (локальний стосовно вихідного файлу, що містить Alloc і Free) і невидимий за його межами.

Іншою потрібною інформацією є, інформація про, яка частина масиву allocbuf уже використана. Для цього використовують вказівник першого вільного елемента – allocp. Коли потрібно функції Alloc виділити місце під n символів, вона перевіряє, чи досить залишилося для цього місця в allocbuf. Якщо так, то Alloc повертає поточне значення allocp (тобто початок вільного блоку), потім збільшує його на n для того, щоб він вказував на наступну вільну область. Функція Free(p) визначає allocp рівним p за умови, що p вказує на позицію всередині allocbuf (див. ПП6.5 на СD).

Зазначимо, що вказівник може бути ініційований так само, як і будь-яка інша змінна. Об’єкт null – вираз, який містить адреси визначених раніше даних відповідного типу. Опис

static char* allocp = allocbuf;

визначає allocp як вказівник на символи і ініціює його так, щоб він вказував на allocbuf, тобто на першу вільну позицію на початку роботи програми. Ім’я масиву є адресою його нульового елемента, що можна записати так:

static char* allocp = &allocbuf[0];

За допомогою перевірки

if (allocp+n <= allocbuf + allocsize)

з’ясовується, чи залишилося досить місця, щоб задовольнити запит на n символів. Якщо достатньо, то нове значення allocp не буде вказувати далі, ніж на останню позицію allocbuf. Якщо запит може бути вдоволено, то Alloc повертає звичайний вказівник. Якщо ж ні, то Alloc повинна повернути деяку ознаку, яка свідчить про те, що більше місця не залишилося. У мові C++ гарантується, що жоден правильний вказівник даних не може мати значення нуль, тому повернення нуля може служити сигналом про ненормальну подію, у цьому випадку про відсутність місця. Однак замість нуля пишемо null (спеціальне значення вказівника). Взагалі цілі числа не можуть присвоюватись вказівникам, а нуль – це особливий випадок.

Перевірки if (allocp + n <= allocbuf + aloocsize) і if (p >= allocbuf && p < allocbuf + allocsize) показують кілька важливих аспектів арифметики вказівників. По-перше, за певних умов вказівники можна порівнювати. Якщо p і q вказують на елементи того самого масиву, то такі відношення, як <, >=, > і т. д., працюють належним чином. Наприклад, p < q справедливо, якщо p вказує на більш ранній елемент масиву, ніж q. Відношення == та != теж працюють. Довільний вказівник можна порівняти на рівність або нерівність із null. Але ні в чому не можна бути впевненим, якщо використати порівняння під час роботи з вказівниками, які вказують на різні масиви.

По-друге, вказівник і ціле число можна додавати і віднімати. Конструкція p + n має на увазі n-й об’єкт за тим, на який p указує в цей момент. Це справедливо, незалежно від того, на який вид об’єктів p повинен вказувати; компілятор сам масштабує n відповідно до опису p розміру об’єктів, які вказують за допомогою p. Наприклад, масштабний множник дорівнює 1 для char, 2 для int і short, 4 для long і float і 8 для double.

Віднімання вказівників теж можливе: якщо p і q вказують на елементи того самого масиву, то p-q – кількість елементів між p і q. Цей факт був використаний для написання варіанта функції StrLen (див. ПП6.6. на СD).

Під час опису вказівник p у цій функції ініційований за допомогою рядка s, у результаті чого він вказує на перший символ рядка. У циклі while по черзі перевіряється кожен символ до появи символа кінця рядка ’\0’ (значення 0). Явну перевірку можна опустити , і цикли записати так:

while (*p)

p++;

p вказує на символи, тому оператор p++ пересуває p щоразу так, щоб він вказував на наступний символ. У результаті вираз p-s обчислює кількість переглянутих символів, тобто довжину рядка. Арифметика вказівників враховує тип змінної. Якби оброблялися змінні типу float, які займають більше пам’яті, ніж змінні типу char, і якби p був вказівником на float, то оператор p++ пересунув би p на наступне значення типу float. Отже, можна написати інший варіант функції Alloc, яка розподіляє пам’ять для float. Для цього потрібно всюди в Alloc і Free дескриптор char змінити на float. Усі дії з вказівниками автоматично враховують розмір об’єктів.

За винятком операцій додавання, віднімання вказівника, цілого числа, віднімання і порівняння двох вказівників уся інша арифметика вказівників є хибною. Заборонено складати два вказівники, множити, ділити, об’єднувати або маскувати, а також додавати до них змінні типу float або double.