Целочисленное продвижение (integer promotion)
В языке C арифметика не выполняется над типами меньше int. Перед любой операцией переменные uint8_t, char и short превращаются в int — это целочисленное продвижение. Оно незаметно, пока не столкнёшься с инверсией байта или сдвигом на AVR, где int всего 16 бит.
Обновлено Редакция GAW
Целочисленное продвижение (integer promotion) — правило языка C, по которому перед вычислением выражения операнды целых типов с рангом ниже int — char, short, uint8_t, int8_t, uint16_t на 32-битных платформах, битовые поля — заменяются на int, если он вмещает все их значения, иначе на unsigned int (C11, 6.3.1.1). Арифметика, сравнения и сдвиги выполняются уже над продвинутыми значениями.
Размер int решает всё
| Платформа | int | uint8_t продвигается в | uint16_t продвигается в |
|---|---|---|---|
| AVR (avr-gcc) | 16 бит | int | остаётся unsigned int (это и есть его тип) |
| Cortex-M (32 бит) | 32 бита | int | int |
Размеры типов avr-gcc — по GCC Wiki: short и int по 2 байта. Поэтому одно и то же выражение может вести себя по-разному на AVR и на STM32.
Три ловушки
- Инверсия байта.
uint8_t x = 0x0F; ~x— это не 0xF0, а 0xFFF0 на AVR и 0xFFFFFFF0 на Cortex-M, потому что инвертируется продвинутый int. Сравнение~x == 0xF0ложно; нужно(uint8_t)~x. - Сдвиг байта на AVR.
x << 8приx = 0x80вычисляется в 16-битном знаковом int: 32 768 не помещается, и по C11 (6.5.7) это неопределённое поведение. Пишут(uint16_t)x << 8. То же на Cortex-M сx << 24. - Сумма без переполнения.
uint8_t a = 200, b = 100;выражениеa + bравно 300 — переполнения нет, пока результат не записали обратно вuint8_t, где он станет 44 по модулю 256.
Числа в примерах — наш расчёт по правилам C11.
Как жить с продвижением
Явно приводить результат к нужному типу при инверсии и сравнении, приводить левый операнд сдвига к беззнаковому типу нужной ширины, следить за знаковостью при смешении int и unsigned int.
Не путать продвижение с обычными арифметическими преобразованиями: продвижение поднимает малые типы до int, а обычные преобразования затем приводят два операнда к общему типу, например int и long — к long. Подробнее о типах на AVR и ARM — в статье «Язык C для микроконтроллеров», о сдвигах и масках — в статье «Битовые операции в C», поле регистра и код маски сгенерирует калькулятор битовых масок.
Частые вопросы
Почему ~x для uint8_t x = 0x0F не равно 0xF0?
Перед инверсией x продвигается до int, и инвертируются все биты int: на 16-битном int AVR получается 0xFFF0, на 32-битном Cortex-M — 0xFFFFFFF0 (как число со знаком это −16). Сравнение ~x == 0xF0 ложно. Нужно привести результат обратно: (uint8_t)~x == 0xF0.
Переполнится ли сумма двух uint8_t?
Нет, если результат хранить в типе пошире: uint8_t a = 200, b = 100; сумма a + b вычисляется в int и равна 300. Переполнение «заворачиванием» до 44 произойдёт только при записи результата обратно в uint8_t — по C11 это преобразование в беззнаковый тип по модулю 256.
Чем опасен сдвиг байта на AVR?
uint8_t x = 0x80; выражение x << 8 вычисляется в int, а int на AVR 16-битный и знаковый. Результат 0x8000 = 32 768 в 16-битный int не помещается, и по C11 (6.5.7) это неопределённое поведение. Правильно сначала привести байт к беззнаковому типу нужной ширины: (uint16_t)x << 8 или (uint32_t)x << 24.
Источники
- ISO/IEC 9899:201x (C11), Committee Draft N1570, 2011 — 6.3.1.1: ранг целых типов; объект с рангом не выше int заменяется на int, если int вмещает все его значения, иначе на unsigned int — это целочисленные продвижения; 6.5.7: продвижения выполняются над операндами сдвига, тип результата — продвинутый левый операнд; сдвиг знакового E1, результат которого не помещается, — неопределённое поведение; 6.3.1.3: преобразование в беззнаковый тип — по модулю
- GCC Wiki: avr-gcc (ABI, Type Layout) — в avr-gcc char — 1 байт, short и int — 2 байта, long — 4, long long — 8
Нашли ошибку?