Виділення пам’яті “на льоту” – це те, як програми на C ростуть і стискаються під час виконання. Ви не знаєте точних потреб вашої програми, поки вона фактично не почне виконуватися. Рішення полягає у використанні купи (heap). Ви просите блок. Операційна система резервує його. Ви використовуєте його. Потім ви повертаєте його за допомогою free. Цей цикл дозволяє іншим програмам повторно використовувати ту саму оперативну пам’ять.
Розглянемо наступний приклад. Це сама базова взаємодія з купою, яка тільки можлива.
`
Як насправді працює malloc
Функція Malloc – це не магія. Це механік, який перевіряє наявність інвентарю. Коли ви її викликаєте, відбуваються три дії послідовно.
- Він перевіряє поточну ємність купи. Він дивиться на параметр, який ви передали – в даному випадку
sizeof(int), що становить 4 байти. Він запитує, чи є досить безперервного простору. Якщо купа фрагментована чи заповнена, операція завершується невдачею. - Якщо простір існує, резервує цей блок. Система позначає його як ваш. Жоден інший виклик Malloc не може торкнутися цього фрагмента. Це запобігає випадковим колізіям між виділеннями пам’яті.
- Він повертає адресу цього блоку. Він не дає вам дані. Він дає вам змінну-покажчик. Ця змінна містить розташування. Тепер ви маєте посилання на конкретне місце в оперативній пам’яті.
У нашому прикладі p отримує цю адресу. Змінна вказівник — це просто контейнер для адреси пам’яті. Фактичне ціле значення знаходиться за цією адресою.
Чому необхідно перевіряти на NULL
Ви бачите перевірку if (p == 0) одразу після виділення пам’яті. Деякі новачки пропускають цей крок. Це фатальна помилка.
Завжди перевіряйте вказівник після будь-якого виклику malloc, щоб переконатися, що вказівник є дійсним.
Розмір купи постійно вагається. Це від того, які інші процеси запущені. Це залежить від того, скільки пам’яті вони захопили. Немає гарантій, що виклик завершиться успішно. Якщо ‘malloc’ зазнає невдачі, він повертає нуль. Нуль – це NULL. Якщо ви спробуєте розіменувати нульовий покажчик, програма аварійно завершить роботу. Жорстко.
Ви можете написати if(p==NULL) або if(!p). Вони означають те саме. Але ви маєте перевіряти. Щоразу.
Що станеться, якщо ви забудете звільнити пам’ять?
Якщо ваша програма завершується, а ви не викликали free, операційна система очищає все за вас. Вона звільняє простір виконуваного файлу. Вона очищує стек. Вона повертає світову пам’ять. Вона також повторно використовує будь-які очікування виділення в купі.
То чи означає це, що нормально «витікати» пам’яттю наприкінці програми? Технічно так. ОС виконує очищення. Для машини немає довгострокових наслідків.
Але це поганий тон. І, що важливіше, витоку пам’яті під час виконання смертельно небезпечні. Якщо ви продовжуєте виділяти пам’ять, не звільняючи її, ви задихаєте свій процес. Купа заповнюється. Нові виділення зазнають невдачі. Програма вмирає від ядухи, а не через аварійне завершення (crash).
Вказівники проти значень, на які вони вказують
Покажчики можуть збивати з пантелику. Вони відрізняються від значень, на які посилаються. Ось як це проявляється практично.
Перший сценарій. Ви виділяєте один блок. Ви вказуєте дві змінні на нього.
`
Висновок:
10
20
Чому? p та q – це різні покажчики. Але q = p змушує їх вказувати на один і той же блок пам’яті. Зміна значення через p змінює його і для q. Зміна його через q змінює його і для p. Це дві ручки одних дверей.
Другий сценарій. Ви виділяєте два окремі блоки.
`
Висновок:
20
Тут p та q вказують на різні адреси. *q містить 20. *p = *q копіює значення 20 у місце, на яке вказує p. Сам покажчик p не переміщається. Змінюється значення на адресу p.
Ця різниця має значення. Одне змінює адресу, що зберігається у змінній. Інше змінює дані на цю адресу. Плутанина між ними призводить до логічних помилок, які складніше налагоджувати, ніж помилки синтаксису.


Вказівники, пам’ять та вільне падіння
Висновок програми: 20. Рядок 6 говорить сама за себе.
Можна подумати, що *p = *q і p = q роблять одне й те саме. Це негаразд. Одна дія переміщує дані, інша – адреси. Компілятор дозволяє привласнення *p = *q, тому що обидва розіменовані покажчики мають тип int. Це прямий обмін значеннями. Але p = q? Це інше. Воно змушує p вказувати на той самий блок пам’яті, що і q. Переміщується адреса. Дані залишаються дома.
Якщо типи не співпадають, компілятор заблокує операцію. Вказівник на ціле число не може вказувати на рядок. Типи мають збігатися.
Чотири способи ініціалізації
Нова змінна-покажчик є небезпечною, доки не ініціалізована.
int *p; створює покажчик, який ні на що не вказує. Він свідчить про сміття. Розіменування такого покажчика – помилка. Вам потрібне відоме місце. Є чотири способи туди потрапити.
- malloc : Ця функція виділяє блок у купі (heap).
pтепер містить дійсну адресу. Покажчик ініціалізований, тому що вказує на конкретний, зарезервований фрагмент пам’яті. - Привласнення :
p = q;якщоqдійсний,pстає дійсним. Він успадковує адресуq. Якщоqмістить сміття,pтеж отримує сміття. - Явна адреса : Ви можете направити
pбезпосередньо на відому змінну.p = &i;направляєpна ціле числоi. Просто. Прямо. - Нуль/NULL :
p = 0;абоp = NULL;. Це встановлює адресу в нуль. Він не вказує на блок. Він свідчить про нульову позицію.
Навіщо використовувати нуль? Це прапор.
Ви можете перевірити if (p == 0), щоб дізнатися, чи дійсний покажчик. Система розпізнає це. Якщо ви спробуєте розіменувати нульовий покажчик, програма аварійно завершить роботу. Жорстко.
p = 0; * p = 5;
Другий рядок зазнає невдачі. p ні на що не вказує. Ви не можете записувати дані у нікуди. Ця поведінка є критично важливою для пов’язаних списків надалі. Це страхова сітка.
Звільнення купи
Виділення пам’яті недостатньо. Ви маєте повернути її.
malloc захоплює простір. free повертає його.
free(p) робить дві речі. По-перше, він скасовує резервування блоку. Купа повертає цю пам’ять. Вона може бути перевикористана. По-друге, p залишається неініціалізованим. Він не стає безпечним магічним чином. Ви повинні повторно ініціалізувати p перед його повторним використанням.
Блок зникає з вашої області видимості. Покажчик стає «висячим» (dangling).
Ось стандартний шаблон для роботи з одним цілим числом у купі:
`
Це переважно демонстрація. Але вона демонструє життєвий цикл.
sizeof(int) повертає розмір у байтах. На більшості машин це 4. Ви можете використовувати жорстко задане значення malloc(4). Не робіть цього. sizeof робить код переносимим. Він читається краще. Він адаптується за зміни архітектури.
Malloc повертає універсальний покажчик. Компілятори часто попереджають, якщо ви не наведете тип. (int *) перетворює цей універсальний блок на покажчик на ціле число. Це відповідає типу p.
free(p) повертає блок у купу.
Структури та пріоритет операторів
Структури поводяться так само. Ви виділяєте блок для всієї структури, а не лише окремих полів.
`
Подивіться на (*p).i = 10;.
Навіщо потрібні дужки?
Тому що . має вищий пріоритет, ніж *.
Без дужок *p.i означає *(p.i). Це намагається отримати доступ до i через покажчик p, а потім розіменувати результат. Це не те, що вам потрібно. Ви хочете спочатку розіменувати p, а потім отримати доступ до i.
Правила пріоритету є суворими. * пов’язується вже, ніж . у цьому контексті. Дужки змушують розіменування відбуватися до доступу до члена структури.
Більшість людей ненавидять багаторазове введення (*p).i. Це громіздко.
У C є скорочення.
p->i
Це точно те саме, що і (*p).i. Але його скоріше друкувати. Ви побачите -> скрізь у коді на C. Це стандартний спосіб доступу до членів структури через покажчик.
Купа – це кінцевий ресурс. Виділяйте пам’ять ретельно. Звільняйте її послідовно. Або спостерігайте, як ваша пам’ять витікає.


























