Это звучит как кошмар рекурсии, но указатели на указатели являются неотъемлемой частью системного программирования. По сути, вы указываете на адрес памяти, который содержит адрес ваших фактических данных. Эта двойная косвенная адресация называется разработчиками ручкой (handle). Это не просто трюк в коде. Это необходимый механизм, который операционные системы используют для эффективного управления памятью кучи (heap).
Рассмотрим кучу как переполненную комнату. Иногда операционной системе нужно перемещать людей, чтобы освободить место. Если вы держите прямой указатель на кого-то, этот человек не может переместиться, не нарушив вашу ссылку. Но если вы держите указатель на список имен, операционная система может изменить местоположение любого человека в этом списке без необходимости обновлять вашу начальную ссылку. В этом и заключается сила ручки.
Вот как эта логика переводится в сырой код на C. Вы объявляете p как указатель на указатель. Затем q становится стандартным указателем. Вы выделяете память для p, а затем выделяете память для того, на что указывает p. Наконец, вы дважды разыменовываете указатель, чтобы присвоить значение 12.
Windows и macOS полагаются на эту структуру для уплотнения памяти. Здесь важно различие. Вы, как программист, управляете внешним указателем p. Операционная система управляет внутренним указателем *p. Поскольку ОС контролирует *p, она может перемещать блок фактических данных (**p) в любое место кучи. Она просто обновляет *p, чтобы отразить новый адрес. Ваш код продолжает использовать p без проблем.
Помимо управления памятью ОС, этот шаблон необходим для передачи указателей в функции. Если вам нужно, чтобы функция изменяла сам указатель, вы передаете указатель на этот указатель. Это единственный способ переназначить ссылку из другой области видимости.
Управление указателями на структуры
Сложность не ограничивается простыми целыми числами. Вы можете вложить эту логику внутрь структур. Это часто встречается при обработке данных переменной длины, таких как строки.
Рассмотрите структуру Addr. Она содержит массивы фиксированного размера для имен, городов и телефонов. Но комментарии имеют разную длину. Поэтому comment определен как указатель на символ (char). Когда вы выделяете память для самой структуры, вы резервируете место для указателей и фиксированных массивов. Вы еще не резервируете место для фактического текста комментария.
Сначала вы выделяете s. Затем вы читаете ввод пользователя в фиксированные буферы. После этого вы читаете комментарий во временный
Вы теряете память, если игнорируете вложенность указателей.
Рассмотрим указатель s, который указывает на структуру. Эта структура содержит другой указатель. Этот второй указатель указывает на реальную строку в памяти. Два уровня косвенной адресации. Одно выделение памяти для структуры. Одно выделение памяти для строки.
Всё просто, пока вы не попытаетесь освободить память.
Именно здесь большинство разработчиков спотыкаются. Вы видите free(s). Вы думаете, что всё готово. Вы ошибаетесь.
Посмотрите на этот фрагмент кода:
Переменная s указывает на структуру Addr. Внутри этой структуры есть поле comment. 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.
- Производительность : Иногда обход проверок безопасности экономит циклы процессора.
Но есть нюанс: компилятор не спасет вас от вас самих. Он позволит скомпилировать код, который упадет во время выполнения.
Как использовать это безопасно
Если вы собираетесь это делать, делайте это осознанно.
- Изолируйте : Не распространяйте этот паттерн по всей кодовой базе. Держите его в небольшом, хорошо протестированном модуле.
- Документируйте всё : Будущий вы поблагодарит настоящего. Или возненавидит. Скорее всего, возненавидит.
- Используйте абстракции : Оберните сырые указатели или небезопасные блоки в чистый интерфейс. Скройте беспорядок.
Проверка реальностью
Большинству разработчиков это не нужно. Они будут использовать стандартные структуры. И будут счастливее от этого.
Но когда вы упретесь в стену, где стандартная библиотека не подходит, вы будете рады, что компилятор не остановил вас.
Вопрос в том, готовы ли вы платить цену за поддержку такого кода.
Это компромисс. Скорость в обмен на безопасность. Мощность в обмен на ясность.
Выбор за вами.






















