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