То, что вы видите, является результатом того, как базовая машина представляет числа , как стандарт C определяет преобразования типов со знаком в беззнаковое (для арифметики) и как базовая машина представляет числа ( для результата неопределенного поведения в конце).
Когда я первоначально писал свой ответ, я предполагал, что стандарт C не определил явно, как знаковые значения должны быть преобразованы в беззнаковые значения, так как стандарт не определяет, как должны быть представлены знаковые значения или как преобразовывать значения без знака в значения со знаком, если диапазон находится за пределами диапазона со знаком типа .
Однако выясняется, что стандарт явно определяет это при преобразовании значений с отрицательным знаком в положительные значения без знака. В случае целого числа отрицательное значение со знаком x будет преобразовано в UINT_MAX + 1-x, как если бы оно было сохранено как значение со знаком в дополнении к двум, а затем интерпретировано как значение без знака.
Итак, когда вы говорите:
unsigned char a;
unsigned char b;
unsigned int c;
a = 0;
b = -5;
c = a + b;
Значение b становится 251, потому что -5 преобразуется в значение без знака типа UCHAR_MAX-5 + 1 (255-5 + 1) с использованием стандарта C. Затем после этого преобразования происходит добавление. Это делает a + b таким же, как 0 + 251, который затем сохраняется в c. Однако, когда вы говорите:
unsigned char a;
unsigned char b;
unsigned int c;
a = 0;
b = 5;
c = (a-b);
printf("c dec: %d\n", c);
В этом случае a и b переводятся в беззнаковые целые, чтобы соответствовать c, поэтому они остаются в значении 0 и 5. Однако 0 - 5 в математике без знака приводит к ошибке недостаточного значения, которая определяется как результат UINT_MAX + 1-5. Если бы это произошло до продвижения, значением было бы UCHAR_MAX + 1-5 (то есть снова 251).
Однако причина, по которой вы видите -5, напечатанную в выходных данных, является комбинацией того факта, что целые числа без знака UINT_MAX-4 и -5 имеют одинаковое точное двоичное представление, как -5 и 251 с однобайтовыми Тип данных и тот факт, что когда вы использовали "% d" в качестве строки форматирования, это указывало printf интерпретировать значение c как целое число со знаком вместо целого числа без знака.
Поскольку преобразование неподписанных значений в подписанные значения для недопустимых значений не определено, результат становится зависящим от реализации. В вашем случае, поскольку базовый компьютер использует дополнение двух для значений со знаком, в результате значение без знака UINT_MAX-4 становится значением со знаком -5.
Единственная причина, по которой это не происходит в первой программе, потому что как unsigned int, так и unsigned int могут представлять 251, поэтому преобразование между ними хорошо определено, а использование "% d" или "% u" не иметь значение. Во второй программе, однако, это приводит к неопределенному поведению и становится специфичным для реализации, так как ваше значение UINT_MAX-4 вышло за пределы диапазона со знаком int.
Что происходит под капотом
Всегда хорошо перепроверять, что, по вашему мнению, происходит или что должно происходить с тем, что на самом деле происходит, поэтому давайте посмотрим на вывод языка компиляции на компиляторе, чтобы увидеть, что именно происходит. Вот значимая часть первой программы:
mov BYTE PTR [rbp-1], 0 ; a becomes 0
mov BYTE PTR [rbp-2], -5 ; b becomes -5, which as an unsigned char is also 251
movzx edx, BYTE PTR [rbp-1] ; promote a by zero-extending to an unsigned int, which is now 0
movzx eax, BYTE PTR [rbp-2] ; promote b by zero-extending to an unsigned int which is now 251
add eax, edx ; add a and b, that is, 0 and 251
Обратите внимание, что, хотя мы храним значение со знаком -5 в байте b, когда компилятор продвигает его, он продвигает его путем расширения нуля числа, что означает, что оно интерпретируется как значение без знака, которое представляет 11111011 вместо подписанного значение. Затем повышенные значения складываются вместе, чтобы стать c. Именно поэтому стандарт C определяет преобразования со знаком в без знака так, как он это делает - легко реализовать преобразования на архитектурах, которые используют дополнение к двум для значений со знаком.
Теперь с программой 2:
mov BYTE PTR [rbp-1], 0 ; a = 0
mov BYTE PTR [rbp-2], 5 ; b = 5
movzx edx, BYTE PTR [rbp-1] ; a is promoted to 32-bit integer with value 0
movzx eax, BYTE PTR [rbp-2] ; b is promoted to a 32-bit integer with value 5
mov ecx, edx
sub ecx, eax ; a - b is now done as 32-bit integers resulting in -5, which is '4294967291' when interpreted as unsigned
Мы видим, что a и b снова повышаются перед любой арифметикой, поэтому в итоге мы вычитаем два беззнаковых целых числа, что приводит к UINT_MAX-4 из-за недостаточного значения, которое также равно -5 как значение со знаком. Таким образом, независимо от того, интерпретируете ли вы это как вычитание со знаком или без знака, поскольку машина использует форму дополнения до двух, результат соответствует стандарту C без каких-либо дополнительных преобразований.