Советы о том, как писать на С в 2016 году
От автора: Наброски для этой статьи появились еще в начале 2015 года, правда, до публикации материалов дело так и не дошло. Наконец, решив, что в ящике моего письменного стола от вышеупомянутого «черновика» не будет никакой пользы, представляю его вашему вниманию в исходном виде. Единственное, что изменилось в тексте – год, с 2015 на 2016.
И я всегда рад услышать комментарии по поводу необходимых исправлений, уточнений или даже ваши жалобы.
Первое правило программирования на С – не используйте его, если можно обойтись другими инструментами.
Когда найти альтернативный метод не удается, самое время вспомнить о современных заповедях программиста. Язык программирования С известен примерно с начала 1970-х гг. Специалистам приходилось «изучать С» на разных стадиях его эволюции, причем более близкое знакомство нередко приводило в тупик. Так у разных программистов было свое представление о мире С, обусловленное первым опытом применения алгоритмов данного языка.
Столкнувшись с программированием на С, очень важно не застрять на уровне «истин, усвоенных в 80-х/90-х».
Если вы читаете эту статью, скорее всего, вы работаете на современных платформах, придерживаетесь актуальных стандартов и мне не нужно ссылаться на бесконечное множество условностей для старого софта. Бессмысленно увековечивать древние стандарты только потому что отдельные компании не удосужились обновить системы, которым, в лучшем случае, лет 20.
ВведениеСтандарт C99 (здесь C99 – «Стандарт программирования на С 1999 года»; C11 — «Стандарт программирования на С 2011 года», а, значит, 11>99).
- По умолчанию clang использует расширенную версию C11 ( режим GNU C11 ), поэтому дополнительные опции для современных программ не требуются.
- Если вам нужен стандарт C11 , укажите -std=c11; если вы предпочитаете работать со стандартом C99, помечайте - std=c99 .
- clang компилирует исходные файлы быстрее, чем gcc .
- Выбирая gcc , важно указывать -std=c99 или -std=c11
- gcc создает исходные файлы медленнее, чем clang , но иногда генерирует более быстрый код. Показательна сравнительная характеристика производительности и результатов регрессионного тестирования.
- По умолчанию gcc-5 работает в режиме GNU С11 (как и clang ), но если вам нужны именно C11 или C99, опять же придется указать -std=c11 или -std=c99 .
-Os выручает, когда появляются вопросы с производительностью кэш-памяти (и это неспроста).
Предупреждения (Warnings)-Wall -Wextra -pedantic В последних версиях компиляторов предлагается опция -Wpedantic , хотя при необходимости можно обращаться и к древней -pedantic , в частности, для расширения возможностей обратной совместимости.
На этапе тестирования добавьте -Werror и -Wshadow для всех платформ.
Обращение к -Werror может несколько затруднить процесс программирования, так как разные платформы, компиляторы и библиотеки могут выдавать те или иные предупреждения. Не думаю, что вам захочется пренебречь разработками заказчика только потому, что его версия GCC на платформе, с которой вы раньше не сталкивались, атакует все новыми и новыми злобными уведомлениями.
Среди дополнительных забавных опций следует упомянуть Wstrict-overflow -fno-strict-aliasing .
Либо вы включаете -fno-strict-aliasing , либо вы сможете работать с объектами исключительно в том виде, в котором они создавались. Так как программирование на С предполагает применение различных псевдонимов, лучше выбирать -fno-strict-aliasing , если только речь не идет о необходимости контролировать все дерево исходных текстов.
Чтобы Clang не отправлял предупреждения о том, что вы пользуетесь, да-да, подходящим синтаксисом, просто добавьте -Wno-missing-field-initializers .
в GCC 4.7.0 и более поздних версиях данное странное предупреждение устранено.
Разработка Compilation unitsДля разработки проектов на С чаще всего просто выделяют в каждом исходном файла – объектный, а затем компонуют полученные объекты в одно целое. Подобная схема прекрасно подходит для поэтапных разработок, но вряд ли ее можно назвать оптимальной, когда речь идет о производительности и оптимизации. При таком подходе ваш компилятор не распознает необходимость оптимизации, анализируя множество объектных файлов.
LTO — Link Time OptimizationLTO осуществляет «анализ и оптимизацию источника в рамках неполадок с единицами компиляции», создавая аннотации для объектных файлов в виде промежуточных пометок, что позволяет в процессе слияния объектов вносить соответствующие корректировки в исходные данные.
LTO может существенно замедлить процесс слияния. Выручает make -j , но только в том случае, если разработка состоит из самостоятельных, не связанных с друг другом конечных исполнителей (.a, .so, .dylib, исполняемые файлы тестировки, исполняемые приложения и т.д.).
К 2016, clang и gcc позаботились о создании вспомогательной LTO , воспользоваться преимуществами которой вы сможете, добавив -flto в список команд при компиляции объектов и итоговом слиянии элементов библиотеки/программы. Однако за LTO по-прежнему нужен глаз да глаз. Иногда, если в программе применяется код, запускаемый не напрямую, а посредством дополнительных библиотек, LTO может исключить соответствующие функции или код, ведь в ходе общего анализа утилита обнаруживает, что они не используются, а, значит, не нужны в финальной версии продукта.
Arch -march=native Позвольте компилятору задействовать все функции вашего процессора и запомните: тестирование производительности и регрессионное тестирование важны (с последующим сравнительным анализом результатов для различных компиляторов и/или их версий), поскольку с их помощью можно убедиться, что элементы оптимизации не имеют негативных побочных эффектов.
-msse2 и -msse4.2 могут понадобиться, если вы работаете с опциями, подготовленными другими разработчиками.
Создание кода Типы(Types)Если вы обнаружили в новом коде что-то вроде char, int, short, long или unsigned , вот вам и ошибки. В современных программах необходимо указывать #include <stdint.h> и только потом выбирать стандартные типы данных. Подробные описания вы найдете здесь: stdint.h specification. Среди наиболее распространенных стандартных типов данных выделяются следующие:
- int8_t, int16_t, int32_t, int64_t — знаковые целые;
- uint8_t, uint16_t, uint32_t, uint64_t — беззнаковые целые;
- float — 32-битный стандарт с плавающей точкой;
- double — 64-битный стандарт с плавающей точкой.
Разработчики ПО то и дело употребляют команду char для обозначения «байта», даже когда выполняются беззнаковые байтовые операции. Гораздо правильнее для отдельных беззнаковых байтовых/октетных величин указывать uint8_t , а для последовательности беззнаковых байтовых/октетных величин выбирать uint8_t *.
Стоит ли ссылаться на intНекоторые из наших читателей признаются, что просто обожают int , о чем вам поведают их холодные застывшие пальцы. Стоит отметить, что технически невозможно программировать корректно, если размеры типов данных изменяются, как им вздумается.
Также ознакомьтесь с Обоснованием, озвученным в ходе обсуждения inttypes.h: тут недвусмысленно поясняется, почему небезопасно применять типы нефиксированной ширины. Если вы уже подметили, что в процессе разработки на отдельных платформах int 16-битный, на других — 32-битный, а также протестировали проблемные зоны на 16 и 32 битах для каждого случая использования int , можете продолжать в том же духе.
Остальным же, кто еще не освоил премудрость удерживания в голове целых комплексов технических условий для платформ с многоуровневой структурой при выполнении очередной головоломки, советую остановиться на типах фиксированной ширины, что автоматически позволит писать более правильный код с заметно меньшим количеством концептуальных погрешностей, для тестирования которого не потребуются дополнительные усилия. Или, как кратко говорится в описании: «правило ISO C по продвижению стандартных целочисленных данных может привести к совершенно неожиданным изменениям».
Без удачи тут не обойтись.
Исключение из правила «никогда не используйте char »Единственный случай, когда в 2016 году можно обращаться к команде char , это, если выбранный API запрашивает char (например, strncat, printf'ing "%s", . ) или если вы задаете строки исключительно для чтения (например, const char *hello = "hello"; ), потому что на языке программирования С строковые литералы («hello») выглядят, как char []. КРОМЕ ТОГО: В С11 предусмотрена поддержка родного unicode, а для UTF-8 строковых литералов по-прежнему используется char , даже если приходится работать с мультибайтовыми последовательностями вроде const char *abcgrr = u8"abc"; .
Исключение из правила «никогда не используйте »Если вы обращаетесь к функциям с типами результата или родными параметрами, используйте типы в соответствии с классом функции или характеристиками API.
Знаковость (Signedness)Не вздумайте использовать unsigned в вашем коде. Теперь вы знаете, как написать приличный код без несуразных условностей C с многочисленными типами данных, которые не только делают содержание нечитабельным, но и ставят под вопрос эффективность использования готового продукта. Кому захочется вводить unsigned long long int , если можно ограничиться простым uint64_t ? Файлы типа <stdint.h> куда конкретнее и точнее по смыслу, они лучше передают намерения автора, компактны – что немаловажно и для эксплуатации, и для читабельности.
Целочисленные указателиВозможно, кто-то из вас возразит: «Но как же без указателей для long , без них же вся математика накроется!»
Вы, конечно, можете и такое заявить, но кто говорит, что утверждение истинно?
Правильный тип для указателей в данном случае — uintptr_t , он задается файлами <stdint.h> . В то же время важно отметить, что весьма полезный ptrdiff_t определяется stddef.h .
Вместо: long diff = (long)ptrOld - (long)ptrNew;
Используйте: ptrdiff_t diff = (uintptr_t)ptrOld - (uintptr_t)ptrNew;
А также: printf("%p is unaligned by %" PRIuPTR " bytes.\n", (void *)p, ((uintptr_t)somePtr & (sizeof(void *) - 1)));
Системно-зависимые типы данныхВы все еще спорите, будто «на 32-битной платформе мне нужны 32-битные long , а на 64-ной — 64-битные!».
Если опустить рассуждения, в ходе которых вы явно затрудняетесь объяснить причину использования в коде двух разных размеров в зависимости от платформы, думаю, в итоге вам все же не захочется зацикливаться на long , ориентированных на системно-зависимые типы данных.
В подобных ситуациях рационально обращаться к intptr_t – целочисленному типу данных, отвечающему за хранение значения указателя для вашей платформы.
На современных 32-битных платформах intptr_t трансформируется в int32_t .
На современных 64-битных платформах intptr_t приобретает вид int64_t .
Также intptr_t встречается в варианте uintptr_t .
Для хранения информации о смещении указателя используйте ptrdiff_t – именно этот тип данных позволяет запоминать параметры вычитаемых указателей.
Максимальное значение величинВы ищете целочисленный тип данных, способный обрабатывать любые целые значения в вашей системе?
Как правило, программисты предпочитают самые известные альтернативы, в частности, неказистый uint64_t , а ведь есть более эффективное техническое решение, благодаря которому любая переменная может применяться для хранения всевозможных значений. Безопасное хранение целочисленных данных гарантирует intmax_t (или uintmax_t ). Вы можете доверить любую знаковую величину intmax_t , будучи уверенными, что точность данных от этого не пострадает. Аналогично и с беззнаковыми целыми, делегированными uintmax_t .
Другой тип данныхЕсли мы говорим о широко распространённых системно-зависимых типах данных, size_t , гарантируемый stddef.h занимает первое место в списке фаворитов.
По сути, size_t – что-то вроде «целой величины, способной хранить огромные индексы массива», а, значит, ему под силу фиксировать внушительные показатели смещения в создаваемой программе.
На практике size_t выступает в роли типа результата для оператора sizeof.
В любом случае на современных платформах size_t обладает, практически, теми же характеристиками, что и uintptr_t , а потому на 32-битных версиях size_t трансформируется в uint32_t , а на 64-битных – в uint64_t .
Существует также ssize_t , который представляет собой знаковый size_t , используемый в качестве типа результата для функций библиотеки – в случае ошибки получаем 1. (Примечание: ssize_t принадлежит пакету POSIX и не подходит для Windows).
Так стоит ли задействовать size_t для произвольных системно-зависимых размеров, задавая параметры собственных функций? Технически, size_t – тип результата sizeof , поэтому любые функции, определяющие размер величины в виде конкретного количества байтов, могут принимать вид size_t .
Другие области применения: size_t — тип аргумента для функции malloc, а ssize_t – тип результата для read() и write() (за исключением интерфейсов Windows, в которых ssize_t не предусмотрен и для значений результата применяется только int).
Типы вывода данных на печать (Printing Types)Не ссылайтесь на типы данных во время печати. Всегда используйте соответствующие указатели типа, как советуют на inttypes.h.
- size_t — %zu
- ssize_t — %zd
- ptrdiff_t — %td
- исходное значение указателя — %p (в современных компиляторах отображается в шестнадцатеричной системе; изначально отсылает указатель к void *)
- int64_t — "%" PRId64
- uint64_t — "%" PRIu64
64-битные типы данных печатаем, используя только макрос стиля PRI[udixXo]64. Почему?
На некоторых платформах 64-битные значения представлены функцией long , на других — long long .Эти макросы обеспечивают оптимальные базовые характеристики формата для различных платформ.
Без данных макросов формата, практически, невозможно создать форматирующую строку, подходящую одновременно для всех платформ, так как типы данных изменяются, независимо от ваших действий (и помните, задавать вышеупомянутые значения до начала печати не только не безопасно, но и нелогично).
intptr_t — "%" PRIdPTR uintptr_t — "%" PRIuPTR intmax_t — "%" PRIdMAX uintmax_t — "%" PRIuMAX
Одно дополнение касательно спецификаторов формата PRI*: это макросы, причем в зависимости от конкретной платформы они расширяются до подходящих спецификаторов класса printf. А, значит, нельзя указывать:
printf("Local number: %PRIdPTR\n\n", someIntPtr);
Вместо этого, зная, что мы имеем дело с макросами, пишем:
printf("Local number: %" PRIdPTR "\n\n", someIntPtr);
Обратите внимание: % попадает в тело литерала форматирующей строки, в то время как указатель типа остается за его пределами, поскольку все соседние строки объединяются препроцессором в одном итоговом комбинированном строковом литерале.
С99 позволяет использовать описания переменных где угодно.
Мы НЕ делаем так:
Вместо этого пишем следующим образом:
Предупреждение: если циклы программы ограничены, проверьте позиции инициализаторов. Иногда несистематизированные описания приводят к неожиданному снижению скорости работы. Для обычного, не ускоренного, кода (который, собственно, и используется в большинстве случаев) лучше всего делать акцент на четкости. Так, определив типы данных сразу же после завершения работы над инициализаторами, вы заметно повысите читабельность.
В С99 можно использовать циклы for для создания встроенных описаний счетчиков.
Никогда НЕ пишите:
Одно исключение: если вы хотите сохранить значение вашего счетчика после выхода из цикла, разумеется, не стоит вставлять соответствующее описание в тело цикла.
Современные компиляторы поддерживают #pragma once.
Вместо него используем #pragma once
#pragma once уведомляет компилятор о необходимости запросить заголовок всего один раз, следовательно, вам больше не придется писать дополнительные строки для его защиты. Данная функция поддерживается всеми компиляторами, причем на различных платформах, и является куда более эффективным механизмом, чем ввод кода заголовка вручную. Подробное описание опции вы найдете в перечне компиляторов, поддерживающих pragma once.
Язык программирования С позволяет проводить статическую инициализацию автоматически созданных массивов.
Итак, мы не пишем:
Работая на С, вы можете осуществлять статическую инициализацию автоматически генерируемых структур.
ВАЖНО: Если в вашей структуре предусмотрено внутреннее выравнивание, метод не обнулит дополнительные байты, предназначенные для этих целей. Так, например, происходит, если в struct thing 4 байта отступов после counter (на 64-битной платформе), потому что структуры заполняются с шагом равным одному слову. Если вам нужно обнулить всю структуру включая неиспользованные байты отступов, указывайте memset(&localThing, 0, sizeof(localThing)) , так как sizeof(localThing) == 16 bytes, несмотря на то, что доступно всего 8 + 4 = 12 байтов.
Если потребуется повторно инициализировать ранее выделенные структуры, используйте общую нулевую структуру для последующего определения значений:
Если вам повезло работать на C99 (или более поздних версиях), вы можете выбирать составные литералы вместо того, чтобы возиться с основной «нулевой структурой» (см. статью The New C: Compound Literals за 2001 год).
Составные литералы позволяют компилятору автоматически создавать временные анонимные структуры, а затем копировать их в соответствующее поле значения: localThing = (struct thing);
В С99 появились массивы переменной длины (в С11 их можно выбирать по желанию).
Поэтому НЕ пишите так (если вы имеете дело с миниатюрным массивом или просто проводите экспресс-тестирование):
Вместо этого указываем:
ВАЖНО: массивы переменной длины (как правило) создаются в стеке, как и обычные массивы. Если у вас не получается создать обычный массив на 3 миллиона элементов статически, не пытайтесь генерировать динамический массив того же объема, используя данный синтаксис. Это вам не масштабируемые автоматические списки Python/Ruby. Если задать длину массива во время запуска программы и она окажется слишком большой для вашего стека, начнется бардак (сбои в работе, проблемы с безопасностью). Массивы переменной длины идеальны для отдельных ситуаций, рассчитанных на выполнение конкретных задач, но не следует использовать их для разработки всех видов программного обеспечения. Если один раз вам понадобилось генерировать массив на 3 элемента, а другой – на 3 миллиона, вряд ли стоит прибегать к помощи массивов переменной длины.
Да, неплохо разбираться в синтаксисе VLA, зная, что он может вам пригодиться (или если нужно провести однократное экспресс-тестирование какого-то продукта). В то же время подобные затеи нередко превращаются в трагедии, когда рушатся целые программы, стоит только забыть точные параметры проверки размеров элемента или упустить из виду, что вы столкнулись с незнакомой целевой платформой, на которой не предусмотрено дополнительное стековое пространство.
ПРИМЕЧАНИЕ: Не сомневайтесь в том, что в данной ситуации arrayLength – оптимальный размер (то есть меньше нескольких килобайт; иногда ваш стек будет достигать максимум 4 КБ на малоизвестных платформах). Вы не сможете создавать огромные массивы (на миллионы записей), но, зная, что в вашем распоряжении ограниченное пространство, гораздо проще использовать возможности С99 VLA, а не вручную готовить запросы в динамическую память с помощью malloc .
И ЕЩЕ: в этом механизме отсутствует функция проверки данных, введенных пользователем, а потому вы можете просто уничтожить программу, выделив несоразмерный VLA. Кто-то и вовсе окрестил VLA антишаблоном, но, если соблюдать необходимые правила предосторожности, в отдельных случаях подобного рода массивы, несомненно, окажутся кстати.
C99 позволяет писать аннотации к непересекающимся параметрам указателей.
Типы параметровЕсли функция работает с произвольными исходными данными и определенной длиной, не ограничивайте тип этого параметра.
Вместо этого используйте:
Типы исходных данных ваших функций описывают интерфейс кода, а не манипуляции кода с параметрами. Вышеупомянутый интерфейс подразумевает процесс «принятия байтового массива и определенной длины», а потому вам не захочется ограничивать пользователей потоками uint8_t . Хотя, возможно, ваши клиенты захотят познакомиться поближе с такими древностями, как char * , или чем-то более оригинальным. Объявив тип исходных данных, как void * , и повторно назначив или еще раз сославшись на фактический тип данных, который нужен прямо в теле функции, вы обезопасите пользователей, ведь так им не придется думать о том, что происходит в вашей библиотеке.
В этом примере некоторые читатели столкнулись с проблемой выравнивания, но, поскольку мы работаем только с отдельными байтовыми элементами ввода, все должно быть в порядке. Если же нужно будет сосредоточиться на более крупных величинах, действительно, придется учитывать выравнивание. Созданию кросс-платформенного кода с учетом выравнивания контента посвящена статья Unaligned Memory Access (напоминаем: это ресурс с общими обзорами не специализируется на тонкостях программирования на С под разные архитектуры, а потому все описанные примеры, желательно, применять с учетом личного опыта и имеющихся знаний).
Типы возвращаемых параметровC99 предоставляет нам весь набор функций <stdbool.h> , где true равняется 1, а false — 0. В случае с удачными/неудачными возвращаемыми значениями функции должны выдавать true or false, а не возвращаемый тип int32_t , требующий ручного ввода 1 и 0 (или, что еще хуже, 1 и -1; как тогда разобраться: 0 – success, а 1 — failure? Или 0 – success, а -1 — failure?).
Если функция произвольно изменяет исходные значения вплоть до признания их недействительными, вместо того, чтобы выдавать обработанный указатель, ваш API будет классифицировать любые двойные указатели, упомянутые в качестве исходных данных, как некорректные. Кроме того, программисты часто совершают одну и ту же ошибку, используя в коде установку, при которой «для некоторых вызовов, возвращаемое значение признает исходные данные недействительными».
Поэтому НЕ пишем так:
Или, если совсем постараться, указываем следующее:
ФорматированиеСтандарт оформления кода одновременно и важен, и совершенно бесполезен.
Если для вашего проекта будет подготовлено руководство на 50 страниц с правилами оформления кода, вряд ли кто-то захочет с вами связываться. Но если созданный код нечитабелен, никто даже не подумает вам помогать.
Совет – всегда используйте автоматические форматтеры кода.
Единственный продукт, который в 2016 году позволит форматировать продукты, разработанные на языке С, — clang-format. Родные настройки clang-format на порядок выше любого другого автоматического форматтера C-кода. Более того, его разработчики постоянно трудятся над новыми функциями продукта.
Я привык использовать следующий скрипт для clang-format: #!/usr/bin/env bash
Вызывайте команду так (если вы присвоили скрипту имя cleanup-format ): matt@foo:
/repos/badcode% cleanup-format -i *.
Опция -i не сохраняет новые файлы и не создает их резервные копии, а перезаписывает существующие файлы вместе с результатами форматирования.
Если у вас много файлов, можно параллельно рекурсивно обработать все дерево исходного кода:
Кроме того, хочется поделиться с вами и новым скриптом cleanup-tidy. Он выглядит, примерно, так:
clang-tidy — инструмент рефакторинга кода. Вышеупомянутые характеристики позволяют решить две задачи:
readability-braces-around-statements – все формулировки с if/while/for заключаются в фигурные скобки;
Не верится, что при программировании на С вдруг появляются одиночные команды в «дополнительных скобках» после конструкций цикла и условных операторов. Просто непозволительно писать современные коды без обязательных скобок для каждого цикла и условия. Если вы поспешите заметить, мол, «но компилятор же принимает такие команды!», уверяю – это не имеет ничего общего с читабельностью, удобством в эксплуатации или возможностью экспресс-тестирования кода. Вы же занимаетесь программированием не для того, чтобы угодить компилятору, а оставляете наследие для будущих поколений, которые смогут понять ход ваших мыслей, даже если все забудут, по какой схеме программы разрабатывались раньше.
misc-macro-parentheses – автоматически добавляются скобки вокруг всех значений, перечисленных в теле макроса.
clang-tidy – отличный инструмент, если, конечно, работает исправно, но при создании сложного кода с ним можно и запутаться. Кроме того, clang-tidy — не является форматом, а потому не забудьте о clang-format после того, как расставите скобки и отформатируете макросы.
ЧитабельностьЗдесь писать, особо, нечего…
КомментарииСледует проанализировать логические автономные части файла кода.
Структура файлаПостарайтесь ограничить файлы максимум 1000 строк (1500 строк в крайнем случае). Если проводимые тесты запрашивают исходный файл (для тестирования статических функций и т.д.), при необходимости отредактируйте его.
Мысли о разном Никогда не используйте mallocПривыкайте к calloc . С этой функцией вам не грозит снижение производительности при очистке памяти. Если вам не по душе calloc(object count, size per object) , можете заменить ее на #define mycalloc(N) calloc(1, N) .
По данному пункту у читателей появилось несколько идей:
- сalloc сказывается на производительности, если мы имеем дело с огромными массивами данных;
- calloc сказывается на производительности программ, работающих на странных платформах (минимальные встраиваемые системы, игровые консоли, аппаратное обеспечение 30-летней давности, и т.п.);
- преобразование функции в calloc(element count, size of each element)не самое удачное решение;
- Среди явных недостатков malloc() можно отметить то, что функция не позволяет проверить переполнение размера целых переменных, что создает потенциальную угрозу безопасности;
- Использование calloc блокирует функцию инструмента valgrind, благодаря которой пользователь получает уведомления о непреднамеренном чтении/копировании данных из неинициализированной памяти, ведь применение calloc автоматически приравнивается 0;
- Перечисленные пункты – отличные дополнения. И именно поэтому важно всегда анализировать производительность, проводить тестирование производительности и регрессионное тестирование скорости для различных компиляторов, платформ, операционных систем и аппаратных средств;
- Одно из преимуществ использования calloc () в чистом виде, без упаковщика, в отличие от malloc(), calloc(), — с его помощью можно проверить переполнение размера целых переменных, так как в этом случае функция умножает все переменные для того, чтобы вычислить конечный размер кластера. Если вы работаете с небольшими объемами данных, calloc() показывает отличные результаты и в тандеме с упаковщиком. Когда же речь идет о потенциально неограниченном потоке информации, можно остановиться и на привычном вызове calloc(element count, size of each element).
Не бывает универсальных советов, но попытка сформулировать почти идеальные общие рекомендации в итоге, наверняка, закончится написанием целой книги о тонкостях конкретного языка программирования.
Если вам интересно, как с помощью calloc() бесплатно освободить дополнительную память, почитайте эти интересные статьи:
В 2016 году я по-прежнему настаиваю на рекомендации всегда использовать calloc() для большинства привычных сценариев (в частности, для х64 целевых платформ, для данных, касающихся конкретного человека, если мы не говорим об особенностях генома). Любые отклонения от «сферы ожидаемого» грозят отчаянием, вызванным сомнениями на тему «знания предмета», но не стоит об этом.
Дополнение: предварительно очищенная с помощью calloc() память – прекрасный результат, но это разовая акция. Если после calloc() вы запросите realloc() , в итоге не будет дополнительного объема чистой памяти. Перераспределенное пространство заполнит всевозможный стандартный неинициализированный контент в зависимости от возможностей ядра системы. Если вы хотите освободить место после работы с realloc() , придется вручную запрашивать memset() .
Никогда не используйте memset (если можно обойтись без него)Не спешите ссылаться на memset (ptr, 0, len, когда можно статически задать для структуры (массива) нулевое исходное значение (или обнулить необходимые показатели, обратившись к встроенному составному литералу или к значению общей нулевой структуры). В то же время memset() — ваш единственный выбор, если нужно обнулить структуру, включая байты внутренних отступов (так как функция распространяется только на определенные участки и игнорирует неопределенные области, отведенные под отступы).
ЗаключениеНайти универсальную формулу для написания правильного кода, практически, невозможно. Мы имеем дело с множеством операционных систем, различным временем автономной работы программ, всевозможными библиотеками и платформами, что только увеличивает список поводов для беспокойства, даже, если мы рассматриваем случайное инвертирование битов RAM или когда наши фильтры вдруг начинают «врать».
Лучшее, что мы можем сделать — это писать простой, понятный код, в котором количество возможных сбоев и неожиданных форс-мажоров сведено к минимуму.