Мы используем куки, чтобы пользоваться сайтом было удобно.
Хорошо
to the top
close form

Заполните форму в два простых шага ниже:

Ваши контактные данные:

Шаг 1
Поздравляем! У вас есть промокод!

Тип желаемой лицензии:

Шаг 2
Team license
Enterprise license
** Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности
close form
Запросите информацию о ценах
Новая лицензия
Продление лицензии
--Выберите валюту--
USD
EUR
RUB
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
Бесплатная лицензия PVS‑Studio для специалистов Microsoft MVP
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
Для получения лицензии для вашего открытого
проекта заполните, пожалуйста, эту форму
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
Мне интересно попробовать плагин на:
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
check circle
Ваше сообщение отправлено.

Мы ответим вам на


Если вы так и не получили ответ, пожалуйста, проверьте папку
Spam/Junk и нажмите на письме кнопку "Не спам".
Так Вы не пропустите ответы от нашей команды.

Вебинар: C++ ЛИНТЕРЫ — ХОРОШО, НО НЕДОСТАТОЧНО - 20.06

>
>
>
Следует ли проверять указатель на NULL …

Следует ли проверять указатель на NULL перед вызовом функции free?

06 Фев 2024

Короткий ответ: нет. Тем не менее, раз про это вновь и вновь спрашивают на Reddit, Stack Overflow и других сайтах, пришло время подробно разобрать эту тему. Оказывается, есть много интересного, о чём можно порассуждать.

1100_check_before_free_ru/image1.png

Функция free

Функция free объявлена в заголовочном файле <stdlib.h> следующим образом:

void free( void *ptr );

Функция освобождает буфер памяти, выделенный ранее с помощью функций malloc, calloc, realloc, aligned_alloc.

Если аргументом является нулевой указатель, то функция ничего не делает.

cppreference.com: free

If ptr is a null pointer, the function does nothing.

Благодаря этому нет нужды предварительно проверять указатель перед вызовом free.

if (ptr)     // избыточная проверка
  free(ptr);

Такой код избыточен, так как проверка не выполняет какую-либо полезную роль. Если указатель нулевой, то его можно без опасений передать в функцию free. Это осознанный выбор разработчиков стандарта С:

cppreference.com: free

The function accepts (and does nothing with) the null pointer to reduce the amount of special-casing

Если указатель не нулевой, но и не валидный, то проверка опять-таки ни от чего не защитит. Невалидный ненулевой указатель всё равно будет передан в функцию free, что приведёт к неопределённому поведению.

cppreference.com: free

The behavior is undefined if the value of ptr does not equal a value returned earlier by malloc(), calloc(), realloc(), or aligned_alloc() (since C11).

The behavior is undefined if the memory area referred to by ptr has already been deallocated, that is, free(), free_sized(), free_aligned_sized(), or realloc() has already been called with ptr as the argument and no calls to malloc(), calloc(), realloc(), or aligned_alloc() resulted in a pointer equal to ptr afterwards.

Поэтому можно и нужно писать просто:

free(ptr);

Откуда берутся вопросы про предварительную проверку указателя

Документация к функции free явно говорит, что в неё можно передавать нулевой указатель и это безопасно. Тем не менее, на разных площадках интернета вновь и вновь появляются обсуждения этой темы. Задаваемые вопросы можно разделить на две категории.

Вопросы новичков. Таких вопросов больше всего и здесь всё просто. Люди только учатся программировать и пока не разобрались, когда нужно проверять указатели, а когда не нужно. Им достаточно простых пояснений. Когда выделяете память с помощью malloc, указатель нужно проверить. Если этого не сделать, может возникнуть неопределённое поведение при разыменовании нулевого указателя. Перед освобождением памяти с помощью free указатель проверять не надо, так как это сделает сама функция.

Собственно, на этом всё. Разве что ещё можно посоветовать новичку использовать online анализатор, чтобы быстрее разобраться, что не так с кодом.

Вопросы бывалых и слишком дотошных. Вот тут интереснее. Эти люди знают, что написано в документации. Однако они всё равно задаются этим вопросом, так как не уверены, что вызов free(NULL) всегда будет безопасен. Они беспокоятся, что их код может быть скомпилирован на очень старых системах, где free не гарантирует безопасную работу с нулевыми указателями. Или что будет использоваться специфическая сторонняя библиотека, которая реализует free нестандартно: не выполняя проверки на NULL.

Чисто теоретически это можно пообсуждать. Но с практической точки зрения это не имеет смысла.

Начнём с очень старых систем. Во-первых, такую систему ещё надо умудриться найти. То, что функция free должна безопасно обрабатывать NULL, написано ещё в первом стандарте языка C89.

C89: 4.10.3.2 The free function.

The free function causes the space pointed to by ptr to be deallocated, that is, made available for further allocation. If ptr is a null pointer, no action occurs.

Во-вторых, если вы столкнётесь с системой, относящейся к "достандартным временам", то, скорее всего, по множеству причин вы не сможете собрать своё приложение. Да и сомнительно, что вообще это может понадобиться. Проблема выглядит надуманной.

Теперь допустим, что система не времён мамонтов, а, скажем так, особенная. Используется какая-то сторонняя специфическая библиотека системных функций, которая реализует функцию free на своё усмотрение: в неё нельзя передавать NULL.

В таком случае сломанный free будет не самой большой вашей проблемой. Если уж в библиотеке сломана одна из базовых функций языка, то в ней будет сломано ещё столько всего, что про защищенный вызов free можно и не переживать.

Это как садиться в самодельную машину с неработающими тормозами, заклинивающим рулевым колесом, без зеркал заднего вида и при этом заботиться, надёжно ли клеммы подключены к аккумулятору. Клеммы — это важно, но проблема не в них, а в ситуации в целом.

Иногда тема предварительной проверки указателя обсуждается с точки зрения микрооптимизации кода: "можно избавиться от вызова функции free, если в коде самим проверить указатель". Это тот случай, когда перфекционизм точно идёт во вред. Мы рассмотрим эту идею подробно чуть ниже.

Кринжовый макрос

Самое бестолковое и потенциально опасное, что только можно сделать — это реализовать проверку указателя с помощью вот такого макроса:

#define SAFE_FREE(ptr) if (ptr) free(ptr)

У нас на собеседовании даже есть вопрос: "Что не так с этим макросом?" С ним не так всё. Казалось бы, такому макросу вообще не место в реальности, но мы его встречали в проектах. Давайте разберём его по косточкам.

Во-первых, этот макрос является лишней сущностью.

Как мы уже обсудили ранее, функция free безопасно обрабатывает нулевые указатели, и такая проверка не даёт никакой дополнительной безопасности при работе с указателями.

Во-вторых, он лишний и с точки зрения микрооптимизации.

Я читал в комментариях, что дополнительная проверка чуть-чуть оптимизирует код, так как компилятору не надо генерировать дорогостоящий вызов функции free. На мой взгляд, это какая-то бессмыслица, а не оптимизация.

Цена вызова функции преувеличена. В любом случае она ничтожна на фоне ресурсоёмкости таких операций, как выделение и освобождение буфера памяти. Если думать над оптимизацией, то следует поработать над уменьшением количества операций выделения памяти, а не над тем, чтобы сделать проверку ещё до вызова функции free.

Типовой сценарий при программировании: память успешно выделена, а затем освобождается. Нулевые указатели, передаваемые в free, это, скорее всего, особые, редкие, нестандартные случаи. Нет смысла их "оптимизировать". Скорее всего, дополнительная проверка будет пессимизацией. Ведь теперь, прежде чем начать освобождать память, будет выполнено две проверки вместо одной. Возможно, компилятор это оптимизирует, но тогда тем более непонятно, зачем вся эта суета. Кстати, раз уж речь зашла об оптимизациях, то рассмотренная ручная оптимизация с помощью макроса выглядит наивной и бестолковой. Надо писать простой понятный код, а не пытаться заниматься микрооптимизациями, которые компилятор делает лучше человека.

Думаю, эта попытка ненужной оптимизации отлично подтверждает известное высказывание Дональда Кнута:

Нет сомнений в том, что Грааль эффективности ведёт к злоупотреблениям. Программисты тратят огромное количество времени на размышления или беспокойство о скорости некритичных частей своих программ, и эти попытки повышения эффективности на самом деле имеют огромное негативное влияние, когда рассматриваются вопросы отладки кода и его поддержка. Мы должны забыть о небольшой эффективности, скажем, примерно в 97% случаев: преждевременная оптимизация — это корень всех зол.

В-третьих, макрос провоцирует ошибки.

Используя макрос, очень легко создать некорректный код.

#define SAFE_FREE(ptr) if (ptr) free(ptr)
....
if (A)
  SAFE_FREE(P);
else
  foo();

Код выглядит не так, как работает. Давайте раскроем макрос:

if (A)
  if (P) free(P);
else
  foo();

Оператор else относится к второму оператору if и выполняется, когда указатель нулевой. В общем, всё работает не так, как задумывалось. SAFE_FREE макрос оказался не таким уж "SAFE".

Есть и другие способы случайно создать некорректный код. Представим вот такой код очистки двумерного массива.

int **A = ....;
....
int **P = A;

for (....)
  SAFE_FREE(*P++);
SAFE_FREE(A);

Да, он немного надуманный, но показывает небезопасность макроса при работе со сложными выражениями. Проверяется один указатель, а освобождается следующий за ним:

for (....)
  if (*P++) free(*P++);

В придачу произойдёт выход за границу массива.

В общем, всё плохо.

А можно ли исправить макрос?

Можно, хотя этим не нужно заниматься. Рассмотрим возможные способы исключительно в образовательных целях. Про это мы тоже спрашиваем на собеседовании.

Во-первых, макрос надо защитить от проблемы с else. Самый простой, но неудачный способ, это добавить фигурные скобки:

#define SAFE_FREE(ptr) { if (ptr) free(ptr); }

Рассмотренный ранее код перестанет компилироваться (error: 'else' without a previous 'if'):

if (A)
  SAFE_FREE(P);
else
  foo();

Поэтому можно воспользоваться вот таким приёмом:

#define SAFE_FREE(ptr) do { if (ptr) free(ptr); } while(0)

Теперь код вновь компилируется. Первая проблема решена, но как быть с проблемой повторных вычислений? Рекомендуемого стандартного решения нет, но, если уж очень хочется, есть обходные пути.

Подобная проблема возникает и при реализации таких макросов как max. Рассмотрим пример:

#define max(a, b) ((a) > (b) ? (a) : (b))
....
int X = 10;
int Y = 20;
int Z = max(X++, Y++);

В переменную Z будет записано не 20, а 21, так как к моменту выбора переменной Y произойдёт её инкремент:

int X = 10;
int Y = 20;
int Z = ((X++) > (Y++) ? (X++) : (Y++));

Чтобы этого избежать, можно воспользоваться магией — расширением компилятора GCC: Referring to a Type with typeof.

#define max(a,b) \
  ({ typeof (a) _a = (a); \
      typeof (b) _b = (b); \
    _a > _b ? _a : _b; })

Суть в том, чтобы скопировать значения во временные переменные и тем самым исключить повторное вычисление выражений. Оператор typeof — это аналог decltype из C++. Только он для C.

Ещё раз обращаю внимание, что это нестандартное расширение. Не стоит им пользоваться без крайней необходимости.

Применим теперь этот способ к SAFE_FREE:

#define SAFE_FREE(ptr) do { \
  typeof(ptr) copy = (ptr); \
  if (copy) \
    free(copy); \
} while(0)

Работает. Но ради этого нам пришлось написать страшный, непереносимый и на самом деле ненужный код.

Более изящным решением является переделать макрос в функцию. Тогда рассмотренные проблемы перестанут существовать, и код упростится:

void SAFE_FREE(void *ptr)
{
  if (ptr)
    free(ptr);
}

Стоп, подождите! Мы опять вернулись к вызову функции! Только теперь перед нами лишняя функция-прослойка. Функция free и так делает ту же самую работу по проверке указателя.

Поэтому знайте, что самый правильный способ исправить макрос SAFE_FREE, это удалить его!

Обнуление указателя после free

Есть тема — не особо связанная с проверкой указателя, — но которую можно обсудить за компанию. Некоторые программисты рекомендуют обнулять указатель после освобождения памяти. Просто на всякий случай.

free(pfoo);
pfoo = NULL;

Можно сказать, что код написан в парадигме защитного программирования (defensive programming). Суть в дополнительных необязательных действиях, которые иногда страхуют от ошибок.

В нашем случае, если указатель pfoo не используется, то обнулять его смысла нет. Однако это можно сделать из следующих соображений.

Доступ по указателю. Если случайно по этому указателю будут записаны данные, произойдёт не порча памяти, а разыменование нулевого указателя. Такая ошибка быстрее будет обнаружена и исправлена. Аналогично с чтением данных по указателю.

Double-free. Обнуление указателя защищает от ошибок повторного освобождения буфера. Однако польза от этого не так однозначна, как может казаться на первый взгляд. Рассмотрим код с ошибкой:

float *ptr1;
char *ptr2;
....
free(ptr1);
ptr1 = NULL;
....
free(ptr1);  // должна была использоваться переменная ptr2
ptr2 = NULL;

Программист опечатался, и вместо ptr2 он повторно освобождает память, используя указатель ptr1. Благодаря обнулению указателя ptr1 при повторном освобождении ничего не произойдёт. Код защищен от double-free ошибки. С другой стороны, обнуление указателя глубже спрятало ошибку. Происходит утечка памяти, которая может оказаться сложной для обнаружения.

Из-за подобных случаев (маскировка ошибок, замена одной ошибки на другую) у защитного программирования есть критики. Это большая тема, и я не готов в неё погрузиться. Однако считаю правильным предупредить о негативных сторонах защитного программирования.

Как лучше поступить, если вы решили обнулять указатели после освобождения памяти?

Для начала напишем опасный вариант:

#define FREE_AND_CLEAR(ptr) do { \
  free(ptr); \
  ptr = NULL; \
} while(0)

Макрос не рассчитан для такого использования:

int **P = ....;
for (....)
  FREE_AND_CLEAR(*P++);

Освобождается один указатель, а обнуляется следующий. Доработаем макрос:

#define FREE_AND_CLEAR(ptr)  do { \
  void **x = &(ptr); \
  free(*x); \
  *x = NULL; \
} while(0)

Работает, но, если честно, мне такой макрос не нравится. Я предпочту явно обнулять указатель:

int **P = ....;
for (....)
{
  free(*P);
  *P = NULL;
  P++;
}

Этот код длинный и тоже мне не нравится. Зато без магии макросов. Я не люблю макросы. То, что код длинный и некрасивый, хороший повод подумать, как его переписать. Нужно ли именно так коряво перебирать и освобождать указатели? Возможно, код можно сделать более изящным. Хороший повод заняться рефакторингом.

Заключение

Не пытайтесь заранее решить выдуманные проблемы на всякий случай. Пишите код простым и понятным.

Дополнительные ссылки

Популярные статьи по теме


Комментарии (0)

Следующие комментарии next comments
close comment form