Почему ((unsigned char) 0x80) <24 получает знак, расширенный до 0xFFFFFFFF80000000 (64-разрядный)?



Следующая программа



#include <inttypes.h> /*  printf(" %" PRIu32 "n"), my_uint32_t) */
#include <stdio.h> /* printf(), perror() */

int main(int argc, char *argv[])
{
uint64_t u64 = ((unsigned char)0x80) << 24;
printf("%" PRIX64 "n", u64);

/* uint64_t */ u64 = ((unsigned int)0x80) << 24;
printf("%016" PRIX64 "n", u64);
}


Производит



FFFFFFFF80000000
0000000080000000


В чем разница между ((unsigned char)0x80) и ((unsigned int)0x80) в этом контексте?

Я предполагаю, что (unsigned char)0x80 повышается до (unsigned char)0xFFFFFFFFFFFFFF80, а затем смещается бит, но почему это преобразование думает, что unsigned char подписано?



Интересно также отметить, что 0x80 << 16 дает ожидаемый результат, 0x0000000000800000.
616   5  

5 ответов:

Левый операнд оператора << подвергается целочисленному продвижению.

(C99, 6.5. 7p3) " целочисленные продвижения выполняются на каждом из операндов."

Это означает следующее выражение:

 ((unsigned char)0x80) << 24

Эквивалентно:

 ((int) (unsigned char)0x80) << 24

Эквивалентно:

  0x80 << 24

, который задает знаковый бит int в 32-разрядной системе int. Затем, когда 0x80 << 24 преобразуется в uint64_t в объявлении u64, расширение знака происходит для получения значения 0xFFFFFFFF80000000.

Редактировать:

Обратите внимание, что какМэтт Макнабб правильно добавил в комментариях, технически 0x80 << 24 вызывает неопределенное поведение в C, поскольку результат не представляется в типе левого операнда <<. Если вы используете gcc, текущая версия компилятора гарантирует , что в данный момент она не делает эту операцию неопределенной.

Компилятор C выполняетцелочисленные продвижения Перед выполнением сдвига.

Правило 6.3.1.1 в стандарте сказано:

Если int может представлять все значения исходного типа, то значение преобразуется в int; в противном случае оно преобразуется в unsigned int. Они называются целочисленными акциями.

Так как все значения unsigned char могут быть представлены int, 0x80 преобразуется в знак int. То же самое не верно в отношении unsigned int: некоторые из его значения не могут быть представлены в виде int, поэтому они остаются unsigned int после применения целочисленных повышений.

Странная часть преобразования происходит при преобразовании результата << из int32 в uint64. Вы работаете с 32-битной системой, поэтому размер целочисленного типа равен 32 битам. Следующий код:

 u64 = ((int) 0x80) << 24;
 printf("%llx\n", u64);

Отпечатки пальцев:

 FFFFFFFF80000000

Потому что (0x80 0x8000000, который является 32-битным представлением -2147483648. Это число преобразуется в 64 бита путем умножения знакового бита и дает 0xFFFFFFFF80000000.

То, что вы наблюдаете, - это неопределенное поведение. C99 §6.5.7/4 описывает сдвиг влево следующим образом:

Результатом E1 << E2 является E1 сдвинутые влево E2 разрядные позиции; освобожденные биты заполняются нулями. Если E1 имеет беззнаковый тип, то значение результата равно E1 × 2E2, уменьшено по модулю на единицу больше максимального значения, представленного в типе результата. Если E1 имеет знаковый тип и неотрицательное значение, и E1 × 2E2 есть представимый в типе результата, то это результирующее значение; в противном случае поведение не определено.

В вашем случае E1 имеет значение 128, и его тип - int, а не unsigned char. Как уже упоминалось в других ответах, значениеповышается до int до оценки. Задействованные операнды имеют знак int, а значение 128 сдвинутых влево 24 места равно 2147483648, что на единицу больше максимального значения, представленного int в вашей системе. Следовательно, поведение вашей программы не определено.

Чтобы избежать этого, вы можете убедиться, что тип E1 является unsigned int путем приведения типа к этому, а не к unsigned char.

Одна из главных трудностей с эволюцией стандарта с заключается в том, что к тому времени, когда были предприняты усилия по стандартизации языка, существовали не только реализации, которые делали определенные вещи по-разному друг от друга, но и был значительный объем кода, написанного для этих реализаций , которые опирались на эти поведенческие различия. Потому что создатели стандарта C хотели избежать запрета реализациям вести себя так, как ведут себя пользователи этих реализаций. можно было бы положиться на то, что некоторые части стандарта C-это настоящий беспорядок. Некоторые из худших аспектов касаются аспектов целочисленного продвижения, таких как тот, который вы наблюдали.

Концептуально, казалось бы, было бы более разумно иметь unsigned char должно способствовать unsigned int, чем signed int, по крайней мере, при использовании в качестве чего-либо иного, чем правый операнд оператора -. Комбинации других операторов могут давать большие результаты, но ни один оператор, кроме -, не может дать отрицательный результат. Чтобы понять, почему signed int был выбран несмотря на то, что результат не может быть отрицательным, рассмотрим следующее:
int i1; unsigned char b1,b2; unsigned int u1; long l1,l2,l3;

l1 = i1+u1;
l2 = i1+b1;
l3 = i1+(b1+b2);

В языке Си нет механизма, с помощью которого операция между двумя различными типами могла бы привести к типу, который не является одним из оригиналов, поэтому первый оператор должен выполнять сложение как со знаком, так и без знака; без знака обычно дает несколько менее удивительные результаты, особенно учитывая, что целочисленные литералы по умолчанию подписаны (было бы очень странно, если бы добавление 1 вместо 1u к беззнаковому значению может сделать его отрицательным). Однако было бы удивительно, если бы третье утверждение могло превратить отрицательное значение i1 в большое беззнаковое число. Если первое утверждение выше дает результат без знака, но третье утверждение дает результат со знаком, то это означает, что (b1+b2) должно быть подписано.

ИМХО, "правильный" способ решить проблемы, связанные с сигнатурностью, - это определить отдельные числовые типы, которые задокументировали поведение "обертывания". (как и нынешние беззнаковые типы), и по сравнению с теми, которые должны вести себя как целые числа,и имеют два типа типов, демонстрирующих различные правила продвижения. Реализации должны были бы поддерживать существующее поведение для кода, использующего существующие типы, но новые типы могли бы реализовать правила, которые были разработаны для удобства использования вместо совместимости.

Comments

    Ничего не найдено.