Преобразование без знака в C - всегда ли это безопасно?

135

Предположим, у меня есть следующий C-код.

unsigned int u = 1234;
int i = -5678;

unsigned int result = u + i;

Какие неявные преобразования здесь происходят, и безопасен ли этот код для всех значений uи i? (Безопасно, в том смысле, что, хотя результат в этом примере будет переполнен до некоторого огромного положительного числа, я мог бы привести его обратно к int и получить реальный результат.)

cwick
источник

Ответы:

223

Короткий ответ

Вы iбудете преобразованы в целое число без знака путем добавления UINT_MAX + 1, затем добавление будет выполняться со значениями без знака, в результате чего получится большое значение result(в зависимости от значений uи i).

Длинный ответ

Согласно стандарту C99:

6.3.1.8 Обычные арифметические преобразования

  1. Если оба операнда имеют одинаковый тип, дальнейшее преобразование не требуется.
  2. В противном случае, если оба операнда имеют целочисленные типы со знаком или оба имеют целочисленные типы без знака, операнд с типом ранга преобразования с меньшим целым числом преобразуется в тип операнда с большим рангом.
  3. В противном случае, если операнд с целочисленным типом без знака имеет ранг, больший или равный рангу типа другого операнда, тогда операнд с целочисленным типом со знаком преобразуется в тип операнда с целочисленным типом без знака.
  4. В противном случае, если тип операнда с целочисленным типом со знаком может представлять все значения типа операнда с целочисленным типом без знака, тогда операнд с целочисленным типом без знака преобразуется в тип операнда с целочисленным типом со знаком.
  5. В противном случае оба операнда преобразуются в тип целого без знака, соответствующий типу операнда с целым типом со знаком.

В вашем случае у нас есть один неподписанный int ( u) и подписанный int ( i). Ссылаясь на (3) выше, так как оба операнда имеют одинаковый ранг, вам iнужно будет преобразовать в целое число без знака.

6.3.1.3 Целые числа со знаком и без знака

  1. Когда значение с целочисленным типом преобразуется в другой целочисленный тип, отличный от _Bool, если значение может быть представлено новым типом, оно не изменяется.
  2. В противном случае, если новый тип является беззнаковым, значение преобразуется путем многократного сложения или вычитания на единицу больше максимального значения, которое может быть представлено в новом типе, до тех пор, пока значение не окажется в диапазоне нового типа.
  3. В противном случае новый тип подписывается, и значение не может быть представлено в нем; либо результат определяется реализацией, либо определяется сигнал реализации.

Теперь нам нужно сослаться на (2) выше. Ваш iбудет преобразован в беззнаковое значение, добавив UINT_MAX + 1. Таким образом, результат будет зависеть от того, как UINT_MAXопределяется ваша реализация. Он будет большим, но не переполнится, потому что:

6.2.5 (9)

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

Бонус: Арифметическое Преобразование Полу-WTF

#include <stdio.h>

int main(void)
{
  unsigned int plus_one = 1;
  int minus_one = -1;

  if(plus_one < minus_one)
    printf("1 < -1");
  else
    printf("boring");

  return 0;
}

Вы можете использовать эту ссылку, чтобы попробовать это онлайн: https://repl.it/repls/QuickWhimsicalBytes

Бонус: Арифметический конверсионный побочный эффект

Правила арифметического преобразования можно использовать для получения значения UINT_MAXпутем инициализации значения без знака -1, то есть:

unsigned int umax = -1; // umax set to UINT_MAX

Это гарантированно будет переносимым независимо от числового представления системы со знаком из-за правил преобразования, описанных выше. См. Этот вопрос SO для получения дополнительной информации: безопасно ли использовать -1, чтобы установить все биты в true?

Озгур Озцитак
источник
Я не понимаю, почему он не может просто сделать абсолютное значение, а затем трактовать как без знака, как с положительными числами?
Хосе Сальватьерра
7
@ D.Singh Можете ли вы указать на неправильные части в ответе?
Кошка
Для преобразования подписанного в беззнаковое мы добавляем максимальное значение беззнакового значения (UINT_MAX +1). Точно так же, как можно легко конвертировать неподписанные в подписанные? Нужно ли вычитать данное число из максимального значения (256 в случае беззнакового символа)? Например: 140 при преобразовании в число со знаком становится -116. Но 20 становится 20 само по себе. Так какой-нибудь легкий трюк здесь?
Джон Уилок
@JonWheelock см .: stackoverflow.com/questions/8317295/…
Озгур Озцитак
24

Преобразование из подписанного в неподписанное не обязательно просто копирует или интерпретирует представление подписанного значения. Цитируя стандарт C (C99 6.3.1.3):

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

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

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

Для представления дополнения к двум, которое в наши дни является почти универсальным, правила соответствуют переосмыслению битов. Но для других представлений (знак-и-величина или их дополнение) реализация C должна все же обеспечить тот же результат, что означает, что преобразование не может просто скопировать биты. Например, (без знака) -1 == UINT_MAX, независимо от представления.

В общем, преобразования в C определены для работы со значениями, а не с представлениями.

Чтобы ответить на оригинальный вопрос:

unsigned int u = 1234;
int i = -5678;

unsigned int result = u + i;

Значение i преобразуется в unsigned int, давая UINT_MAX + 1 - 5678. Это значение затем добавляется к значению без знака 1234, давая UINT_MAX + 1 - 4444.

(В отличие от переполнения без знака, переполнение со знаком вызывает неопределенное поведение. Обтекание является обычным, но не гарантируется стандартом C - и оптимизация компилятора может нанести ущерб коду, который делает необоснованные предположения.)


источник
5

Ссылаясь на Библию :

  • Ваша операция добавления вызывает преобразование int в unsigned int.
  • Предполагая представление в виде дополнения до двух и одинакового размера, битовая комбинация не изменяется.
  • Преобразование из неподписанного int в подписанное int зависит от реализации. (Но, вероятно, в наши дни это работает так, как вы ожидаете на большинстве платформ.)
  • Правила немного сложнее в случае объединения подписанных и неподписанных разных размеров.
SMH
источник
3

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

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

Матс Фредрикссон
источник
Не правда. 6.3.1.8 Обычные арифметические преобразования Если вы суммируете int и unsigned char, последний преобразуется в int. Если вы сложите два неподписанных символа, они преобразуются в int.
2501
3

При преобразовании из подписанного в неподписанное есть две возможности. Числа, которые изначально были положительными, остаются (или интерпретируются как) одинаковыми значениями. Число, которое изначально было отрицательным, теперь будет интерпретироваться как большие положительные числа.

Тим Ринг
источник
1

Как уже было сказано ранее, вы можете без проблем переходить между подписанным и неподписанным. Пограничный регистр для целых чисел со знаком равен -1 (0xFFFFFFFF). Попробуйте сложить и вычесть из этого, и вы обнаружите, что можете отбросить и сделать это правильно.

Тем не менее, если вы собираетесь выполнять приведение типа «вперед-назад», я настоятельно рекомендую назвать ваши переменные так, чтобы было ясно, к какому типу они относятся, например:

int iValue, iResult;
unsigned int uValue, uResult;

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

Тейлор Прайс
источник
0

Какие неявные преобразования происходят здесь,

я буду преобразован в целое число без знака.

и является ли этот код безопасным для всех значений u и i?

Безопасно в смысле четкого определения да (см. Https://stackoverflow.com/a/50632/5083516 ).

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

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

деление и приведение к большим целым типам без знака будут иметь четко определенные результаты, но эти результаты не будут дополнительными представлениями «реального результата».

(Безопасно, в том смысле, что, хотя результат в этом примере будет переполнен до некоторого огромного положительного числа, я мог бы привести его обратно к int и получить реальный результат.)

В то время как преобразования из подписанного в беззнаковое определяются стандартом, обратное определяется реализацией, и gcc и msvc определяют преобразование таким образом, что вы получите «реальный результат» при преобразовании числа дополнения, хранящегося в целом без знака, обратно в целое число со знаком , Я ожидаю, что вы обнаружите любое другое поведение только в неясных системах, которые не используют дополнение 2 для целых чисел со знаком.

https://gcc.gnu.org/onlinedocs/gcc/Integers-implementation.html#Integers-implementation https://msdn.microsoft.com/en-us/library/0eex498h.aspx

plugwash
источник
-17

Ужасные ответы в изобилии

Озгур Озцитак

При приведении от подписи к неподписанию (и наоборот) внутреннее представление числа не изменяется. Что меняется, так это то, как компилятор интерпретирует знаковый бит.

Это совершенно неправильно.

Матс Фредрикссон

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

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

SMH

Ваша операция добавления вызывает преобразование int в unsigned int.

Неправильно. Может быть, да, а может и нет.

Преобразование из неподписанного int в подписанное int зависит от реализации. (Но, вероятно, в наши дни это работает так, как вы ожидаете на большинстве платформ.)

Неправильно. Это либо неопределенное поведение, если оно вызывает переполнение, либо значение сохраняется.

анонимное

Значение i преобразуется в беззнаковое целое ...

Неправильно. Зависит от точности int относительно неподписанного int.

Тейлор Прайс

Как уже было сказано ранее, вы можете без проблем переходить между подписанным и неподписанным.

Неправильно. Попытка сохранить значение вне диапазона целого числа со знаком приводит к неопределенному поведению.

Теперь я наконец могу ответить на вопрос.

Если точность int равна unsigned int, вы будете переведены в int со знаком, и вы получите значение -4444 из выражения (u + i). Теперь, если у вас и у меня есть другие значения, вы можете получить переполнение и неопределенное поведение, но с этими точными числами вы получите -4444 [1] . Это значение будет иметь тип int. Но вы пытаетесь сохранить это значение в unsigned int, чтобы затем оно было приведено к unsigned int, и в результате получилось бы значение (UINT_MAX + 1) - 4444.

Если точность unsigned int будет больше, чем точность int, подписанное int будет преобразовано в unsigned int, что даст значение (UINT_MAX + 1) - 5678, которое будет добавлено к другому unsigned int 1234. Если вы и я имеем другие значения, из-за которых выражение выходит за пределы диапазона {0..UINT_MAX}, значение (UINT_MAX + 1) будет либо добавляться, либо вычитаться до тех пор, пока результат не попадет в диапазон {0..UINT_MAX) и не произойдет неопределенное поведение ,

Что такое точность?

Целые числа имеют биты заполнения, знаковые биты и биты значений. Целые числа без знака, очевидно, не имеют знакового бита. Также гарантируется, что беззнаковый символ не будет иметь битов заполнения. Число битов значений, которые имеет целое число, это то, сколько точности оно имеет.

[Gotchas]

Макрос sizeof macro сам по себе не может использоваться для определения точности целого числа, если присутствуют биты заполнения. И размер байта не должен быть октетом (восемь битов), как определено C99.

[1] Переполнение может произойти в одной из двух точек. Либо перед добавлением (во время продвижения) - если у вас есть неподписанное целое число, которое слишком велико, чтобы поместиться в нем. Переполнение может также произойти после добавления, даже если unsigned int находилось в диапазоне int, после добавления результат может все еще переполниться.

Элит Мх
источник
6
Msgstr "Беззнаковые целые могут быть повышены до целых". Не правда. Целочисленное продвижение не происходит, так как типы уже имеют ранг> = int. 6.3.1.1: «Ранг любого целого типа без знака должен совпадать с рангом соответствующего целого типа со знаком, если таковой имеется». и 6.3.1.8: «В противном случае, если операнд с целым типом без знака имеет ранг, больший или равный рангу типа другого операнда, то операнд с целым типом со знаком преобразуется в тип операнда с целым числом без знака тип." оба гарантируют, что intпреобразуются в то время, unsigned intкогда применяются обычные арифметические преобразования.
CB Bailey
1
6.3.1.8 Происходит только после целочисленного продвижения. Первый абзац гласит: «В противном случае целочисленные преобразования выполняются для обоих операндов. Затем следующие правила применяются к повышенным операндам». Поэтому прочитайте правила продвижения 6.3.1.1 ... «Объект или выражение с целочисленным типом, чей целочисленный коэффициент преобразования меньше или равен EQUAL для ранга int и unsigned int» и «Если int может представлять все значения Исходный тип, значение преобразуется в int ".
Elite Mx
1
6.3.1.1. Целочисленное продвижение используется для преобразования некоторых целочисленных типов, которые не относятся intни unsigned intк одному из тех типов, где что-то типа unsigned intили intожидается. «Или равно» было добавлено в TC2, чтобы позволить перечисленным типам ранга преобразования равным intили unsigned intбыть преобразованным в один из этих типов. Никогда не предполагалось, что описанная реклама будет конвертирована между unsigned intи int. Определение общего типа между unsigned intи intпо-прежнему регулируется 6.3.1.8, даже после TC2.
CB Bailey
19
Публикация неправильных ответов при критике неправильных ответов других не является хорошей стратегией для получения работы ... ;-)
R .. GitHub ОСТАНОВИТЬ ПОМОЩЬ ЛЬДУ
6
Я не голосую за удаление, поскольку этот уровень неправильности в сочетании с высокомерием слишком интересен
ММ