Когда следует использовать 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 является целым литералом, но, возможно, это было не так с ранними компиляторами.