Тип преобразования - неподписанный в подписанный int/char
Я попытался выполнить следующую программу:
#include <stdio.h>
int main() {
signed char a = -5;
unsigned char b = -5;
int c = -5;
unsigned int d = -5;
if (a == b)
printf("\r\n char is SAME!!!");
else
printf("\r\n char is DIFF!!!");
if (c == d)
printf("\r\n int is SAME!!!");
else
printf("\r\n int is DIFF!!!");
return 0;
}
Для этой программы я получаю вывод:
Чар является DIFF!!! Int то же самое!
Почему мы получаем разные результаты для обоих?
Должен ли вывод быть таким, как показано ниже?
Чар это то же самое! Int то же самое!
5 ответов
Это из-за различных неявных правил преобразования типов в C. Программист C должен знать о двух из них: обычные арифметические преобразования и целочисленные повышения (последние являются частью первого).
В случае char у вас есть типы (signed char) == (unsigned char)
, Это оба типа целых чисел. Другие такие маленькие целочисленные типы bool
а также short
, Правила целочисленного продвижения гласят, что всякий раз, когда малый целочисленный тип является операндом операции, его тип будет повышен до int
, который подписан. Это произойдет независимо от того, был ли тип подписан или не подписан.
В случае с signed char
, знак будет сохранен, и он будет повышен до int
содержащий значение -5. В случае с unsigned char
, он содержит значение, которое составляет 251 (0xFB). Он будет повышен до int
содержащий это же значение. Вы в конечном итоге
if( (int)-5 == (int)251 )
В целочисленном случае у вас есть типы (signed int) == (unsigned int)
, Они не являются маленькими целочисленными типами, поэтому целочисленные рекламные акции не применяются. Вместо этого они уравновешиваются обычными арифметическими преобразованиями, в которых говорится, что если два операнда имеют одинаковый "ранг" (размер), но разную подпись, то подписанный операнд преобразуется в тот же тип, что и беззнаковый. Вы в конечном итоге
if( (unsigned int)-5 == (unsigned int)-5)
Классный вопрос!
int
Сравнение работает, потому что оба типа содержат одинаковые биты, поэтому они по сути одинаковы. Но как насчет char
s?
Ах, С неявно способствует char
с int
с в разных случаях. Это одна из них. Ваш код говорит if(a==b)
, но то, что компилятор фактически превращает это в:
if((int)a==(int)b)
(int)a
это -5, но (int)b
это 251. Это определенно не то же самое.
РЕДАКТИРОВАТЬ: Как @Carbonic-Acid указал, (int)b
251, только если char
длиной 8 бит. Если int
длиной 32 бита, (int)b
это -32764.
РЕДИТ: Есть целая куча комментариев, обсуждающих природу ответа, если байт не имеет длины 8 бит. Единственная разница в этом случае заключается в том, что (int)b
это не 251, а другое положительное число, которое не равно -5. Это не очень актуально для вопроса, который все еще очень крут.
Добро пожаловать в целочисленное продвижение. Если я могу процитировать с сайта:
Если int может представлять все значения исходного типа, значение преобразуется в int; в противном случае он конвертируется в беззнаковое целое. Они называются целочисленными акциями. Все остальные типы не изменяются целочисленными акциями.
Си может быть очень запутанным, когда вы делаете такие сравнения, я недавно озадачил некоторых моих друзей, не занимающихся программированием на С, следующим дразнить:
#include <stdio.h>
#include <string.h>
int main()
{
char* string = "One looooooooooong string";
printf("%d\n", strlen(string));
if (strlen(string) < -1) printf("This cannot be happening :(");
return 0;
}
Который действительно печатает This cannot be happening :(
и, казалось бы, демонстрирует, что 25 меньше, чем -1!
Однако, что происходит внизу, это то, что -1 представляется как целое число без знака, которое из-за базового представления битов равно 4294967295 в 32-битной системе. И, естественно, 25 меньше, чем 4294967295.
Однако, если мы явно приведем size_t
тип, возвращаемый strlen
как целое число со знаком:
if ((int)(strlen(string)) < -1)
Тогда он будет сравнивать 25 с -1, и все будет хорошо с миром.
Хороший компилятор должен предупредить вас о сравнении между целым числом без знака и со знаком, и все же его так легко пропустить (особенно если вы не включаете предупреждения).
Это особенно сбивает с толку программистов на Java, так как все примитивные типы там подписаны. Вот что сказал по этому поводу Джеймс Гослинг (один из создателей Java):
Гослинг: Для меня, как для дизайнера языков, который я не считаю себя таким, как сейчас, то, что в действительности означало "простое", было то, мог ли я ожидать, что J. Random Developer будет держать спецификацию в своей голове. Это определение говорит, что, например, Java не является - и на самом деле многие из этих языков заканчиваются множеством угловых случаев, вещей, которые на самом деле никто не понимает. Опросите любого разработчика на C о unsigned, и довольно скоро вы обнаружите, что почти никто из разработчиков C не понимает, что происходит с unsigned, что такое беззнаковая арифметика. Такие вещи делали Си сложным. Я думаю, что языковая часть Java довольно проста. Библиотеки, которые вы должны искать.
Шестнадцатеричное представление -5
является:
- 8-бит, два дополнения
signed char
:0xfb
- 32-разрядный, два дополнения
signed int
:0xfffffffb
Когда вы конвертируете число со знаком в число без знака или наоборот, компилятор... ничего не делает. Чем там можно заняться? Число может быть конвертируемым или нет, и в этом случае следует неопределенное или определяемое реализацией поведение (я на самом деле не проверял, какое), и наиболее эффективное поведение, определяемое реализацией, - ничего не делать.
Итак, шестнадцатеричное представление (unsigned <type>)-5
является:
- 8-битный,
unsigned char
:0xfb
- 32-битный,
unsigned int
:0xfffffffb
Выглядит знакомо? Они по крупицам такие же, как подписанные версии.
Когда ты пишешь if (a == b)
, где a
а также b
имеют тип char
что компилятор на самом деле должен прочитать if ((int)a == (int)b)
, (Это то "целочисленное продвижение", о котором все остальные стучатся.)
Итак, что происходит, когда мы конвертируем char
в int
?
- 8-разрядный
signed char
до 32-разрядногоsigned int
:0xfb
->0xfffffffb
- Ну, это имеет смысл, потому что это соответствует представлениям
-5
выше! - Он называется "знак-расширение", потому что он копирует верхний бит байта, "знак-бит", влево в новое, более широкое значение.
- Ну, это имеет смысл, потому что это соответствует представлениям
- 8-разрядный
unsigned char
до 32-разрядногоsigned int
:0xfb
->0x000000fb
- На этот раз он выполняет "расширение с нуля", потому что тип источника не имеет знака, поэтому нет знака-бита для копирования.
Так, a == b
действительно делает 0xfffffffb == 0x000000fb
=> нет совпадений!
А также, c == d
действительно делает 0xfffffffb == 0xfffffffb
=> соответствовать!
Моя точка зрения: вы не получили предупреждение во время компиляции "сравнение подписанного и неподписанного выражения"?
Компилятор пытается сообщить вам, что он имеет право делать сумасшедшие вещи!:) Я бы добавил, что сумасшедшие вещи будут происходить с использованием больших значений, близких к возможностям примитивного типа. А также
unsigned int d = -5;
присваивает определенно большое значение d, эквивалентно (даже если, вероятно, не гарантировано эквивалентно) быть:
unsigned int d = UINT_MAX -4; ///Since -1 is UINT_MAX
Редактировать:
Однако интересно отметить, что только второе сравнение дает предупреждение (проверьте код). Таким образом, это означает, что компилятор, применяющий правила преобразования, уверен, что при сравнении unsigned char
а также char
(во время сравнения они будут преобразованы в тип, который может безопасно представлять все его возможные значения). И он прав в этом вопросе. Затем он сообщает вам, что это не будет иметь место для unsigned int
а также int
: во время сравнения один из 2 будет преобразован в тип, который не может полностью его представить.
Для полноты я проверил это также для краткости: компилятор ведет себя так же, как и для символов, и, как и ожидалось, ошибок во время выполнения нет.
,
В связи с этой темой я недавно задал этот вопрос (пока что ориентирован на C++).