Почему имеет значение, используются ли сдвиги влево и вправо вместе в одном выражении или нет?

У меня такой код:

unsigned char x = 255;
printf("%x\n", x); // ff

unsigned char tmp = x << 7;
unsigned char y = tmp >> 7;
printf("%x\n", y); // 1

unsigned char z = (x << 7) >> 7;
printf("%x\n", z); // ff

Я ожидал, что y и z будут одинаковыми. Но они различаются в зависимости от того, используется ли промежуточная переменная. Было бы интересно узнать, почему это так.


person odzhychko    schedule 22.05.2020    source источник
comment
(x<<7)>>7 в принципе также сохраняет промежуточный результат. Но я не знаю, где написано, какого типа должен быть этот промежуточный результат.   -  person The Photon    schedule 22.05.2020
comment
@ThePhoton: в стандарте C говорится, что промежуточный тип, используемый для оценки (x << 7) >> 7, равен int или unsigned int в зависимости от размеров unsigned char и int.   -  person chqrlie    schedule 22.05.2020


Ответы (3)


Этот небольшой тест на самом деле более тонкий, чем кажется, поскольку поведение определяется реализацией:

  • unsigned char x = 255; здесь нет двусмысленности, x - это unsigned char со значением 255, тип unsigned char гарантированно имеет достаточный диапазон для хранения 255.

  • printf("%x\n", x); Это дает ff на стандартный вывод, но было бы чище написать printf("%hhx\n", x);, поскольку printf ожидает unsigned int для преобразования %x, а x - нет. Передача x может фактически передать int или unsigned int аргумент.

  • unsigned char tmp = x << 7; Чтобы оценить выражение x << 7, x, являющееся unsigned char, сначала проходит целочисленное повышение, определенное в стандарте C 6.3.3.1: Если int может представлять все значения исходный тип (ограниченный шириной для битового поля), значение преобразуется в int; в противном случае он преобразуется в unsigned int. Это называется целочисленными рекламными акциями.

    Таким образом, если количество битов значения в unsigned char меньше или равно int (наиболее распространенный случай в настоящее время 8 против 31), x сначала повышается до int с тем же значением, которое затем сдвигается влево на 7 позиций. Результат, 0x7f80, гарантированно соответствует типу int, поэтому поведение четко определено, и преобразование этого значения в тип unsigned char эффективно усекает старшие биты значения. Если тип unsigned char имеет 8 бит, значение будет 128 (0x80), но если тип unsigned char имеет больше бит, значение в tmp может быть 0x180, 0x380, 0x780, 0xf80, 0x1f80, 0x3f80 или даже 0x7f80.

    Если тип unsigned char больше, чем int, что может происходить в редких системах, где sizeof(int) == 1, x повышается до unsigned int, и для этого типа выполняется сдвиг влево. Значение равно 0x7f80U, что гарантированно соответствует типу unsigned int, и сохранение его в tmp фактически не приводит к потере информации, поскольку тип unsigned char имеет тот же размер, что и unsigned int. Таким образом, tmp в этом случае будет иметь значение 0x7f80.

  • unsigned char y = tmp >> 7; Оценка выполняется так же, как указано выше, tmp повышается до int или unsigned int в зависимости от системы, которая сохраняет его значение, и это значение сдвигается вправо на 7 позиций, что полностью определено, поскольку 7 меньше ширины типа (int или unsigned int) и значение положительное. В зависимости от количества битов типа unsigned char значение, хранимое в y, может быть 1, 3, 7, 15, 31, 63, 127 или 255, наиболее распространенная архитектура будет иметь y == 1.

  • printf("%x\n", y); опять же, было бы лучше написать printf("%hhx\n", y);, и результат может быть 1 (наиболее частый случай) или 3, 7, f, 1f, 3f, 7f или ff в зависимости от количества битов значения в типе unsigned char.

  • unsigned char z = (x << 7) >> 7; Целочисленное продвижение выполняется на x, как описано выше, значение (255) затем сдвигается влево на 7 битов как int или unsigned int, всегда дает 0x7f80, а затем сдвигается вправо на 7 позиций с конечным значением 0xff. Это поведение полностью определено.

  • printf("%x\n", z); Еще раз, строка формата должна быть printf("%hhx\n", z);, а на выходе всегда будет ff.

Системы, в которых байты имеют более 8 бит, в наши дни становятся редкостью, но некоторые встроенные процессоры, такие как специализированные DSP, все еще делают это. При передаче unsigned char в качестве %x спецификатора преобразования потребовалась бы извращенная система, но лучше использовать %hhx или более переносимо писать printf("%x\n", (unsigned)z);

В этом примере переключение на 8 вместо 7 было бы еще более надуманным. Он будет иметь неопределенное поведение в системах с 16-битным int и 8-битным char.

person chqrlie    schedule 22.05.2020
comment
Я готов возразить, что ошибка при передаче unsigned char в printf не соответствует спецификации. - person Joshua; 23.05.2020
comment
Вы говорите, что unsigned char может быть больше, чем int в системах с sizeof(int)==1. По определению, в этом случае они будут иметь одинаковый sizeof(), поэтому было бы неправильно говорить больше. Возможно, что unsigned char может иметь больше битов значений, чем int (int может иметь заполнение; unsigned char не разрешено). Но даже без всего этого верхний предел диапазона значений unsigned char может быть больше, чем для int, для того же количества битов значения просто потому, что он беззнаковый. - person Peter Cordes; 23.05.2020
comment
Мне также кажется странным говорить, что они равны, если верхние пределы диапазона значений совпадают между unsigned char и signed int (что позволяет unsigned char перейти в int). Они не могут быть одного типа (они должны отличаться подписью), и наличие одного и того же верхнего предела диапазона значений (положительный конец) будет означать, что int имеет еще 1 бит значения. - person Peter Cordes; 23.05.2020
comment
@PeterCordes: да, действительно, я колебался по поводу термина равно ... Конечно, равны только числа битов значений, и int потребуется как минимум один дополнительный бит для знака и как минимум 15 битов заполнения на эту сверхъестественную архитектуру. Интересно, может ли что-нибудь еще в Стандарте препятствовать соответствию в этом случае. Мой компилятор DS9K еще не созрел, чтобы это проверить :( - person chqrlie; 23.05.2020
comment
@chqrlie: бит знака является частью значения в представлении объекта, а не отступом. Я пытался использовать терминологию стандарта ISO C. - person Peter Cordes; 23.05.2020
comment
@PeterCordes: бит знака не является частью битов значения, как используется в C17 6.2.6.2: [...] Для целочисленных типов со знаком биты представления объекта должны быть разделены на три группы: биты значения, биты заполнения и бит знака. [...]. Так что технически int и unsigned char могут иметь одинаковое количество битов значения, но тогда у них должен быть отдельный бит знака и, следовательно, по крайней мере CHAR_BIT-1 битов заполнения в такой странной архитектуре. - person chqrlie; 23.05.2020
comment
Ах, моя ошибка, спасибо, что поправили меня относительно того, как C использует термин «биты значения». Приведение примера 8 против 31 очень полезно, чтобы прояснить, что он не включает знаковый бит на случай, если кто-то забыл. Хорошая редакция. - person Peter Cordes; 24.05.2020

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

Из этого проекта стандарта C11:

6.5.7 Операторы побитового сдвига
...
3 Целочисленные повышения выполняются для каждого из операндов. Тип результата - это продвинутый левый операнд ...

Однако в вашем первом случае, unsigned char tmp = x << 7;, tmp теряет шесть «старших» битов, когда результирующее «полное» целое число преобразуется (т. Е. усечено) обратно в один байт, давая значение 0x80; когда это затем сдвигается вправо в unsigned char y = tmp >> 7;, результат (как и ожидалось) 0x01.

person Adrian Mole    schedule 22.05.2020
comment
Превосходно! Теперь, будет ли целочисленное повышение до unsigned int, поскольку исходным типом является unsigned char? В противном случае я мог бы ожидать увидеть расширение знака в правой смене. - person Fred Larson; 22.05.2020
comment
@FredLarson Не имеет значения, подписан ли продвигаемый тип или нет! Поскольку значение 255 может быть правильно представлено любым из них, расширение знака не происходит. То есть, даже если вы явно приведете unsigned char значение 255 к подписанному 32-битному int, его значение будет 255 (не INT_MIN). - person Adrian Mole; 22.05.2020
comment
@FredLarson Вы точно не увидите знакового расширения с беззнаковым типом. Что касается того, что он продвигает, он продвигается до int (при условии, что int больше, чем char в указанной системе) в соответствии с разделом 6.3.1.1 проекта стандарта C11: Если int может представлять все значения исходного типа (как ограничено шириной, для битового поля), значение преобразуется в int; в противном случае он преобразуется в unsigned int. - person Christian Gibbons; 22.05.2020

Оператор сдвига не определен для типов char. Значение любого char операнда преобразуется в int, а результат выражения преобразуется в тип char. Таким образом, если вы поместите операторы сдвига влево и вправо в одно и то же выражение, расчет будет выполняться как тип int (без потери бита), а результат будет преобразован в char.

person ivangreek    schedule 22.05.2020