Как работает unsigned?

Недавно начал изучать С и познакомился с таким классификатором как unsigned.

#include <stdio.h>

int main() {
    unsigned int a;
    a = -3;
    printf("%d", a + -10);
}

Выводится число -13. Почему? Ведь переменные с unsigned не содержат информацию о знаке?


Ответы (1 шт):

Автор решения: eanmos

Смотрите, в чем дело. Эта программа содержит неопределенное поведение из-за несоответствия спецификаторов формата и фактических аргументов в вызове функции printf. Стандарт не налагает никаких ограничений на поведение программы, содержащей неопределенное поведение. Однако мы можем по-рассуждать, почему в вашем конкретном случае программа выводит -13.


unsigned int a;
a = -3;

Оператор = называется simple assignment operator. Вот, что происходит в строке с присваиванием:

  1. Тип assignment expression (т. е. всего выражения a = -3) является типом выражения с левой стороны (т. е. типом aunsigned int).

  2. Выражение справа преобразуется в тип assignment expression и затем замещает значение в объекте, на который указывает операнд слева (т. е. a).

    Другими словами выражение справа типа int неявно преобразуется в тип unsigned int. По правилам значение конвертируется с помощью добавления к нему UINT_MAX + 11, 2, согласно § 6.3.1.3 / 23.

Т. е. в переменную a никогда не попадет значение -3 типа int, оно будет неявно преобразовано в тип unsigned int перед присваиванием. А вот что получится в результате этого неявного преобразование зависит от значения UINT_MAX, которое само уже зависит от реализации. Другими словами, мы можем считать, что в a сейчас хранится -3 + 1 + UINT_MAX.

Далее — выражение a - 10. Здесь имеют место usual arithmetic conversions (§ 6.3.1.8), в ходе которых операнд 10 типа signed int преобразуется в unsigned int. Путем несложных подсчетов можно выяснить, что в итоге функции передается значение (-3 + 1 + UINT_MAX) - 104.

Далее. При вызове функции с переменным числом аргументов (каковой и является printf) происходит процесс, который называется default argument promotions (§ 6.5.2.2 / 6), при котором каждый аргумент проходит преобразование в некоторый тип и только затем, в преобразованном виде, передается на вход функции. Согласно правилам таких преобразований аргумент a в данном случае не будет преобразован ни в какой другой тип, т. е. останется типа unsigned int.

Здесь возникает проблема: функция printf ожидает на входе тип int (потому что мы указали спецификатор %d в формате), но мы её на вход передаем тип unsigned int. Все, что произойдет далее зависит от того, как сложатся звезды как реализована функция printf.

Можем сделать некоторые предположения. Пусть UINT_MAX равно 4294967295 и на нашей платформе используется two's complement. Тогда в printf передается значение (-3 + 1 + UINT_MAX) - 10, что равно 4294967283. Объектное представление беззнаковых целых в C — это просто их двоичная запись. В данном случае — это 4 байта 0xFFFFFFF3. Если эти 4 байта интерпретировать как int в two's complement (что и вынуждена делать функция printf в нашем примере), то получится как раз -13.


1 UINT_MAX — это в случае unsigned int.

2 На этом, кстати, основан трюк с получением значения UINT_MAX: unsigned int a = -1;.

3 Все ссылки в данном ответе предполагают рабочий черновик стандарта ISO/IEC 9899:202x (E) от 7-го июня 2019 года.

4 Как правильно заметил @wololo в комментариях, передается на самом деле (-3 + 1 + UINT_MAX) + (-10 + 1 + UINT_MAX), т. к. (int) -10 тоже подвергается преобразованию в unsigned int. На результат этот не повлияет и для простоты вычислений закроем глаза на этот нюанс.

→ Ссылка