Функция malloc является одной из главных функций динамического выделения памяти в языке программирования C и неизменно вызывает интерес у разработчиков и исследователей программирования. Несмотря на кажущуюся простоту, в её поведении заключены некоторые тонкости и особенности, которые часто удивляют даже опытных программистов. Особенно спорным остаётся вопрос поведения malloc при запросе нулевого объёма памяти, то есть malloc(0). В некоторых реализациях она возвращает валидный указатель, в других — NULL. Почему так происходит, какие причины этому предшествовали, и как эта особенность влияет на написание кода? Попробуем разобраться в истории и деталях.
Стандарт языка C изначально определял поведение malloc таким образом, что вызов malloc с параметром 0 не запрещался и не исключался, однако конкретный результат мог отличаться. Стандарт просто указывает, что в случае запроса выделения нулевого размера может быть возвращён NULL или указатель на объект, который не обязательно доступен для использования, кроме как для освобождения памяти. Такой неконкретный стандарт создаёт существенную неоднозначность, которую разные реализации решают по-разному. Исторически malloc(0), возвращая NULL, казался сомнительным решением, ведь возвращение NULL часто интерпретируется как ошибка выделения памяти. Для программиста это усложняет обработку возвращаемого значения, так как необходимо различать случаи нулевого запроса от реальной ошибки.
Другая стратегия — возвращать валидный указатель, который нельзя разыменовывать, но который можно передать функции free для корректного освобождения памяти. Многие современные реализации malloc обязаны следовать именно такому поведению, чтобы обеспечить более предсказуемую работу. Однако в разных системах ситуация была иной. Особенно выделяется Unix System V и его «fast malloc» библиотека, которая при вызове malloc(0) возвращала именно NULL. Это было решением, продиктованным экономией памяти — на системах с небольшим объёмом оперативной памяти целесообразно отказаться от выделения «пустых» областей, чтобы не тратить драгоценные ресурсы напрасно, особенно если вызовов malloc(0) в программах много.
Стоит отметить, что сама спецификация System V Interface Definition (SVID), публикуемая AT&T, требовала именно такую реализацию — возвращать NULL при выделении нулевого размера. Несмотря на то, что официальные релизы System V могли вести себя иначе, это требование отражалось в документации и частично влиялось на последующие реализации. Примером может служить платформа AIX от IBM, где malloc(0) возвращал NULL. Распространено мнение, что именно использование fast malloc из System V привело к такому поведению в AIX, что со временем повлияло и на POSIX — стандарт, ориентированный на совместимость с Unix-подобными системами. Интересно, что среди не-Unix систем и компиляторов поведение malloc(0) тоже было разным.
Многие ранние C-компиляторы для персональных компьютеров с ограниченными ресурсами и без полноценной поддержки Unix ориентировались на свои собственные стандарты библиотек. Whitesmiths C, как правило, округлял нулевые запросы в malloc до минимального ненулевого размера, возвращая валидный указатель. Аналогичная практика наблюдалась и в других системах. Особым случаем можно считать компилятор Manx Aztec C для MS-DOS. Его версия 5.
2a, судя по обзорам и информации, позволяла malloc(0) возвращать NULL, что соответствовало подходу экономии ресурсов и снижению количества выделений пустой памяти. В более ранних версиях этого компилятора присутствовала более простая реализация malloc, которая всегда округляла размер и возвращала стандартный указатель. Такое разнообразие поведения в разных системах порождает ряд проблем при переносе кода с одной платформы на другую. Программисты вынуждены учитывать, что malloc(0) может возвращать NULL не из-за ошибки, а из-за особенностей реализации, и корректно обрабатывать этот случай. В противном случае риск возникновения непредвиденных ошибок, включая аварийные завершения или утечки памяти, существенно возрастает.
Важно понимать, что выделение нулевого размера памяти в принципе не имеет смысла с точки зрения логики работы программ, но встречается в ситуациях, когда размеры объектов вычисляются динамически и могут оказаться равными нулю. Обычно это происходит в обобщённых структурах данных, когда входные параметры или конфигурации вынуждают выполнить вызов malloc с нулевым аргументом. Поэтому правильная обработка результата является критичной для стабильности программ. С точки зрения современных стандартов POSIX и C, реализация malloc должна обеспечивать определённую предсказуемость. Возвращать ненулевой указатель или валидный дожидаться выделения памяти, даже если она технически не нужна, — лучший вариант для переносимости и безопасности кода.
Это позволяет программистам писать универсальные функции, не боясь сталкиваться с двусмысленностью возврата NULL. Значительную роль в развитии этой темы сыграли исследования и публикации независимых специалистов и сообществ. Анализ множества Unix malloc-реализаций показал, что лишь относительно мало систем придерживались возвращения NULL в случае malloc(0). Такая практика оставалась исключительной и в основном относилась к специфическим вариантам систем. В свою очередь, менее известные не-Unix платформы могли иметь иные модели поведения, поскольку их разработчики преследовали свои собственные цели — максимально эффективное использование ограниченных ресурсов или совместимость с существующими библиотеками.