Преобразование без знака в C - всегда ли это безопасно? - PullRequest
120 голосов
/ 09 сентября 2008

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

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

unsigned int result = u + i;

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

Ответы [ 8 ]

198 голосов
/ 09 сентября 2008

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

Ваше 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?

19 голосов
/ 07 мая 2009

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

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

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

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

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

Как правило, преобразования в Си определены для работы со значениями, а не с представлениями.

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

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

unsigned int result = u + i;

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

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

4 голосов
/ 09 сентября 2008

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

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

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

3 голосов
/ 09 сентября 2008

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

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

1 голос
/ 09 сентября 2008

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

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

int iValue, iResult;
unsigned int uValue, uResult;

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

0 голосов
/ 07 февраля 2016

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

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

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

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

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

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

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

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

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

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

0 голосов
/ 08 июля 2010

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

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

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

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

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

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

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

SMH

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

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

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

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

Anonymous

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

Неправильно. Зависит от точности 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. Если u и у меня есть другие значения, которые заставляют выражение выходить за пределы диапазона {0..UINT_MAX}, значение (UINT_MAX + 1) будет либо добавлено, либо вычтено, пока результат не попадет в диапазон {0..UINT_MAX) и не будет неопределенным поведение будет происходить.

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

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

[Gotchas]

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

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


На несвязанной ноте я недавно аспирант, пытающийся найти работу;)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...