Когда следует использовать UINT32_C(), INT32_C(),... макросы в C?
Я переключился на целые типы фиксированной длины в своих проектах, главным образом потому, что они помогают мне более четко определять размер целых чисел при их использовании. Включение их через #include <inttypes.h>
также включает в себя кучу других макросов, таких как макросы печати PRIu32
, PRIu64
,...
Чтобы назначить постоянное значение переменной фиксированной длины, я могу использовать макросы типа UINT32_C()
и INT32_C()
. Я начал использовать их, когда я назначил постоянное значение.
Это приводит к похожему на код:
uint64_t i;
for (i = UINT64_C(0); i < UINT64_C(10); i++) { ... }
Теперь я увидел несколько примеров, которые не заботились об этом. Одним из них является файл stdbool.h
include:
#define bool _Bool
#define false 0
#define true 1
bool
имеет размер 1 байт на моей машине, поэтому он не выглядит как int
. Но 0
и 1
должны быть целыми числами, которые должен быть автоматически преобразован в правильный тип компилятором. Если бы я использовал это в моем примере, код был бы намного легче читать:
uint64_t i;
for (i = 0; i < 10; i++) { ... }
Итак, когда следует использовать постоянные макросы постоянной длины, такие как UINT32_C()
, и когда я должен оставить эту работу компилятору (я использую GCC)? Что делать, если я буду писать код в MISRA C?
Ответы
Ответ 1
Как правило, вы должны использовать их, когда тип литерала имеет значение. Есть две вещи, которые нужно учитывать: размер и подпись.
Относительно размера:
Тип int
гарантируется стандартными значениями C до 32767
. Поскольку вы не можете получить целочисленный литерал с меньшим типом, чем int
, все значения, меньшие, чем 32767
, не должны использовать макросы. Если вам нужны более крупные значения, тогда тип литерала начинает иметь значение, и рекомендуется использовать эти макросы.
Относительно подписанности:
Целочисленные литералы без суффикса обычно имеют тип подписи. Это потенциально опасно, поскольку это может вызвать всевозможные тонкие ошибки при неявном продвижении по типу. Например, (my_uint8_t + 1) << 31
приведет к ошибке поведения undefined в 32-битной системе, тогда как (my_uint8_t + 1u) << 31
не будет.
Вот почему у MISRA есть правило, утверждающее, что все целые литералы должны иметь суффикс u
/u
, если намерение заключается в использовании неподписанных типов. Поэтому в моем примере выше вы можете использовать my_uint8_t + UINT32_C(1)
, но вы также можете использовать 1u
, что, пожалуй, наиболее читаемо. Либо должно быть хорошо для MISRA.
Что касается того, почему stdbool.h определяет true/false как 1/0, это потому, что стандарт явно говорит об этом. Булевы условия в C по-прежнему используют тип int
, а не тип bool
типа, как на С++, для соображений обратной совместимости.
Однако считается хорошим стилем для обработки булевых условий, как если бы C имел истинный булев тип. MISRA-C: 2012 содержит целый набор правил, касающихся этой концепции, называемой по существу булевым типом. Это может обеспечить лучшую безопасность типов во время статического анализа, а также предотвратить различные ошибки.
Ответ 2
Это для использования небольших литералов с целым числом, в которых контекст не приведет к тому, что компилятор вернет его к правильному размеру.
Я работал на встроенной платформе, где int
- 16 бит, а long
- 32 бита. Если вы пытались написать переносимый код для работы на платформах с 16-разрядными или 32-разрядными типами int
и хотели передать 32-разрядный "беззнаковый целочисленный литерал" в вариационную функцию, вам понадобится бросок
#define BAUDRATE UINT32_C(38400)
printf("Set baudrate to %" PRIu32 "\n", BAUDRATE);
На 16-битной платформе cast создает 38400UL
и на 32-битной платформе только 38400U
. Они будут соответствовать макросу PRIu32
либо "lu"
, либо "u"
.
Я думаю, что большинство компиляторов будут генерировать идентичный код для (uint32_t) X
, как и для UINT32_C(X)
, когда X
является целым литералом, но, возможно, это было не так с ранними компиляторами.