Почему целочисленные типы повышаются во время добавления в 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-битный ints.

Итак, ваш первый 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

Другие вопросы по тегам