Если бы язык С был оружием
От автора: Наброски для этой статьи появились еще в начале 2015 года, правда, до публикации материалов дело так и не дошло. Наконец, решив, что в ящике моего письменного стола от вышеупомянутого «черновика» не будет никакой пользы, представляю его вашему вниманию в исходном виде. Единственное, что изменилось в тексте – год, с 2015 на 2016.
И я всегда рад услышать комментарии по поводу необходимых исправлений, уточнений или даже ваши жалобы.
Итак, статья ...
Первое правило программирования на С – не используйте его, если можно обойтись другими инструментами.
Когда найти альтернативный метод не удается, самое время вспомнить о современных заповедях программиста.
Язык программирования С известен примерно с начала 1970-х гг. Специалистам приходилось «изучать С» на разных стадиях его эволюции, причем более близкое знакомство нередко приводило в тупик. Так у разных программистов было свое представление о мире С, обусловленное первым опытом применения алгоритмов данного языка.
Столкнувшись с программированием на С, очень важно не застрять на уровне «истин, усвоенных в 80-х/90-х».
Если вы читаете эту статью, скорее всего, вы работаете на современных платформах, придерживаетесь актуальных стандартов и мне не нужно ссылаться на бесконечное множество условностей для старого софта. Бессмысленно увековечивать древние стандарты только потому что отдельные компании не удосужились обновить системы, которым, в лучшем случае, лет 20.
Введение
Стандарт C99 (здесь C99 – «Стандарт программирования на С 1999 года»; C11 — «Стандарт программирования на С 2011 года», а, значит, 11>99).
clang, default
- По умолчанию 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
.
Оптимизация
-O2, -О3.
Обычно вам подходит
-O2
, но иногда нужен -O3
.Протестируйте оба варианта (в том числе и для разных компиляторов), а затем сохраните самые эффективные исполняемые файлы.— Os
-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 Optimization
LTO
осуществляет «анализ и оптимизацию источника в рамках неполадок с единицами компиляции», создавая аннотации для объектных файлов в виде промежуточных пометок, что позволяет в процессе слияния объектов вносить соответствующие корректировки в исходные данные.LTO
может существенно замедлить процесс слияния. Выручает make -j
, но только в том случае, если разработка состоит из самостоятельных, не связанных с друг другом конечных исполнителей (.a, .so, .dylib, исполняемые файлы тестировки, исполняемые приложения и т.д.).clang LTO.
GCC LTO.
К 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
. Обычно на языке программирования С команду char
не только называют, но и используют неправильно.Разработчики ПО то и дело употребляют команду
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";
.Исключение из правила «никогда не используйте {int,long,etc}
»
Если вы обращаетесь к функциям с типами результата или родными параметрами, используйте типы в соответствии с классом функции или характеристиками 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
— "%" PRIdPTRuintptr_t
— "%" PRIuPTRintmax_t
— "%" PRIdMAXuintmax_t
— "%" PRIuMAXОдно дополнение касательно спецификаторов формата PRI*: это макросы, причем в зависимости от конкретной платформы они расширяются до подходящих спецификаторов класса printf. А, значит, нельзя указывать:
printf("Local number: %PRIdPTR\n\n", someIntPtr);
Вместо этого, зная, что мы имеем дело с макросами, пишем:
printf("Local number: %" PRIdPTR "\n\n", someIntPtr);
Обратите внимание: % попадает в тело литерала форматирующей строки, в то время как указатель типа остается за его пределами, поскольку все соседние строки объединяются препроцессором в одном итоговом комбинированном строковом литерале.
С99 позволяет использовать описания переменных где угодно.
Мы НЕ делаем так:
void test(uint8_t input) {
uint32_t b;
if (input > 3) {
return;
}
b = input;
}
Вместо этого пишем следующим образом:
void test(uint8_t input) {
if (input > 3) {
return;
}
uint32_t b = input;
}
Предупреждение: если циклы программы ограничены, проверьте позиции инициализаторов. Иногда несистематизированные описания приводят к неожиданному снижению скорости работы. Для обычного, не ускоренного, кода (который, собственно, и используется в большинстве случаев) лучше всего делать акцент на четкости. Так, определив типы данных сразу же после завершения работы над инициализаторами, вы заметно повысите читабельность.
В С99 можно использовать циклы for для создания встроенных описаний счетчиков.
Никогда НЕ пишите:
uint32_t i;
for (i = 0; i < 10; i++)
Правильно будет:
for (uint32_t i = 0; i < 10; i++)
Одно исключение: если вы хотите сохранить значение вашего счетчика после выхода из цикла, разумеется, не стоит вставлять соответствующее описание в тело цикла.
Современные компиляторы поддерживают #pragma once.
НЕПРАВИЛЬНЫЙ вариант:
#ifndef PROJECT_HEADERNAME
#define PROJECT_HEADERNAME
.
.
.
#endif /* PROJECT_HEADERNAME */
Вместо него используем
#pragma once
#pragma once
уведомляет компилятор о необходимости запросить заголовок всего один раз, следовательно, вам больше не придется писать дополнительные строки для его защиты. Данная функция поддерживается всеми компиляторами, причем на различных платформах, и является куда более эффективным механизмом, чем ввод кода заголовка вручную.Подробное описание опции вы найдете в перечне компиляторов, поддерживающих pragma once.
Язык программирования С позволяет проводить статическую инициализацию автоматически созданных массивов.
Итак, мы не пишем:
uint32_t numbers[64];
memset(numbers, 0, sizeof(numbers));
Правильно будет:
uint32_t numbers[64] = {0};
Работая на С, вы можете осуществлять статическую инициализацию автоматически генерируемых структур.
Классическая ошибка:
struct thing {
uint64_t index;
uint32_t counter;
};
struct thing localThing;
void initThing(void) {
memset(&localThing, 0, sizeof(localThing));
}
Корректно:
struct thing {
uint64_t index;
uint32_t counter;
};
struct thing localThing = {0};
ВАЖНО: Если в вашей структуре предусмотрено внутреннее выравнивание, {0} метод не обнулит дополнительные байты, предназначенные для этих целей. Так, например, происходит, если в struct thing 4 байта отступов после counter (на 64-битной платформе), потому что структуры заполняются с шагом равным одному слову. Если вам нужно обнулить всю структуру включая неиспользованные байты отступов, указывайте
memset(&localThing, 0, sizeof(localThing))
, так как sizeof(localThing) == 16 bytes, несмотря на то, что доступно всего 8 + 4 = 12 байтов.Если потребуется повторно инициализировать ранее выделенные структуры, используйте общую нулевую структуру для последующего определения значений:
struct thing {
uint64_t index;
uint32_t counter;
};
static const struct thing localThingNull = {0};
.
.
.
struct thing localThing = {.counter = 3};
.
.
.
localThing = localThingNull;
Если вам повезло работать на C99 (или более поздних версиях), вы можете выбирать составные литералы вместо того, чтобы возиться с основной «нулевой структурой» (см. статью The New C: Compound Literals за 2001 год).
Составные литералы позволяют компилятору автоматически создавать временные анонимные структуры, а затем копировать их в соответствующее поле значения:
localThing = (struct thing){0};
В С99 появились массивы переменной длины (в С11 их можно выбирать по желанию).
Поэтому НЕ пишите так (если вы имеете дело с миниатюрным массивом или просто проводите экспресс-тестирование):
uintmax_t arrayLength = strtoumax(argv[1], NULL, 10);
void *array[];
array = malloc(sizeof(*array) * arrayLength);
/ * Не забудьте освободить (массив)после завершения работы над ним * /
Вместо этого указываем:
uintmax_t arrayLength = strtoumax(argv[1], NULL, 10);
void *array[arrayLength];
/* не нужно освобождать массив */
ВАЖНО: массивы переменной длины (как правило) создаются в стеке, как и обычные массивы. Если у вас не получается создать обычный массив на 3 миллиона элементов статически, не пытайтесь генерировать динамический массив того же объема, используя данный синтаксис. Это вам не масштабируемые автоматические списки Python/Ruby. Если задать длину массива во время запуска программы и она окажется слишком большой для вашего стека, начнется бардак (сбои в работе, проблемы с безопасностью). Массивы переменной длины идеальны для отдельных ситуаций, рассчитанных на выполнение конкретных задач, но не следует использовать их для разработки всех видов программного обеспечения. Если один раз вам понадобилось генерировать массив на 3 элемента, а другой – на 3 миллиона, вряд ли стоит прибегать к помощи массивов переменной длины.
Да, неплохо разбираться в синтаксисе VLA, зная, что он может вам пригодиться (или если нужно провести однократное экспресс-тестирование какого-то продукта). В то же время подобные затеи нередко превращаются в трагедии, когда рушатся целые программы, стоит только забыть точные параметры проверки размеров элемента или упустить из виду, что вы столкнулись с незнакомой целевой платформой, на которой не предусмотрено дополнительное стековое пространство.
ПРИМЕЧАНИЕ: Не сомневайтесь в том, что в данной ситуации
arrayLength
– оптимальный размер (то есть меньше нескольких килобайт; иногда ваш стек будет достигать максимум 4 КБ на малоизвестных платформах). Вы не сможете создавать огромные массивы (на миллионы записей), но, зная, что в вашем распоряжении ограниченное пространство, гораздо проще использовать возможности С99 VLA, а не вручную готовить запросы в динамическую память с помощью malloc
.И ЕЩЕ: в этом механизме отсутствует функция проверки данных, введенных пользователем, а потому вы можете просто уничтожить программу, выделив несоразмерный VLA. Кто-то и вовсе окрестил VLA антишаблоном, но, если соблюдать необходимые правила предосторожности, в отдельных случаях подобного рода массивы, несомненно, окажутся кстати.
C99 позволяет писать аннотации к непересекающимся параметрам указателей.
Типы параметров
Если функция работает с произвольными исходными данными и определенной длиной, не ограничивайте тип этого параметра.
Заведомо ошибочно:
void processAddBytesOverflow(uint8_t *bytes, uint32_t len) {
for (uint32_t i = 0; i < len; i++) {
bytes[0] += bytes[i];
}
}
Вместо этого используйте:
void processAddBytesOverflow(void *input, uint32_t len) {
uint8_t *bytes = input;
for (uint32_t i = 0; i < len; i++) {
bytes[0] += bytes[i];
}
}
Типы исходных данных ваших функций описывают интерфейс кода, а не манипуляции кода с параметрами. Вышеупомянутый интерфейс подразумевает процесс «принятия байтового массива и определенной длины», а потому вам не захочется ограничивать пользователей потоками
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 будет классифицировать любые двойные указатели, упомянутые в качестве исходных данных, как некорректные. Кроме того, программисты часто совершают одну и ту же ошибку, используя в коде установку, при которой «для некоторых вызовов, возвращаемое значение признает исходные данные недействительными».
Поэтому НЕ пишем так:
void *growthOptional(void *grow, size_t currentLen, size_t newLen) {
if (newLen > currentLen) {
void *newGrow = realloc(grow, newLen);
if (newGrow) {
/* размер успешно изменен */
grow = newGrow;
} else {
/* отказ в изменении размера, функция работает в свободном режиме, сигнал не проходит через ноль */
free(grow);
grow = NULL;
}
}
return grow;
}
Лучше иначе:
/* Возвращаемое значение:
* - 'true' если newLen > currentLen и стремится к увеличению
* - в данном случае 'true' не указывает на успешный результат, о нем может свидетельствовать только '*_grow'
* - 'false' если newLen <= currentLen */
bool growthOptional(void **_grow, size_t currentLen, size_t newLen) {
void *grow = *_grow;
if (newLen > currentLen) {
void *newGrow = realloc(grow, newLen);
if (newGrow) {
/* размер успешно изменен */
*_grow = newGrow;
return true;
}
/* отказ в изменении размера */
free(grow);
*_grow = NULL;
/* для данной функции,
* 'true' не указывает на успешный результат, а лишь подчеркивает стремление к увеличению */
return true;
}
return false;
}
Или, если совсем постараться, указываем следующее:
typedef enum growthResult {
GROWTH_RESULT_SUCCESS = 1,
GROWTH_RESULT_FAILURE_GROW_NOT_NECESSARY,
GROWTH_RESULT_FAILURE_ALLOCATION_FAILED
} growthResult;
growthResult growthOptional(void **_grow, size_t currentLen, size_t newLen) {
void *grow = *_grow;
if (newLen > currentLen) {
void *newGrow = realloc(grow, newLen);
if (newGrow) {
/* размер успешно изменен */
*_grow = newGrow;
return GROWTH_RESULT_SUCCESS;
}
/* отказ в изменении размера, не удаляйте данные, так как они могут понадобится для уведомления об ошибке */
return GROWTH_RESULT_FAILURE_ALLOCATION_FAILED;
}
return GROWTH_RESULT_FAILURE_GROW_NOT_NECESSARY;
}
Форматирование
Стандарт оформления кода одновременно и важен, и совершенно бесполезен.
Если для вашего проекта будет подготовлено руководство на 50 страниц с правилами оформления кода, вряд ли кто-то захочет с вами связываться. Но если созданный код нечитабелен, никто даже не подумает вам помогать.
Совет – всегда используйте автоматические форматтеры кода.
Единственный продукт, который в 2016 году позволит форматировать продукты, разработанные на языке С, — clang-format. Родные настройки clang-format на порядок выше любого другого автоматического форматтера C-кода. Более того, его разработчики постоянно трудятся над новыми функциями продукта.
Я привык использовать следующий скрипт для clang-format:
#!/usr/bin/env bash
clang-format -style="{BasedOnStyle: llvm, IndentWidth: 4, AllowShortFunctionsOnASingleLine: None, KeepEmptyLinesAtTheStartOfBlocks: false}" "$@"
Вызывайте команду так (если вы присвоили скрипту имя
cleanup-format
):matt@foo:~/repos/badcode% cleanup-format -i *.{c,h,cc,cpp,hpp,cxx}
Опция -i не сохраняет новые файлы и не создает их резервные копии, а перезаписывает существующие файлы вместе с результатами форматирования.
Если у вас много файлов, можно параллельно рекурсивно обработать все дерево исходного кода:
#!/usr/bin/env bash
# обратите внимание: clang-tidy принимает только один файл за раз, но мы можем обращаться к функции
# параллельно для непересекающихся коллекций.
find . \( -name \*.c -or -name \*.cpp -or -name \*.cc \) |xargs -n1 -P4 cleanup-tidy
# clang-format принимает несколько файлов за раз, но устанавливает ограничение на уровне 12
# что (возможно) позволит предотвратить перегрузку памяти.
find . \( -name \*.c -or -name \*.cpp -or -name \*.cc -or -name \*.h \) |xargs -n12 -P4 cleanup-format -i
Кроме того, хочется поделиться с вами и новым скриптом cleanup-tidy. Он выглядит, примерно, так:
#!/usr/bin/env bash
clang-tidy -fix -fix-errors -header-filter=.* --checks=readability-braces-around-statements,misc-macro-parentheses $1 -- -I.
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()
бесплатно освободить дополнительную память, почитайте эти интересные статьи:Benchmarking fun with calloc() and zero pages (2007)
Copy-on-write in virtual memory management
В 2016 году я по-прежнему настаиваю на рекомендации всегда использовать
calloc()
для большинства привычных сценариев (в частности, для х64 целевых платформ, для данных, касающихся конкретного человека, если мы не говорим об особенностях генома). Любые отклонения от «сферы ожидаемого» грозят отчаянием, вызванным сомнениями на тему «знания предмета», но не стоит об этом.Дополнение: предварительно очищенная с помощью
calloc()
память – прекрасный результат, но это разовая акция. Если после calloc()
вы запросите realloc()
, в итоге не будет дополнительного объема чистой памяти. Перераспределенное пространство заполнит всевозможный стандартный неинициализированный контент в зависимости от возможностей ядра системы. Если вы хотите освободить место после работы с realloc()
, придется вручную запрашивать memset()
.Никогда не используйте memset
(если можно обойтись без него)
Не спешите ссылаться на
memset
(ptr, 0, len, когда можно статически задать для структуры (массива) нулевое исходное значение (или обнулить необходимые показатели, обратившись к встроенному составному литералу или к значению общей нулевой структуры).В то же время
memset()
— ваш единственный выбор, если нужно обнулить структуру, включая байты внутренних отступов (так как функция{0} распространяется только на определенные участки и игнорирует неопределенные области, отведенные под отступы).Заключение
Найти универсальную формулу для написания правильного кода, практически, невозможно. Мы имеем дело с множеством операционных систем, различным временем автономной работы программ, всевозможными библиотеками и платформами, что только увеличивает список поводов для беспокойства, даже, если мы рассматриваем случайное инвертирование битов RAM или когда наши фильтры вдруг начинают «врать».
Лучшее, что мы можем сделать — это писать простой, понятный код, в котором количество возможных сбоев и неожиданных форс-мажоров сведено к минимуму.
Комментарии (80)
CodeRush
22.01.2016 14:06+10Очень много крайне спорных советов, на самом деле. Вот достойный ответ, особенно на использование calloc.
ragequit
22.01.2016 14:09+7Ну, это касается любых статей-мнений. Мне предложили приложить еще одну статью-ответ.
/me Принимаю реквесты на переводы «ответов».
Greyushko
22.01.2016 14:11+2Согласен. Когда читал, тоже соглашался далеко не со всем. Например, про тот же calloc, кажется, что его использование «всегда и везде» может привести к маскированию некоторых ошибок: например, забытую инициализацию какого-то поля структуры ненулевым значением.
Да и «первое правило» весьма сомнительное. Зачем автор целенаправленно его уточнил до «не используйте Си, если не надо»? Ведь это же частный случай более правильной и очевидной вещи: «Для решения задач используйте подходящие инструменты». В большинстве случаев, если использования Си можно избежать, значит, он там изначально и не был нужен.CodeRush
22.01.2016 14:23Там не только с маскированием ошибок проблемы, там integer overflow потенциальный при любом вызове. Если уж советовать замену, то на reallocarray() из OpenBSD, что автор вышеупомянутой статьи и предлагает.
lockywolf
22.01.2016 14:45+6Мне очень понравилась статья. У С++ есть Страуструп, который ездит по миру и в лекциях активно пропагандирует использование современных конструкций С++, таких как unique_ptr. У чистого С же мне такой человек не известен, и потому появлению подобных статей я очень рад.
Viacheslav01
22.01.2016 14:58+2На мой взгляд очень спорное решение
void processAddBytesOverflow(void *input, uint32_t len) {
uint8_t *bytes = input;
for (uint32_t i = 0; i < len; i++) {
bytes[0] += bytes[i];
}
}stack_trace
23.01.2016 10:05Можете пояснить, что в нём спорного?
qw1
23.01.2016 16:33+1Повышаются шансы сделать опечатку на вызывающей стороне.
Вместо поля uint8_t* случайно передать соседнее поле SomeClass*stack_trace
23.01.2016 16:35Какой класс? Это же С. К тому же, как быть если вам надо передать именно поле SomeClass?
qw1
23.01.2016 16:39+1Какой класс? Это же С
ну ок, пусть будет struct SomeClass* :)
К тому же, как быть если вам надо передать именно поле SomeClass?
просто сделать каст.
когда для суммирования всех байт с накоплением результата в первом байте передаётся адрес структуры, это наиболее вероятно опечатка, чем реальный кейс.stack_trace
23.01.2016 16:42Речь в статье идёт именно о случаях, когда можно передать любые данные, функции типа memcpy(), где операции производятся над памятью, независимо от структуры и это реальный кейс. В остальных случаях естественно надо указывать именно те типы, которые вы хотите использовать. Другие примеры подобных функций: send, encrypt, md5sum и тп.
qw1
23.01.2016 16:47+3Согласен, если это просто кусок памяти с размером.
Значит, пример был неудачный, т.к. в передаваемом указателе на память не все байты равнозначны, первый имеет особенное значение.stack_trace
23.01.2016 23:10+1На самом деле, совет стоящий. Я, к сожалению, когда писал на C, много чаще чем мне хотелось бы сталкивался с сигнатурами вида:
void foo(char* data, int len); void bar(uint8_t* data, unsigned len);
Вместо каноничного
void baz(void* data, size_t len);
Почему второй вариант лучше?
- По такой сигнатуре сразу видно, что принимается просто кусок памяти определенного размера. Понять эту сигнатуру по-другому просто невожможно.
- Вам не надо каждый раз кастовать данные к типу первого параметра.
qw1
24.01.2016 00:46По такой сигнатуре сразу видно, что принимается просто кусок памяти определенного размера. Понять эту сигнатуру по-другому просто невожможно.
Сильно зависит от функции. Для ф-ций с названиями foo, bar совершенно не очевидно, что они делают. К примеру,strchr(char*, int)
принимает кусок памяти?stack_trace
24.01.2016 02:41В этом и проблема с функциями вида (char*, int) — непонятно, принимают они просто кусок памяти, или строку. Но в данном случае, учитывая что в названии функции есть str, я бы предположил что именно строку. А в случае, если бы там было (void*, size_t) — тут без вариантов, только кусок памяти.
Viacheslav01
23.01.2016 22:26+2Во второй можно передать указатель на что угодно, с каким угодно типом и размером типа. По параметру void* не понять, сигнатуру функции это может привести к тому, что в функцию передадут int* и len в виде количества тех самых int.
Опять же передав на вход int8_t мы получим на выходе не то, что ожидали, в виду беззнаковой математики.
Видимо пример не удачный, если кусок памяти и его длина в байтах, тогда void* проблем не создаст.stack_trace
23.01.2016 22:51Во второй можно передать указатель на что угодно, с каким угодно типом и размером типа.
Так в этом и суть объявления. Функции неважно, какой тип. Пример обычный, суть в том, что функция проводит операции над памятью и ей не важен тип. Суть совета в том, что когда вы хотите объявить такую функцию, то иногда может возникнуть соблазн написать uint8_t* — как бы массив байт. Автор говорит о том, что такой подход заведомо неверный и надо писать void*.
Например, если вы хотите объявить какую-то свою memcpy то следует в сигнатуре указывать не uint8_t* (по сути, байтовый массив), а void*.creker
23.01.2016 23:49+1Вообще сам код говорит, что ему важно какой тип. Ему нужны байты. Так и передавайте байты, а не указатель на ничего. Мне как раз запись вида void* всегда не нравилась. Функция ожидает кусок памяти, память у нас считается в байтах. Так с какого она прячет свои намерения за каким-то непонятным void*? И то что надо делать каст скорее как раз плюс, потому что четко показывает намерение. И практически во всех современных API дело обстоит именно так. Взять тех же ближайших родственников ObjC и С++
stack_trace
24.01.2016 00:12+3C этим подходом есть несколько проблем:
1. В C нет типа byte. Вместо него есть множества других типов: char, signed char, unsigned char, uint8_t, int8_t и тд и тп.
2. void* — это не «указатель на ничего», как вы выразились, а банально адрес в памяти. Такая сигнатура показывает, что функции не важен тип данных. Функции не нужны байты, как вы говорите, ей нужна память. Представление ей не важно. Например, ей не важна знаковость типа, которая иначе всегда вылазит.
Так с какого она прячет свои намерения за каким-то непонятным void*
Как-раз таки своим void* функция и сообщает нам о своих намерениях. Им она говорит пользователю: «Мне нужна память и мне плевать что там у тебя. Просто дай мне адрес.» Хочу также добавить, что тип void* известен и понятен любому, кто хоть немного знает C и не я не понимаю, почему вы считаете его «каким-то непонятным».creker
24.01.2016 00:25memcpy не нужны байты? malloc не выделяет байты? Не надо самого себя обманывать. Нужна всем этим функциям конкретная информация из этого указателя. Вы длинной то что передаете? Правильно, сколько байтов. Ваш же пример — функция работает с байтами, она делает каст даже специально внутри себя. То что вы рекомендуете в этом случае void* это не более чем дань тому, что принято в С. По всем остальным объективным причинам там должен быть массив char, uint8_t или еще чего, как это сделано у других, кто смог позволить себе уйти от С прошлого.
то хоть немного знает C и не я не понимаю, почему вы считаете его «каким-то непонятным
То что он известен и понятен не делает его заведомо хорошим и правильным. И аргумент в пользу void* тогда должен звучать как «так причин и известно остальным, так принято в языке», а не выдумать аргументы, который, по сути, ложны.stack_trace
24.01.2016 00:34Ну, те же функции типа memcpy кастуют этот указатель обычно к машинному слову, то есть чему-то типа int*. Да и многие другие функции кастуют такую память вовсе не к массиву байт. Но даже если бы я и хотел принять байты в открытом виде у меня всё ещё возникла бы проблема — в C нет типа для байтов. Массив char — вообще худшее что можно сделать — в зависимости от платформы и настроек компилятора он может быть либо знаковым либо беззнаковым. А uint8_t — это беззнаковый тип, о котором известно только что он занимает не менее 8 бит. На какой-нибудь платформе, где байты содержат меньше бит (например 7) это будет не однобайтовый а двухбайтовый тип.
creker
24.01.2016 00:42-1Вот и опять пришли к выводу — void* не потому что так правильно, а потому что того требует совместимость с различными платформами и языками.
А uint8_t — это беззнаковый тип, о котором известно только что он занимает не менее 8 бит. На какой-нибудь платформе, где байты содержат меньше бит (например 7) это будет не однобайтовый а двухбайтовый тип.
Вообще стандарт четко дает понять, что char всегда будет равен байту. Более того, тот же gnu C говорит о 8 битах. Как это будет реально работать на таких странных железках я не знаю. Что до использования char, то стандартная библиотека С++ как раз его и использует при работе с теми же файлами, где нас четко просят массив char и его длину.khim
24.01.2016 00:49Что до использования char, то стандартная библиотека С++ как раз его и использует при работе с теми же файлами, где нас четко просят массив char и его длину.
Давайте не смешивать всё-таки C и C++. C++ может себе позволить просить массивCharT
(неchar
, как все прекрасно знают!!!), в C жизнь устроена иначе.
Вот и опять пришли к выводу — void* не потому что так правильно, а потому что того требует совместимость с различными платформами и языками.
Это бессмысленная софистика. Обсуждать как и когда описывать параметры функции не упоминая того, о каком языке идёт речь — это, конечно, отличное развлечение для философов, но программистам обычно нужна конкретика.creker
24.01.2016 01:03программистам обычно нужна конкретика.
И тут внезапно мы используем void* там, где должен быть массив char или unsigned char — при записи в теже потоки мы таки не адрес хотим, а блок байтов. Вот и я хочу конкретики — memcpy копирует мне байты, так пусть она у меня байты и просит. Хорошо что современные API уходят от этих ужасов и даже полностью совместимый Objective C использует для тех же потоков тип uint8_t.
Еще раз, я могу понять, что С это нужно, потому что там жизнь сложна. Но тогда и аргумент в пользу должен быть соответствующий, а не void* потому что функции не важен тип, когда эта функция пишет байты в файл или сокет.stack_trace
24.01.2016 02:53+3при записи в теже потоки мы таки не адрес хотим, а блок байтов.
Это неверно. При работе с потоками мы манипулируем символами, которые не связаны напрямую с байтами и могут быть любого размера. Вы почему-то перешли от C к C++ и от памяти к операциям ввода/вывода. Я теряю нить спора.
Вот и опять пришли к выводу — void* не потому что так правильно, а потому что того требует совместимость с различными платформами и языками.
Именно потому, что так правильно. Совместимость — один из критериев корректности кода, но это лишь одна причина использовать void*. Вам привели уже десяток аргументов, но вы всё ещё считаете правильным другой вариант по неясным для меня причинам.
Вот и я хочу конкретики — memcpy копирует мне байты
Да не копирует он байты. Он машинными словами копирует, я уже говорил. А если вы имеете ввиду именно физический смысл происходящих процессов, то тогда любая функция превращается в операцию над байтами. Давайте везде просить массивы байт? А что, в итоге-то процессор всегда ими оперирует. А можно просить массив битов. Тоже с физической точки зрения верно — memcpy же скопирует вам биты.
khim
24.01.2016 04:30+2Вот и я хочу конкретики — memcpy копирует мне байты, так пусть она у меня байты и просит.
Хотите конкретики? Её есть у меня.
Рассмотрим простейшую функцию использующую ваш любимый memcpy.
Ну, скажем, такую:
bool fsignbit(float f) { int32_t x; static_assert(sizeof x == sizeof f, "int32_t and float must have identical size"); memcpy(&x, &f, sizeof x); return x & 0x80000000; }
Можете показать тут ваши любимые «байты» и объяснить что тут происходит? Я — могу: копируется один объект в другой (это, кстати, единственный способ это сделать переносимо), дальше мы интерпретируем структуру IEEE 754 числа и получаем ответ. При этом нас вообще не волнует тот факт, что на 68K мы берём первый байт, а на X86 — последний. Нам это не нужно. Если стандарт IEEE 754 поддерживается — ответ будет верен. А как вы происходящее будете объяснять со своим подходом?
P.S. Версия для C, понятно, обойдётся обычным assert'ом, не static, но суть дела это не меняет. memcpy не работает с «байтами». Она работает с «кусками памяти». А уж что там внутри у них — это не её собачье дело. То же самое — всякиеsha512
и прочие. Вот ICU — та да, работает с байтами (при использовании UTF-8, по крайней мере). А всякие низкоуровневые функции — нет, это не их забота!
khim
24.01.2016 00:39+1По всем остальным объективным причинам там должен быть массив char, uint8_t или еще чего
Вот с этого момента — поподробнее. У вас там «массив char», «uint8_t» или «ещё чего»?
Впомните про пассаж либо вы включаете -fno-strict-aliasing, либо вы сможете работать с объектами исключительно в том виде, в котором они создавались из статьи и объясните мне наконец что вы предлагаете использовать вместоvoid *
? Технически там допустимы два типа:char *
иvoid *
и из этих двух альтернатив всё-такиvoid *
выглядит предпочтительнее.creker
24.01.2016 00:44-1Я бы предпочел typedef на unsigned char как это принято у Apple например. Хотя char тоже успешно используется в том же С++ по стандарту. Оба варианта успешно работают и никогда не доставляли проблем будь это x86 или ARM
К слову, насколько знаю, в той же iOS по-умолчанию используется strict aliasing и там uint8_t* повсеместен для куска памяти. Единственный раз, когда меня это ударило больно, это когда я смешивал выравнивание на различные границы в структурах, от чего код падал.khim
24.01.2016 00:53+2К слову, насколько знаю, в той же iOS по-умолчанию используется strict aliasing и там uint8_t* повсеместен для куска памяти.
iOS может себе это позволить, так как она рассчитана только и исключительно на платформы с 8-битовыми байтами и ни на каких других процессорах работать не может.
Кстати рассказы про 7-битовые байты — это фигня. C их не поддерживает (UCHAR_MAX по стандарту — минимум 255). Но что делать с машинками, где типа uint8_t просто нет? Такие вполне себе существуют: CRAY, к примеру. Там sizeof(char) == sizeof(int) == 1, а в байте — 32 бита.
CodeRush
24.01.2016 00:12+2void* — это не «указатель на ничего», это указатель на кусок памяти без типа, ровно такой, как нам её выделяет malloc() и освобождает free(). В C, вообще говоря, нет типа данных «байт», и даже нигде не написано, что в байте должно быть 8 бит (это написано в стандарте POSIX, но на C можно писать и для архитектур с другим размером байта), и, таким образом, использовать uint8_t в этом месте — совершенно неразумное ограничение как переносимости, как так и удобства использования вашего кода. Понятно, что явное лучше, чем неявное, но в данном случае void* все-таки намного лучше отражает намерения программиста, на мой взгляд.
creker
24.01.2016 00:21-1Вот это единственный реальный аргумент в пользу void* — переносимость на странные платформы. Что до malloc и free — их сигнатура так же не соответствует тому, что она делает. Эти функции выделяю и освобождают память, которая измеряется в байтах. И если для free это вопрос реального удобства, то malloc это реально функция для работы с байтами. Как и все mem* функции. Просто так повелось и пошло поехало, а современный подход использовать именно четкое намерение — функции нужен массив байт, она это и показывает. Но у них и проблем нет — байт всегда байта и в 8 бит
CodeRush
24.01.2016 00:29Еще раз, в C никаких байт нет, и потому никакая память в них не измеряется, точка, конец истории.
Если делать функции работы с памятью через uint8_t*, то на некоторых архитектурах с hard-fail на misaligned read, к примеру, на ARM Cortex-M0, у вас процессор будет просто зависать на первом же обращении к такой памяти примерно в 3/4 случаев.creker
24.01.2016 00:32-2Опять двадцать пять. Вы сигнатуру memcpy видели? Что там 3 аргументом передается? Что в malloc аргументом передается?
CodeRush
24.01.2016 00:54Сигнатуры — видел:
void* memcpu(void*, const void*, size_t)
void* malloc(size_t)
Теперь поговорим о байтах. Если вы считаете, что «байт» — это uint8_t, то в общем случае, вы не правы. Если считаете, что malloc выделяет ровно столько «байт», сколько вами запрошено, то вы тоже не всегда правы (это сильно зависит от реализации, может вообще целую страницу выделить).
Все, что вам гарантирует malloc — это то, что вам вернут достаточно большой кусок нетипизированной памяти для хранения указанного числа «байт», размер которых зависит от архитектуры, и что память потом можно будет освободить при помощи free, либо NULL, если выделить не получилось. Обычная malloc из C даже выравнивание не гарантирует, и в POSIX специально пришлось вводить posix_memalign, чтобы не ловить CPU Exception'ы в случайном месте.
Все, что я хочу тут сказать — в языке нет и не было понятия «восьмибитный байт», а само слово «байт» в документации используется в значении «минимальная единица размера типа».khim
24.01.2016 01:01+1Обычная malloc из C даже выравнивание не гарантирует
Гарантирует: The pointer returned if the allocation succeeds is suitably aligned so that it may be assigned to a pointer to any type of object with a fundamental alignment requirement and then used to access such an object or an array of such objects in the space allocated (until the space is explicitly deallocated).
в POSIX специально пришлось вводить posix_memalign, чтобы не ловить CPU Exception'ы в случайном месте.
В стандарте тоже естьaligned_alloc
, но это для другого: для явно типов явным выравниванием (_Alignas
или всякие нестандартные SIMD-типы).CodeRush
24.01.2016 01:10Внезапно, отлично, буду знать.
У меня, честно говоря, нет ни malloc, ни free (вместо них AllocatePool/FreePool и AllocatePages/FreePages, первая из которых по умолчанию выравнивает по sizeof(size_t), а вторая — по размеру страницы), но я помню проблемы с выравниванием выделенной malloc памяти при работе с Cortex-M0 в Keil uVision 4, видимо, это был баг в реализации для этих ядер или просто процессор попался неудачный.
khim
24.01.2016 00:55+1Вы сигнатуру memcpy видели? Что там 3 аргументом передается? Что в malloc аргументом передается?
Размеры объектор. В байтах, да. По стандарту — гарантируется, что в нём есть как минимум 8 бит. Но может быть и больше.CodeRush
24.01.2016 00:57Вот про «может быть и больше» я и пытаюсь говорить, но видимо, построил слишком уж категорический императив, прошу пардону.
moonug
24.01.2016 01:24+2Семантически void* — указатель на начало участка памяти. uint8_t* — указатель на начало массива uint8_t, char* — на начало строки символов. size_t — штуки, для memcpy и malloc — количество байт. Байт, что отдельно радует, не обязан быть 8 бит.
Для char гарантируется sizeof(char)==1. При этом char может быть как знаковым, так и беззнаковым типом.
Байт, фактически, можно определить как unsigned char, содержащий CHAR_BIT.
CHAR_BIT бит — это минимально возможная часть для чтения на этой платформе и может быть больше, чем например short.
Поэтому, в C, когда нужно сослаться на участок в памяти лучше использовать void* — так Вы точно показываете свои намерения.
При этом функция, приведенная в статье в качестве примера, должна иметь сигнатуру (uint8_t*, size_t), в силу своей реализации.
Isis
22.01.2016 18:13+1Сравнение #pragma once и ifndef.
khim
23.01.2016 03:55+3Зачем сравнивать работающий инструмент с неработающим?
"#pragma once" исходит из неверной посылки: из того, что компилятор может определить, что два файла идентичны. Что, в общем случае, невозможно (почему она и отсутствует в стандарте).
Пример номер 1Файлы, которые случайно оказались одинаковыми не включатся дважды.$ ls -l total 12 -rw-r----- 1 khim khim 26 Jan 23 01:28 ns1.h -rw-r----- 1 khim khim 26 Jan 23 01:28 ns2.h -rw-r----- 1 khim khim 113 Jan 23 01:28 test.cpp $ cat ns1.h #pragma once class A { }; $ cat ns2.h #pragma once class A { }; $ cat test.cpp namespace NS1 { #include "ns1.h" } namespace NS2 { #include "ns2.h" } int main() { NS2::A a; return 0; } $ gcc test.cpp test.cpp: In function ‘int main()’: test.cpp:11:5: error: ‘A’ is not a member of ‘NS2’ NS2::A a; ^ test.cpp:11:5: note: suggested alternative: In file included from test.cpp:3:0: ns1.h:2:7: note: ‘NS1::A’ class A ^ test.cpp:11:12: error: expected ‘;’ before ‘a’ NS2::A a; ^ $ gcc --version gcc (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4 Copyright (C) 2013 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
qw1
23.01.2016 16:36#pramga once
— не замена модулей, а замена конструкции#ifndef XXX / #define XXX / #endif
И тут она очень хороша, потому что исходный вариант заставляет препроцессор парсить весь файл до последнего endif, когда это не нужно.khim
23.01.2016 17:01+2Если вы когда-либо общались с языками, которые поддерживают модули, то можете знать, что у модулей есть важная особенность: они имеют имя.
Заголовочные файлы — это такие «модули для бедных на основе препроцессора». Вопрос: где у них имя? Файлы с одинаковым именем далеко не всегда являются одним и тем же модулем (к примеру time.h и sys/time.h — это совершенно разные вещи). Более того: как и в настоящих, «продвинутых» языках в «C» один файл может содержать несколько модулей. К примеру
и#define __need_size_t #include <stddef.h> #undef __need_size_t
#define __need_ptrdiff_t #include <stddef.h> #define __need_ptrdiff_t
дадут вам разный результат. Про windows.h я вообще молчу.
Таким образом ответ на вопрос таков: так уж получилось, что имя модуля — это как раз вышеуказзанные XXX в вашей конструкции. А при использовании заголовочного файла с#pramga once
имени у модуля нет. Со всеми вытекающими.
В сложных, больших, программах#pragma once
использовать просто опасно — вы можете нарваться на разного рода проблемы, описанные выше. В маленьких программах же она бессмысленна, они и так быстро компилируются.
Так что всё, что про неё нужно знать — что использовать эту директиву не следует. Как умные люди и делают. Do not use#pragma once
, , include guards are portable and seem to be just as efficient even on platforms that do support#pragma once
, so there is no reason to use it, etc.qw1
23.01.2016 22:46Логично делать
#define __need_size_t #define __need_ptrdiff_t #include <stddef.h>
Понятно, нельзя застраховатся от того, что #include был где-то раньше и из-за pramga once его уже не переподключишь, но ведь и нельзя застраховаться от того, что кто-то раньше уже не сделал
#define __need_size_t #include <stddef.h>
и оно выпадет сerror: redeclaration of ...smthg...
у каждого подхода есть свои проблемы.
мы рассмотрели пример 3
В примере 2 можно столкнуться с ситуацией, когда два разных модуля с одинаковым именем используют одинаковый guard symbol, например, гипотетический loctypes.h и LOCTYPES_H_INCLUDED. В этом случае #pragma once отлично справляется без лишних undef или замены метки в h-файле.
И пример 1. если модуль надо включать 2 раза именно по дизайну программы, его просто не надо оборачивать в #pragma once.
У меня были такие примеры, например какой-нибудь эмулятор чипа надо включить дважды с разными настройками, чтобы получить код эмуляции близких чипов, но немного разных. Разумеется, тут не нужен ни guard symbol, ни pragma once.
Про windows.h я вообще молчу.
Была потребность включить Windows.h два раза в одну единицу трансляции? #pragma once не серебрянная пуля, а компромис именно для таких и похожих заголовков.khim
24.01.2016 00:25Логично делать
Это вообще как? Рассмотрите
#define __need_size_t #define __need_ptrdiff_t #include <stddef.h>
полный пример$ cat header1.h #ifndef HEADER1_H_ #define HEADER1_H_ #define __need_size_t #include <stddef.h> extern size_t h1_size; #endif /* HEADER1_H_ */ $ cat header2.h #ifndef HEADER2_H_ #define HEADER2_H_ #define __need_ptrdiff_t #include <stddef.h> extern ptrdiff_t h2_ptrdiff; #endif /* HEADER2_H_ */ $ cat header3.h #ifndef HEADER3_H_ #define HEADER3_H_ #define __need_size_t #define __need_ptrdiff_t #include <stddef.h> extern size_t h3_size; extern ptrdiff_t h3_ptrdiff; #endif /* HEADER3_H_ */ $ cat main.c #include "header1.h" #include "header2.h" #include "header3.h" extern size_t h1_size; extern ptrdiff_t h2_ptrdiff; extern size_t h3_size; extern ptrdiff_t h3_ptrdiff; int main() { } $ gcc main.c
qw1
24.01.2016 01:04Откуда столько ненависти к бедной
#pragma
:)
Возможно, я что-то делаю не так, но никаких проблем у меня с ней не было.
Нужно просто понимать, как это работает и для заголовков, подразумевающих повторные включения, использовать другой механизм.
Была потребность включить Windows.h два раза в одну единицу трансляции?
Конечно.
Предствляю, какая получилась каша из объявлений и макросов. Мне кажется, можно было придумать решение получше.CodeRush
24.01.2016 01:13+1От нестандартности всё. Этих #pragma развели три мешка, даже в стандарт добавили в #pragma STDC, а большую часть проблем они если и решают, то только на определенных компиляторах и при определенной фазе луны. От того их и не любят почти все, кто с ними работает.
khim
24.01.2016 01:59Как правило их любят те, кто развёл. И не любят те, кому приходится общаться разными системами.
Вот мне тут говорят: «Предствляю, какая получилась каша из объявлений и макросов. Мне кажется, можно было придумать решение получше».
Ну да — можно было. При написании заголовочного файла! А для этого — нужно использовать не#pragma once
, а нормальные ifdef'ы. GNU, кстати, тоже неидеально сделан в этом месте: если вам нужен _GNU_SOURCE, а кто-то какой-нибудь до вас ужеtime.h
включил, то фиг вы свою любимую getdate_r увидите. Но то, что кто-то где-то инструмент неправильно применяет — не повод использовать вместо него другой, который не работает совсем! Тем более нестандартный.
0xd34df00d
26.01.2016 19:05+1Основная причина использовать
#pragma once
— меньше возможностей сделать ошибку при перемещении файла в рамках ФС, разделении файла на несколько, и так далее. Ошибки, когда у отрефакторенного файла забыли переименовать гард, весьма забавны, и я был свидетелем, как даже весьма опытные программисты тратят часы, чтобы их починить. Ошибок с прагмой я не видел ни разу, хотя кодовая база их использует даже больше, чем гарды.
Видимо, у меня просто не distributed FS.
stack_trace
23.01.2016 17:34+3О чём вообще тут говорить? #pragma once не имеет отношение к стандарту, а значит использовать её (как и всё, что не имеет отношение к стандарту) следует лишь в крайнем случае, когда нет альтернатив. В данном случае альтернатива есть и она работает лучше. Вывод простой — #pragma once — использовать не следует вообще никогда.
encyclopedist
25.01.2016 14:55Это неверно.
Когда компилятор читает заголовочный файл первый раз, он обязан прочитать и препроцессировать его полность в любом случае. При этом в случае «pragma once» компилятор запоминает: «файл alpha/bravo/delta.h посещён», а в случае ifndef он запоминает «файл alpha/bravo/delta.h посещён и зашищен символом DELTA_H».
Когда компилятор в следующий раз встречает include «alpha/bravo/delta.h» он проверяет наличие символа DELTA_H и если он попределен, то не читает файл совсем, точно также как в случае использования прагмы.
Документация GCC тутkhim
25.01.2016 17:07Даже если вы используете MSVC, где подобных оптимизаций вроде нет (по крайней мере Microsoft про них не пишет) на время компиляции на сегодняшних машинах
#pragma once
практически никак не влияет.
Что и логично: они обрабатываются на первом, препроцессорном, проходе, который вообще самый простой и быстрый из того, что в современном компиляторе есть. Даже синтаксический разбор программы на C++ на порядок (а то и на два) сложнее и дольше. А там ещё и оптимизации…
0xd34df00d
26.01.2016 19:02+1Не могу воспроизвести ваш пример 1.
Примеры 2 и 3 — ошибки программиста, непонятно, причём тут #pragma once. Совершенно аналогично можно накосячить с include guard'ами.khim
26.01.2016 20:27-1На одинаковые/разные времена модификации файлов внимание обратили?
Что касается примера 2 — это типичная ситуация, когда у вас обновляется отдельно собираемая библиотека. Она, с одной стороны, видит «свои» заголовки в том виде, в котором они лежат у неё в каталоге, с другой — может подхватывать (напрямую или через промежуточные заголовки) файлы более старой версии, которые установлены в системый каталог. Если библиотека разработаывается нормальными людьми, то эти заголовочные файлы должны быть совместимы, но у них вполне могут оказаться разные времена модификации и вообще разное содержимое.
Пример 3 с "#pragma once
" не работает совсем.
Пункт номер ноль при обсуждении любого инструмента — это вопрос: всегда ли он работает корректно при корректном его использовании. Если нет, то уже неважно — как удобно и хорошо её использовать в тех или иных случаях. Конечно удобство важно, котнечно include guard'ы — не идеал. Но они, по крайней мере, работают когда их правильно используют.#pragma once
же этот тест проваливает: она может не работать тогда, когда человек, в общем-то, ничего неправильного и не делал.0xd34df00d
26.01.2016 20:49На одинаковые/разные времена модификации файлов внимание обратили?
А какими они должны быть? Я-то просто один файл написал иcp
сделал.
Сделал ради экспериментаcp -ap
— да, gcc падает от этого. clang, что интересно, нет.
Если библиотека разработаывается нормальными людьми
Я бы сказал, что тогда билдсистема должна быть устроена так, что старые версии по системным путям не подхватываются. Это чревато кучей геморроя, проще за этим следить, нежели чем думать, какая там старая версия стоит, совместима ли, и так далее.
Пример 3 с "#pragma once" не работает совсем.
С гардами он тоже как-то не очень:
Скрытый текстbash-4.2$ cat header.h #ifndef HEADER_H #define HEADER_H #ifdef NEED_TYPE_A struct A { }; #endif #ifdef NEED_TYPE_B struct B { }; #endif #endif bash-4.2$ cat test.c #define NEED_TYPE_A #include "header.h" #define NEED_TYPE_B #include "header.h" int main() { struct A a; struct B b; } bash-4.2$ gcc test.c test.c: In function 'main': test.c:9: error: storage size of 'b' isn't known
khim
28.01.2016 03:09-1С гардами он тоже как-то не очень:
В таком виде — да, конечно. Можете найти и посмотреть на то, как в GCC сделан stddef.h. Если вы хотите подобные вещи деть, то гарды тоже нужно делать другие: обрамлять только те вещи, которые нужны. В вашем примере — вам будет нужны два гарда. На HEADER_H_TYPE_A и на HEADER_H_TYPE_B.
…
Всё, перестаём пользоваться?qw1
28.01.2016 17:57Кстати, интересно. Тут встречалось упоминание, что есть костыль, который если встречает файл, начинающийся с
#ifndef XXX
/#define XXX
и заканчивающийся#endif
, обработка ускоряется как для#pragma once
, т.е. парсинг всего файла не делается. Получается, два гарда в одном файле заставят этот костыль работать некорректно?khim
29.01.2016 01:10-1Это не «костыль», а «оптимизация». И нет, два гарда ни к каким проблемам не приводят — см. пример stddef.h ещё раз. Этот файл входит в поставку clang'а и gcc, так что будьте уверены — последнее, что мы сможем увидеть, это проблемы с этой оптимизацией, попавшие в релиз.
qw1
29.01.2016 13:56Не поленился скачать файл, посмотреть почему «оптимизация» его не ломает.
Оказывается, вместо#ifndef _STDDEF_H
там применили#if (!defined(_STDDEF_H) ... )
, чтобы «оптимизация» не сработала.
0xd34df00d
29.01.2016 15:25Это уже не каноничные гарды получаются, не засчитано.
А вообще, ИМХО, делать такие хедеры — идиотизм. Тут уж никакие модули не помогут — один из ранних пропозалов на них, что я видел, предполагал изолированное препроцессорное окружение внутри модуля.
mkarev
22.01.2016 22:19+2Выбирая gcc, важно указывать -std=c99...
В некоторых случаях (google nacl newlib/glibc) лучше будет -std=gnu99, иначе становятся недоступными POSIX расширения, такие как strdup и др.
Пару слов о портабельности и замечательном компиляторе MSVC, который так несправедливо упустили из повествования
— если MSVC достаточно свежий (>=13), то поддержка c99 там хоть какая-то, да есть
— если MSVC более старый, то можно использовать трюк: на все сишные единицы компиляции вешаем свойство «язык С++» и получаем возможность собирать Си код.
Пример для cmake:
set_source_files_properties(${SOURCES} PROPERTIES LANGUAGE CXX)
Конечено, есть и минусы — придется таскать за собой stdbool.h, статическая инициализация именованных полей структур перестанет работать, в switch case нельзя будеть объявлять переменные (прыжок через инициализацию) и т.п.mkarev
22.01.2016 22:29Так же к слову о портабельности — форматный спецификатор "%zd" в MSVC реализации стандатной библиотеки не поддерживается.
khim
23.01.2016 04:05+3Особенно радует пассаж "В С99 появились массивы переменной длины (в С11 их можно выбирать по желанию)".
Создаётся впечатление, что у разработчика на «C» в C11 появился выбор: использовать массивы переменной длины или нет. Нет, ребятки: выбор-то появился не у разработчка! А как раз у разработчика компилятора. И на некоторых популярных платформах их таки и нету…
Рекомендовать использовать VLA вместо alloca — это просто издевательство. Особенно в статье, где ещё и#pragma once
рекламируется:#pragma once
может быть полезна под Windows с MSVC (хотя и там пользы немного), но вот VLA там как раз и нету, так что… где же полный набор этих «вредных советов», чёрт побери, автор предлагает применять?
Clash
24.01.2016 02:07+2Статья очень сильно долбанутая. Очень много спорных мест. Но больше всего выбесил unsigned во всех проявлениях. Беззнаковые нельзя использовать не потому, что unsigned long long выглядит глупо, а потому что математика с unsigned делает совсем не то, что ожидает здравый смысл, и более того, это даже зависит от битности процессора. Но в статье огромная куча ссылок на использование unsigned типов. Для тех, кто не понимает этого тезиса — добро пожаловать в реальный мир, пройдите этот тест по теме и порадуйтесь blog.regehr.org/archives/721
Mrrl
25.01.2016 09:24+3Хороший тест. Хорош тем, что показывает, что unsigned использовать просто необходимо — иначе любая мелочь приводит к неопределённому поведению. Написать какую-нибудь распаковку битового потока (типичнейшая задача) на знаковых числах, наверное, можно, но это будет выглядеть, в лучшем случае, как упорная борьба с созданными себе самому трудностями.
qw1
25.01.2016 13:13unsigned использовать просто необходимо
Всегда по-разному. Недавно ловил ошибку в
из-за того, что разность получается отрицательной (for (size_t i = 0; i <= textlen - stringlen; i++)
textlen
иstringlen
тожеsize_t
, как положено). Беглая замена типа i на ptrdiff_t не помогает, т.к. сравнение signed с unsigned делается как unsigned. В итоге наплевал на красоту и сделалfor (size_t i = 0; ptrdiff_t(i) <= ptrdiff_t(textlen - stringlen); i++)
Duduka
25.01.2016 14:20а не проще проверить разность перед циклом?
qw1
25.01.2016 15:08не хочется выносить эту проверку в особый случай.
этот цикл ищет строку в тексте и перебирает возможную позицию начала строки. если строка длиннее текста, должно быть 0 итерацийMrrl
25.01.2016 15:25+2Но это как раз особый случай. Его и компилятор всё равно рассмотрит отдельно, и всякие инварианты цикла в нём нарушаются.
Но может быть, я бы его записал как
— выхода из диапазона unsigned бы не было.for (size_t j = stringlen; j <= textlen; j++)
qw1
25.01.2016 17:02В принципе, красиво. Но тут j — это конец строки в тексте, когда i — был началом, придётся немного поломать внутри цикла…
Mrrl
25.01.2016 16:15Другой вариант — записать условие в виде
Но это если не страшна некоторая потеря производительности.i+stringlen<=textlen
Clash
25.01.2016 13:16+2Я там выше выразился конечно же не корректно. unsigned вполне нужно использовать, но там, где это нужно, например, битовые поля, да. Но очень, ну очень много людей используют unsigned переменные просто из соображений, что вот конкретно тут отрицательного числа не может быть, и вроде логично сделать этот счетчик unsigned. И дальше это где-то сравнивается с signed с удивительным результатом. Я вот только про это использование. Это существенный ляп, в который влетают вполне себе опытные люди. В одном из выступлений на cppcon 2014 года Майерс (просмотрел их все, сейчас на какое-то конкретное выступление ссылку не дам, просто запомнился момент) на вопрос про signed/unsigned, почему размер контейнера в STL возвращается как size_t, не нашел ничего лучшего, как сказать «we were young».
mend0za
26.01.2016 12:44Единственный продукт, который в 2016 году позволит форматировать продукты, разработанные на языке С, — clang-format.
GNU indent смотрит на вас с недоумением.
ragequit
Обо всех проблемах, неточностях, опечатках и прочем, что сумело спрятаться от меня в этой простыне, прошу сообщать в ЛС :). Спасибо.