Вебинар: ГОСТ Р 71207–2024 - Статический анализ программного обеспечения. Терминология - 31.07
Указатель, который вернула функция malloc, необходимо проверить перед использованием. Неправильным решением будет использовать для этого макрос assert. В этой статье мы разберём, почему это является антипаттерном.
Под функцией malloc далее я буду подразумевать не только эту функцию, но и calloc, realloc, _aligned_malloc, _recalloc, strdup и так далее.
Функция malloc возвращает нулевой указатель, если невозможно выделить буфер памяти указанного размера. Поэтому прежде, чем разыменовать указатель, его нужно проверить на равенство NULL.
Почему эта проверка является обязательной, я подробно разобрал в статье "Четыре причины проверять, что вернула функция malloc". Если у вас есть хоть малейшее сомнение, что такая проверка нужна, прошу посмотреть эту статью, прежде чем продолжить чтение.
Классическая проверка выглядит следующим образом:
int *ptr = malloc(sizeof(int) * N);
if (!ptr)
{
// Обработка ошибки выделения памяти
}
Хотя мне больше нравится явный вариант сравнения. Такой стиль дополнительно защищает код от опечаток и делает его чуть понятнее (сразу очевидно, что проверяется указатель):
if (ptr == NULL)
Главное, ошибка выделения памяти должна быть обработана. Это может быть:
Не следует предполагать, что проверку можно пропустить, так как при разыменовании нулевого указателя программа в любом случае аварийно завершится. Она может и не завершиться. Разыменование нулевого указателя — это неопределённое поведение, и его проявления могут быть очень неожиданными для программиста. Про всё это как раз написано в уже упомянутой ранее статье.
Теперь поговорим про этот антипаттерн:
int *ptr = malloc(sizeof(int) * N);
assert(ptr);
memcpy(ptr, foo, sizeof(int) * N);
Для проверки указателя некоторые программисты используют макрос assert. Или его аналоги, например макрос ASSERT из библиотеки MFC.
Важно. Если объявлен макрос NDEBUG, то макрос assert выключается (ничего не делает). Пример реализации из assert.h:
#ifdef NDEBUG
#define assert(condition) ((void)0)
#else
#define assert(condition) /*implementation defined*/
#endif
На практике это означает следующее: в релизной версии программы, которая должна работать максимально быстро, будет объявлен макрос NDEBUG, и assert превратится в "ничто". В результате никакой защиты от нулевого указателя нет, и последствия могут быть весьма неприятными.
В отладочной версии программы assert работает как полагается, но в этом нет практического смысла.
Обычно при тестировании отладочной версии программы используется простые сценарии работы, не подразумевающие выделения больших объемов памяти. Скорее всего не произойдёт и сильной фрагментации памяти. В общем, очень маловероятно, что возникнет ситуация, когда память не сможет быть выделена.
Если же такое всё-таки произойдёт, то assert действительно обнаружит нулевой указатель. Однако для отладочной версии программы в этом мало прока. В ней разыменование нулевого указателя и так сразу проявит себя. Там не будет никаких хитрых проблем с неопределённым поведением, как в оптимизированном коде.
В итоге получается:
А что, если не объявлять макрос NDEBUG для релизных версий?
Плохая идея. Фактически ошибка выделения памяти всегда будет приводить к остановке работы приложения. Такое поведение не уместно для многих типов приложений (особенно библиотек). Более того, на тот же макрос закладываются многие разработчики, чтобы реализовать дополнительные проверки в требующих того местах. Поэтому отсутствие NDEBUG может быть воспринято как баг и исправлено. Не думайте в эту сторону.
Не ленитесь писать полноценные проверки аварийных ситуаций, и пользователи будут вам благодарны. Спасибо за внимание.
Дополнительные ссылки:
0