2011-11-07 2 views
4

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

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 

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

+3

Ну, это будет зависеть от того, нужна ли вам целая переменная принимать отрицательные значения ... –

+2

'printf («% u », i)' is wrong (неопределенное поведение даже, IIRC). – delnan

+1

Это довольно старая и ценная статья, написанная Скоттом Мейерсом давно: http://www.aristeia.com/Papers/C++ReportColumns/sep95.pdf – AraK

ответ

8

Это имеет значение в этом контексте:

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

Это действительно странно с предположением о переполнении, но я это проверил. Мне кажется, что это должно быть неверно, когда x + 1 переполняется, но компилятор действительно игнорирует это. –

+0

@NicholasFlynt: Это красота неопределенного поведения. Переполняемое целое число со знаком вызывает неопределенное поведение, когда компилятор может делать все, что захочет. Поэтому он утверждает 'x + 1> x', потому что он может, хотя фактическое вычисление показало бы иначе. В то же время он выполняется для непереполняющих вычислений, поэтому компилятор может постоянно утверждать 'x + 1> x', что приводит к упрощению программы. – jpalecek

+0

Если этот ответ верен, он выглядит как полный список. Поэтому я принял это и поддержал другие хорошие ответы. – AndreKR

3

В основном это касается сравнения.

printf("%d", (u-3) < 0); // -> 0 
printf("%d", (i-3) < 0); // -> 1 
6

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

+0

Нет, это не так. Я могу хранить 129 в int8_t без каких-либо проблем, пока я интерпретирую его как unsigned при его использовании. – AndreKR

+1

@AndreKR: И, переосмыслив это, вы * как минимум * сбиваете с толку всех. Это может быть даже определенное реализацией или неопределенное поведение (хотя я бы не стал спорить об этом перед тем, как посоветоваться с юристом по языку). Система типа C уже достаточно слаба, не нужно делать тип средним даже меньше. – delnan

+1

@AndreKR: Это будет неопределенное поведение. – jpalecek

8

Да. Знаковость будет влиять на результат больше и меньше, чем операторы в С. Рассмотрим следующий код:

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 компилятором. Переполнение делает ваши операции корректными для переменных одного размера, но при расширении компилятор не расширяет знаковый бит для неподписанных переменных, поэтому он рассматривает их как их исходное положительное представление в пространстве большего размера. Изучите, как работает two's compliment, и вы увидите, откуда этот результат.

+0

В частности, это повлияет на то, какие коды операций используются для сравнения переменной. Внутреннее представление для целых чисел не изменяется - какие изменения заключаются в том, как компилятор интерпретирует результат сравнения. –

+0

Вы правы, с результатом, но я бы назвал это «неправильным». Хранение отрицательного значения в 'unsigned' должно сказать вам, что должно быть что-то не совсем тривиальное: :) –

3

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

+0

Хотя я нахожу теоретически возможным, что * что-либо может случиться, мне трудно поверить в возможность того, что * все * может бывает. Это был бы один неудачный компилятор ... –

+0

все, что может случиться, включает в себя возможность того, что даже все может произойти, нет?) –

+0

@JensGustedt: Все происходит * включает * обе * Землю, сталкивающуюся с Луной и Землей, теряющую Луну в глубоком космосе? – jpalecek

2

Подпись номера дополнений 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^п, что соответствует 0 по модулю 2^п! Разве это не удобно?

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

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

  • Если число имеет п бит, без знака представляет собой число от 0 до 2^п-1
  • Если число имеет п битов, подписанная представляет число от -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.(Смотрите комментарий Peter Корд ниже на несколько тонкостей)

Чтобы лучше это понять, принять этот пример:

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

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

+0

В C это определение реализации, которое '>>' делает для отрицательных целых чисел. На машинах, имеющих арифметические операторы смены вправо, этот сдвиг в копиях знакового бита использует его программисты-компиляторы. Для кода было бы совершенно законно компилировать '>>' в логический сдвиг вправо, сдвигая нули. Это позволяет эффективно использовать C на других машинах, но недостатком является то, что нет необходимости требовать арифметического сдвига вправо, когда это то, что вам нужно. Вот почему всегда рекомендуется выполнять бит-сдвиг на неподписанных типах, чтобы убедиться, что вы получите тот же результат во всех реализациях C. –

+0

Кроме того, C не требует целых чисел со знаком со знаком 2, которые, по моему мнению, являются более важной причиной для того, чтобы оставить подписанные смены небезопасными. –

+0

@PeterCordes, спасибо, я упомянул ваш комментарий в ответе. – Shahbaz

Смежные вопросы