Когда действительно важна подпись целого числа? - PullRequest
4 голосов
/ 07 ноября 2011

Из-за того, как преобразования и операции определяются в C, кажется, что редко имеет значение, используете ли вы подписанную или беззнаковую переменную:

uint8_t u; int8_t i;

u = -3;    i = -3;
u *= 2;    i *= 2;
u += 15;   i += 15;
u >>= 2;   i >>= 2;

printf("%u",u); // -> 2
printf("%u",i); // -> 2

Итак, есть ли набор правил, которые нужно указать вв каких условиях подпись переменной действительно имеет значение?

Ответы [ 6 ]

9 голосов
/ 07 ноября 2011

Это имеет значение в следующих контекстах:

  • деление и по модулю: -2/2 = 1, -2u/2 = UINT_MAX/2-1, -3%4 = -3, -3u%4 = 1
  • сдвиги. Для отрицательных значений со знаком результат >> и << определяется реализацией или не определен, соответственно. Для значений без знака они всегда определены.
  • реляционные -2 < 0, -2u > 0
  • переливается. x+1 > x может считаться компилятором всегда истинным если x имеет тип со знаком.
8 голосов
/ 07 ноября 2011

Да.Подпись повлияет на результат операторов «Больше, чем Меньше, чем» на языке C. Рассмотрим следующий код:

unsigned int a = -5;
unsigned int b = 7;

if (a < b)
    printf("Less");
else
    printf("More");

В этом примере неправильно выводится «Больше», поскольку -5 преобразуется в очень высокийположительное число от компилятора.

Это также повлияет на вашу арифметику с переменными разных размеров.Опять же, рассмотрим этот пример:

unsigned char a = -5;
signed short b = 12;

printf("%d", a+b);

Возвращенный результат 263, не ожидаемый 7. Это потому, что компилятор на самом деле интерпретирует -5 как 251Переполнение заставляет ваши операции работать правильно для переменных того же размера, но при расширении компилятор не расширяет знаковый бит для переменных без знака, поэтому он рассматривает их как их исходное положительное представление в пространстве большего размера.Изучите, как работает комплимент для двоих , и вы увидите, откуда этот результат.

6 голосов
/ 07 ноября 2011

Это влияет на диапазон значений, которые вы можете сохранить в переменной.

3 голосов
/ 07 ноября 2011

Переполнение на целых числах без знака просто переносится. Для значений со знаком это неопределенное поведение, все может произойти.

3 голосов
/ 07 ноября 2011

Это актуально в основном в сравнении.

printf("%d", (u-3) < 0); // -> 0
printf("%d", (i-3) < 0); // -> 1
2 голосов
/ 07 ноября 2011

Подписанность чисел дополнения 2 просто зависит от того, как вы интерпретируете число.Представьте себе 3-битные числа:

000
001
010
011
100
101
110
111

Если вы думаете, что 000 - ноль, а числа - как естественные для людей, вы бы интерпретировали их следующим образом:

000: 0
001: 1
010: 2
011: 3
100: 4
101: 5
110: 6
111: 7

Это называется "целое число без знака".Вы видите все как число, большее или равное нулю.

А что если вы хотите, чтобы некоторые числа были отрицательными?Ну, а 2-е дополнение приходит на помощь.Дополнение 2 известно большинству людей как просто формула, но на самом деле это всего лишь конгруэнтность по модулю 2 ^ n, где n - количество бит в вашем числе.

Позвольте мне привести несколько примеров соответствия:

2 = 5 = 8 = -1 = -4 module 3
-2 = 6 = 14 module 8

Теперь, просто для удобства, допустим, вы решили использовать в качестве знака самый левый бит числа.Итак, вы хотите иметь:

000: 0
001: positive
010: positive
011: positive
100: negative
101: negative
110: negative
111: negative

Просмотр ваших чисел конгруэнтно по модулю 2 ^ 3 (= 8), вы знаете, что:

4 = -4
5 = -3
6 = -2
7 = -1

Следовательно, вы просматриваете свои номера как:

000: 0
001: 1
010: 2
011: 3
100: -4
101: -3
110: -2
111: -1

Как видите, фактические биты для -3 и 5 (например) одинаковы (если число имеет 3 бита).Поэтому написание x = -3 или x = 5 дает тот же результат.

Интерпретация чисел с конгруэнтным модулем 2 ^ n имеет и другие преимущества.Если вы сложите 2 числа, одно отрицательное и одно положительное, на бумаге может случиться так, что у вас будет керри, который будет выброшен, но результат по-прежнему верен.Зачем?Этот перенос был 2 ^ n, который соответствует 0 по модулю 2 ^ n!Разве это не удобно?

Переполнение - это еще один случай конгруэнтности.В нашем примере, если вы сложите два беззнаковых числа 5 и 6, вы получите 3, что на самом деле равно 11.

Итак, почему вы используете подписанные и без знака?Для процессора на самом деле очень мало различий.Для you однако:

  • Если число имеет n битов, беззнаковые представляют числа от 0 до 2 ^ n-1
  • Если число имеет n битов, знак представляет числа от -2 ^ (n-1) до 2 ^ (n-1) -1

Так, например, если вы назначаете -1 для числа без знака, этото же самое, что присвоить ему 2 ^ n-1.

В соответствии с вашим примером это именно то, что вы делаете.вы назначаете -3 для uint8_t, что является недопустимым, но что касается процессора, вы назначаете ему 253.Тогда все остальные операции одинаковы для обоих типов, и в итоге вы получите один и тот же результат.

Однако есть момент, который ваш пример пропускает.оператор >> на подписанном номере расширяет знак при переключении.Так как результат обеих ваших операций равен 9 до сдвига, вы этого не замечаете.Если бы у вас не было +15, вы бы получили -6 в i и 250 в u, что затем >> 2 приведет к -2 в i (при печати с% u, 254) и62 в u.(См. Комментарий Питера Кордеса ниже для некоторых технических деталей)

Чтобы лучше это понять, возьмите следующий пример:

  (signed)101011 (-21) >> 3 ----> 111101 (-3)
(unsigned)101011 ( 43) >> 3 ----> 000101 ( 5)

Если вы заметили, слово (-21/8)на самом деле -3 и пол (43/8) равен 5. Однако -3 и 5 не равны (и не совпадают по модулю 64 (64, потому что есть 6 бит))

...