Когда я должен использовать 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:

#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?


person TimFinnegan    schedule 26.11.2016    source источник
comment
Лично я избегаю использования типов с фиксированной шириной: не все компиляторы их поддерживают, а правила расширения для этих типов практически отсутствуют — по моему скромному мнению, это большая оплошность стандарта C. Тем не менее, голосуйте за вопрос.   -  person Bathsheba    schedule 26.11.2016
comment
Интересный вопрос, не терпится увидеть ответы   -  person cat    schedule 26.11.2016
comment
Они могут вам понадобиться, если int меньше 32 бит.   -  person nwellnhof    schedule 26.11.2016
comment
@Bathsheba С типами stdint у вас гораздо меньше проблем с неявным продвижением, чем с нативными типами. Это потому, что они ведут себя детерминировано, а не имеют произвольный размер. Таким образом, с помощью типов stdint вы можете писать код, который работает переносимо независимо от рекламных акций, в то время как с нативными типами вы получаете код, который может работать или не работать. Но для правильного написания переносимого кода в основном нужно, чтобы программист действительно знал о неявных продвижениях, независимо от выбора типов.   -  person Lundin    schedule 28.11.2016
comment
comment
Еще одна ошибка: Какой инициализатор подходит для int64_t?   -  person P.P    schedule 28.11.2016


Ответы (2)


Как правило, вы должны использовать их, когда тип литерала имеет значение. Есть две вещи, которые следует учитывать: размер и подписанность.

Относительно размера:

Тип int гарантируется стандартными значениями C до 32767. Поскольку вы не можете получить целочисленный литерал с меньшим типом, чем int, все значения меньше, чем 32767, не должны использовать макросы. Если вам нужны большие значения, то начинает иметь значение тип литерала, и рекомендуется использовать эти макросы.

По поводу подписания:

Целочисленные литералы без суффикса обычно имеют знаковый тип. Это потенциально опасно, так как может вызвать всевозможные тонкие ошибки во время неявного продвижения типа. Например, (my_uint8_t + 1) << 31 вызовет ошибку неопределенного поведения в 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++, из соображений обратной совместимости.

Однако считается хорошим стилем рассматривать логические условия, как если бы C имел настоящий логический тип. В MISRA-C:2012 есть целый набор правил, касающихся этого понятия, называемого по существу логическим типом. Это может повысить безопасность типов во время статического анализа, а также предотвратить различные ошибки.

person Lundin    schedule 28.11.2016
comment
Целочисленный размер также не имеет значения. Целочисленные литералы будут иметь наименьший тип, который может содержать значение литерала, или (unsigned) int, в зависимости от того, что больше. - person alecov; 29.05.2017
comment
... чтобы сделать язык более подробным: (my_uint8_t + 1) может стать целым числом со знаком, и его сдвиг сделает его отрицательным значением. - person Nikolai Ehrhardt; 10.03.2021
comment
@JayLee Нет, поскольку 1 тоже имеет тип, и это int, который является большим целочисленным типом, чем uint8_t, с более высоким рейтингом конверсии. Подробнее см. здесь: stackoverflow.com/questions/46073295/ - person Lundin; 19.04.2021

Это для использования небольших целочисленных литералов, где контекст не приведет к тому, что компилятор приведет его к правильному размеру.

Я работал на встроенной платформе, где int — 16 бит, а long — 32 бита. Если вы пытаетесь написать переносимый код для работы на платформах с 16-битными или 32-битными типами int и хотите передать 32-битный «целочисленный литерал без знака» в функцию с переменным числом аргументов, вам понадобится приведение:

#define BAUDRATE UINT32_C(38400)
printf("Set baudrate to %" PRIu32 "\n", BAUDRATE);

На 16-битной платформе приведение создает 38400UL, а на 32-битной платформе только 38400U. Они будут соответствовать макросу PRIu32 либо "lu", либо "u".

Я думаю, что большинство компиляторов генерировали бы идентичный код для (uint32_t) X и для UINT32_C(X), когда X является целочисленным литералом, но это могло быть не так с ранними компиляторами.

person tomlogic    schedule 28.11.2016