Це звучить як жах рекурсії, але покажчики на покажчики є невід’ємною частиною системного програмування. По суті, ви вказуєте адресу пам’яті, яка містить адресу ваших фактичних даних. Ця подвійна непряма адресація називається розробниками ручкою (handle). Це не просто трюк у коді. Це необхідний механізм, який операційні системи використовують для управління пам’яттю купи (heap).
Розглянемо купу як переповнену кімнату. Іноді операційній системі потрібно переміщати людей, щоб звільнити місце. Якщо ви тримаєте прямого покажчика на когось, ця людина не може переміститися, не порушивши ваше посилання. Але якщо ви тримаєте вказівник на список імен, операційна система може змінити розташування будь-якої людини в цьому списку без необхідності оновлювати ваше початкове посилання. У цьому полягає сила ручки.
Ось як ця логіка перекладається в сирий код на C. Ви оголошує p як покажчик на покажчик. Потім q стає стандартним покажчиком. Ви виділяєте пам’ять для p, а потім виділяєте пам’ять для того, на що вказує p. Нарешті, ви двічі розіменовуєте вказівник, щоб визначити значення 12.
`
Windows та macOS покладаються на цю структуру для ущільнення пам’яті. Тут важлива відмінність. Ви, як програміст, керуєте зовнішнім покажчиком p. Операційна система управляє внутрішнім покажчиком *p. Оскільки ОС контролює *p, вона може переміщати блок фактичних даних (**p) у будь-яке місце купи. Вона просто оновлює *p, щоб відобразити нову адресу. Ваш код продовжує використовувати p без проблем.
Крім управління пам’яттю ОС, цей шаблон необхідний передачі покажчиків у функції. Якщо вам потрібно, щоб функція змінювала сам покажчик, ви передаєте цей покажчик. Це єдиний спосіб перепризначити посилання з іншої області видимості.
Управління вказівниками на структури
Складність не обмежується простими цілими числами. Ви можете вкласти цю логіку у структури. Це часто зустрічається під час обробки даних змінної довжини, таких як рядки.
Розгляньте структуру Addr. Вона містить масиви фіксованого розміру для імен, міст та телефонів. Але коментарі мають різну довжину. Тому comment визначено як покажчик на символ (char). Коли ви виділяєте пам’ять для самої структури, ви резервуєте місце для вказівників та фіксованих масивів. Ви ще не резервуєте місце для фактичного тексту коментаря.
`
Спочатку ви виділяєте s. Потім ви читаєте введення користувача у фіксовані буфери. Після цього ви читаєте коментар у тимчасовий

Ви втрачаєте пам’ять, якщо ігноруєте вкладеність покажчиків.
Розглянемо вказівник s, який вказує на структуру. Ця структура містить ще один покажчик. Цей другий покажчик вказує на фактичний рядок у пам’яті. Два рівня непрямої адресації. Один розподіл пам’яті на структуру. Один розподіл пам’яті на рядок.
Все просто, поки ви не спробуєте звільнити пам’ять.
Саме тут більшість розробників спотикаються. Ви бачите «безкоштовно(и)». Ви думаєте, що все готово. Ви помиляєтесь.
Подивіться на цей фрагмент коду:
Змінна s вказує на структуру Addr. Усередині цієї структури є поле «коментар». comment вказує на блок пам’яті в купі, виділеній для даних рядка.
Коли ви викликаєте free(s), ви звільняєте пам’ять, яку займає сама структура. Структура Addr зникає. Пам’ять повертається в систему.
Але що відбувається з s->comment?
Він зникає в забутті.
Покажчик на дані рядка зберігається в структурі, яку ви щойно звільнили. Доступ до нього більше неможливий. Ви не викликали free() у рядку. Пам’ять залишається виділеною, але недоступною.
Це витік пам’яті.
Це не аварія. Це не повідомлення про помилку. Програма працює нормально. Він просто повільно споживає все більше і більше оперативної пам’яті, доки система не почне використовувати файл підкачки або не вийде з ладу.
Як виправити витоки через подвійні покажчики
Спочатку потрібно звільнити внутрішній покажчик. Або збережіть його у тимчасовій змінній перед звільненням зовнішньої структури.
Тепер ці рядки звільнені. Потім конструкцію відпускають. Немає втрачених блоків пам’яті.
Чому це так часто трапляється?
Люди сприймають покажчики як цінності. Вони забувають, що покажчики є посиланнями на ресурси.
Якщо структура містить вказівник, структура не є самодостатньою. Це залежить від зовнішньої пам’яті. Звільнення контейнера не звільняє його вміст.
Ситуація погіршується при глибшому вкладенні. Вказівник на вказівник на вказівник. Три рівня. Вам потрібно три виклики free(). У правильному порядку.
Якщо ви забудете один, станеться витік.
Проблема з gets()
У прикладі використовується gets(). Ця функція небезпечна. Його було видалено зі стандарту C11, оскільки він допускає переповнення буфера. Але логіка роботи з пам’яттю залишається незмінною.
Незалежно від того, використовуєте ви gets(), fgets() або scanf(), стратегія розподілу пам’яті є вузьким місцем, що призводить до витоків.
Ви виділяєте пам’ять для s.
Ви виділяєте пам’ять для s->comment.
Якщо ви звільняєте лише s, ви залишаєте s->comment позаду.
Ключові висновки
- Подвійні покажчики вимагають подвійного звільнення.
- Перевірте кожен
mallocна наявність відповідногоfree. - Якщо структура містить покажчик на динамічну пам’ять, ця пам’ять повинна бути звільнена перед тим, як можна буде звільнити саму структуру.
free()не звільняє члени структури рекурсивно.
Його легко промахнутися. Код компілюється. Це працює. Витік відбувається непомітно.
Поки не пізно.
Створіть пов’язані списки
Якщо звільнити контейнер до даних, які він вказує, можна отримати катастрофічну ситуацію з пам’яттю. Структура, що містить покажчик, буде очищена, а блок рядка залишиться. Він стане втраченим блоком. Це відбувається, коли порядок визволення неправильний.
Зв’язування
Структури можуть вказувати на себе. Це дозволяє об’єднувати ідентичні записи в ланцюжок. Результатом є пов’язаний перелік. Це стандартний спосіб організації даних у мові C.
Ось як це визначається:
`
Поле next містить адресу наступного запису. Для початку ланцюжка використовується одна змінна-покажчик.

Вартість гнучкості
Компілятор дозволяє порушувати правила, які на перший погляд можуть здатися контрінтуїтивними. При достатньому досвіді ви можете створювати структури, схожі на наведену вище. Це вияв сили.
Але це не обходиться без ризиків. Ви йдете по тонкій межі між розумним кодом і спагетті-кодом, що не підтримується.
Чому це важливо
Йдеться не просто про синтаксис. Йдеться про те, що мова дозволяє робити, коли ви долаєте її обмеження.
- Контроль : Ви отримуєте детальний контроль за розташуванням даних у пам’яті.
- Взаємодія : Ви можете легко взаємодіяти з бібліотеками на C.
- Продуктивність : Іноді обхід перевірок безпеки заощаджує цикли процесора.
Але є аспект: компілятор не врятує вас від вас самих. Він дозволить скомпілювати код, який впаде під час виконання.
Як використовувати це безпечно
Якщо ви збираєтеся це робити, робіть це свідомо.
- Ізолюйте : Не розповсюджуйте цей патерн по всій кодовій базі. Тримайте його у невеликому, добре протестованому модулі.
- Документуйте все : Майбутній ви подякує справжньому. Або зненавидить. Швидше за все, зненавидить.
- Використовуйте абстракції : Поверніть сирі покажчики або небезпечні блоки у чистий інтерфейс. Сховайте безладдя.
Перевірка реальністю
Більшості розробників це не потрібно. Вони використовуватимуть стандартні структури. І будуть щасливішими від цього.
Але коли ви упреєтеся у стіну, де стандартна бібліотека не підходить, ви будете раді, що компілятор не зупинив вас.
Питання, чи готові ви платити ціну за підтримку такого коду.
Це компроміс. Швидкість за безпеку. Потужність за ясність.
Вибір за вами




















































