Подтвердить что ты не робот

Почему при добавлении в 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? Или это какая-то странность компилятора/аппаратного обеспечения?

4b9b3361

Ответ 1

Итак, кажется, что результат numberA + 1 был повышен до uint32_t

операнды добавления были продвинуты до int до того, как произошло добавление, и результат добавления был того же типа, что и эффективные операнды (int).

В самом деле, если int имеет 32-разрядную ширину на вашей платформе компиляции (это означает, что тип, который представляет uint16_t, имеет более низкий "ранг преобразования", чем int), тогда numberA + 1 вычисляется как int > дополнение между 1 и продвинутым numberA как часть правил целочисленной рекламы, 6.3.1.1-2 в стандарте C11:

В выражении могут использоваться следующие выражения: int... или int unsigned int: [...] Объект или выражение с целым типом (отличным от int или unsigned int), целочисленный ранг преобразования которого меньше или равен ранг int и unsigned int.

[...]

Если int может представлять все значения исходного типа [...], значение преобразуется в int

В вашем случае unsigned short, который, по всей вероятности, означает, что uint16_t определен как на вашей платформе, имеет все его значения, представляемые как элементы int, поэтому значение unsigned short numberA получает повышение до int, когда это происходит в арифметической операции.

Ответ 2

Для арифметических операторов, таких как +, применяются обычные арифметические преобразования.

Для целых чисел первый шаг этих преобразований называется целыми рекламными акциями, и это означает, что любое значение типа меньше 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 уменьшается (mod 65536), что дает 0.

Обратите внимание, что последние два оператора printf вызывают поведение undefined; вы должны использовать %u для печати unsigned int. Чтобы справиться с различными размерами int, вы можете использовать "%" PRIu32, чтобы получить спецификатор формата для uint32_t.

Ответ 3

Когда язык 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 разрешило бы компилятору отрицать законы времени и причинности [btw, я не знаю, есть ли какие-либо 17-битные платформы, но есть 32-битные платформы, где uint16_t x=0xFFFF; uint16_t y=x*x; заставит компилятор исказить поведение предшествующего ему кода].

Ответ 4

Literal 1 in int, то есть в вашем случае тип int32, поэтому операции с int32 и int16 дают результаты int32.

Чтобы получить результат numberA + 1 в качестве uint16_t попробовать явный тип cast для 1, например: numberA + (uint16_t)1