Почему целочисленные типы повышаются во время добавления в C?
Таким образом, у нас возникла проблема с полем, и после нескольких дней отладки мы сузили проблему до этого конкретного фрагмента кода, где обработка в цикле while не происходила:
// heavily redacted code
// numberA and numberB are both of uint16_t
// Important stuff happens in that while loop
while ( numberA + 1 == numberB )
{
// some processing
}
Это работало нормально, пока мы не достигли предела uint16 65535. Еще одна куча операторов печати позже, мы обнаружили, что numberA + 1
имел значение 65536
, в то время как numberB
завернутый в 0
, Это провалило проверку, и никакая обработка не была сделана.
Это заинтересовало меня, поэтому я собрал быструю программу на C (скомпилированную с GCC 4.9.2), чтобы проверить это:
#include <stdio.h>
#include <stdint.h>
int main()
{
uint16_t numberA, numberB;
numberA = 65535;
numberB = numberA + 1;
uint32_t numberC, numberD;
numberC = 4294967295;
numberD = numberC + 1;
printf("numberA = %d\n", numberA + 1);
printf("numberB = %d\n", numberB);
printf("numberC = %d\n", numberC + 1);
printf("numberD = %d\n", numberD);
return 0;
}
И результат был:
numberA = 65536
numberB = 0
numberC = 0
numberD = 0
Итак, похоже, что результат numberA + 1
был повышен до uint32_t. Это предназначено языком C? Или это какая-то странность компилятора / аппаратного обеспечения?
4 ответа
Итак, похоже, что результат
numberA + 1
был повышен доuint32_t
Операнды дополнения были повышены до int
до сложения, и результат сложения имеет тот же тип, что и действующие операнды (int
).
Действительно, если int
имеет 32-битную ширину на вашей платформе компиляции (это означает, что тип, который представляет uint16_t
имеет более низкий "рейтинг конверсии", чем int
), затем numberA + 1
рассчитывается как int
сложение между 1
и повышен numberA
как часть правил целочисленного продвижения, 6.3.1.1:2 в стандарте C11:
Следующее может использоваться в выражении везде, где может использоваться int или unsigned int: […] Объект или выражение с целочисленным типом (кроме int или unsigned int), чей ранг целочисленного преобразования меньше или равен рангу int и unsigned int.
[...]
Если int может представлять все значения исходного типа […], значение преобразуется в int
В твоем случае, unsigned short
что, по всей вероятности, что uint16_t
определяется как на вашей платформе, имеет все свои значения, представляемые как элементы int
, Итак unsigned short
значение numberA
получает повышение в int
когда это происходит в арифметической операции.
Для арифметических операторов, таких как +
, применяются обычные арифметические преобразования.
Для целых чисел первый шаг этих преобразований называется целочисленным продвижением, и это продвигает любое значение типа меньше, чем int
быть int
,
Другие шаги не относятся к вашему примеру, поэтому я опущу их для краткости.
В выражении numberA + 1
, целочисленные акции применяются. 1
уже int
так что остается без изменений. numberA
имеет тип uint16_t
который уже чем int
в вашей системе, так numberA
получает повышение в int
,
Результат сложения двух int
с другой int
, а также 65535 + 1
дает 65536
так как у вас есть 32-битный int
s.
Итак, ваш первый printf
выводит этот результат.
В соответствии:
numberB = numberA + 1;
вышеуказанная логика все еще применяется к +
оператор, это эквивалентно:
numberB = 65536;
поскольку numberB
имеет тип без знака, uint16_t
В частности, 65536
уменьшается (мод 65536), что дает 0
,
Обратите внимание, что ваши последние два printf
заявления вызывают неопределенное поведение; ты должен использовать %u
для печати unsigned int
, Чтобы справиться с разными размерами int
, ты можешь использовать "%" PRIu32
получить спецификатор формата для uint32_t
,
Когда разрабатывался язык C, было желательно минимизировать количество типов арифметических компиляторов, с которыми приходится иметь дело. Таким образом, большинство математических операторов (например, сложение) поддерживают только int+int, long+long и double+double. Хотя язык можно было бы упростить, опуская int+int (продвигая все long
вместо), арифметика на long
значения обычно занимают в 2-4 раза больше кода, чем арифметика int
ценности; так как в большинстве программ преобладают арифметические int
типы, это было бы очень дорого. Продвижение float
в double
напротив, во многих случаях будет сохранять код, потому что это означает, что для поддержки необходимы только две функции float
: преобразовать в double
и конвертировать из double
, Все остальные арифметические операции с плавающей запятой должны поддерживать только один тип с плавающей запятой, а поскольку математика с плавающей запятой часто выполняется путем вызова библиотечных подпрограмм, стоимость вызова подпрограммы для добавления двух double
значения часто совпадают со стоимостью вызова подпрограммы для добавления двух float
ценности.
К сожалению, язык C получил широкое распространение на различных платформах, прежде чем кто-то действительно понял, что должно означать 0xFFFF + 1, и к тому времени уже были некоторые компиляторы, где выражение дало 65536, а некоторые - ноль. Следовательно, разработчики стандартов старались писать их так, чтобы компиляторы могли продолжать делать то, что они делали, но это было довольно бесполезно с точки зрения тех, кто надеется написать переносимый код. Таким образом, на платформах, где int
32 бита, 0xFFFF+1 даст 65536, и на платформах, где int
16 бит, это даст ноль. Если на какой-то платформе int
Оказалось, что это 17 бит, 0xFFFF+1 позволит компилятору игнорировать законы времени и причинности [кстати, я не знаю, есть ли 17-битные платформы, но есть 32-битные платформы, где uint16_t x=0xFFFF; uint16_t y=x*x;
заставит компилятор искажать поведение кода, который ему предшествует ].
Буквальный 1
в из int
в вашем случае тип int32, поэтому операции с int32 и int16 дают результаты int32.
Иметь результат numberA + 1
заявление как uint16_t
попробуйте явное приведение типа для 1
Например: numberA + (uint16_t)1