Подтвердить что ты не робот

Почему ((без знака 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.

4b9b3361

Ответ 1

Левый операнд оператора << претерпевает целую рекламу.

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

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

 ((unsigned char)0x80) << 24

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

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

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

  0x80 << 24

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

EDIT:

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

Ответ 2

C компилятор выполняет целые рекламные акции перед выполнением сдвига.

Правило 6.3.1.1 стандарта гласит:

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

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

Ответ 3

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

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

печатает:

 FFFFFFFF80000000

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

Ответ 4

Что вы наблюдаете, это поведение undefined. C99 & sect; 6.5.7/4 описывает смещение влево следующим образом:

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

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

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

Ответ 5

Одна из основных трудностей с эволюцией стандарта C состоит в том, что к тому времени, когда были предприняты усилия по стандартизации языка, существовали не только реализации, которые делали определенные вещи по-другому друг от друга, но существовал значительный код кода, написанный для те реализации, которые основывались на этих поведенческих различиях. Поскольку создатели стандарта 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);

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

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