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

Как оценивается оператор сдвига в C?

Недавно я заметил (странное) поведение, когда я проводил операции с использованием shift >> <<!

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

#include <stdio.h>

int main(void) {
    unsigned char a=0x05, b=0x05;

    // first operation
    a = ((a<<7)>>7);

    // second operation
    b <<= 7;
    b >>= 7;

    printf("a=%X b=%X\n", a, b);
    return 0;
} 

При запуске a = 5 и b = 1. Я ожидаю, что они оба будут равны 1! Может кто-нибудь объяснить, почему я получил такой результат?

P.S: В моей среде размер unsigned char равен 1 байт

4b9b3361

Ответ 1

Операции сдвига будут делать целые продвижения по своим операндам, а в вашем коде полученный int преобразуется обратно в char следующим образом:

// first operation
a = ((a<<7)>>7); // a = (char)((a<<7)>>7);

// second operation
b <<= 7; // b = (char) (b << 7);
b >>= 7; // b = (char) (b >> 7);

Цитата из черновика N1570 (которая стала стандартом C11 позже):

6.5.7 Операторы побитового сдвига:

  1. Каждый из операндов должен иметь целочисленный тип.
  2. Целочисленные рекламные акции выполняются для каждого из операндов. Тип результата - это продвинутый левый операнд. Если значение правого операнда отрицательное или больше или равно ширине продвинутого левого операнда, поведение undefined.

И он предположил, что в C99 и C90 есть похожие утверждения.

Ответ 2

В первом примере:

  • a преобразуется в int, сдвигается влево, затем направо и затем преобразуется обратно в usigned char.

Это приведет к a=5, очевидно.

Во втором примере:

  • b преобразуется в int, сдвигается влево, а затем преобразуется обратно в unsigned char.
  • b преобразуется в int, сдвигается вправо, а затем преобразуется обратно в unsigned char.

Разница в том, что во втором примере вы теряете информацию во время преобразования в unsigned char

Ответ 3

Подробное объяснение вещей, происходящих между строками:

Случай a:

  • В выражении a = ((a<<7)>>7); сначала оценивается a<<7.
  • В стандарте C указано, что каждый операнд операторов сдвига неявно целенаправленно продвигается, а это означает, что если они имеют типы bool, char, short и т.д. (в совокупности это "малые целые типы" ), они получают повышение до int.
  • Это стандартная практика для почти каждого оператора в C. Что делает операторы сдвига отличными от других операторов, так это то, что они не используют другую общую неявную рекламу под названием "балансировка". Вместо этого результат сдвига всегда имеет тип продвинутого левого операнда. В этом случае int.
  • Итак, a получает повышение типа int, все еще содержащее значение 0x05. Литерал 7 уже имел тип int, поэтому он не получил повышение.
  • Когда вы переместили сдвиг на int на 7, вы получите 0x0280. Результатом операции является тип int.
  • Обратите внимание, что int является подписанным типом, поэтому, если бы вы продолжали переносить данные дальше, в знаковые биты вы бы вызвали поведение undefined. Аналогично, если либо левый, либо правый операнд был отрицательным значением, вы также вызывали бы поведение undefined.
  • Теперь у вас есть выражение a = 0x280 >> 7;. Никаких рекламных акций не происходит для следующей операции смены, так как оба операнда уже являются int.
  • Результат равен 5 и типа int. Затем вы преобразовываете этот int в unsigned char, что прекрасно, так как результат достаточно мал, чтобы соответствовать.

Случай b:

  • b <<= 7; эквивалентен b = b << 7;.
  • Как и раньше, b получает статус int. Результат снова будет 0x0280.
  • Затем вы пытаетесь сохранить этот результат в unsigned char. Он не подходит, поэтому он будет усечен, чтобы содержать только младший байт 0x80.
  • В следующей строке b снова получает статус int, содержащий 0x80.
  • И затем вы меняете 0x80 на 7, получая результат 1. Это тип int, но может вписываться в unsigned char, поэтому он будет вписываться в b.

Хороший совет:

  • Никогда не используйте битовые операторы со знаками целочисленных типов. Это не имеет никакого смысла в 99% случаев, но может привести к различным ошибкам и плохо определенному поведению.
  • При использовании битовых операторов используйте типы stdint.h, а не примитивные типы по умолчанию в C.
  • При использовании битовых операторов используйте явные приведения к предполагаемому типу, чтобы предотвратить ошибки и непреднамеренные изменения типа, но также чтобы было ясно, что вы действительно понимаете, как работают неявные промо-акции, и что вы не просто получили код работает случайно.

Лучше, более безопасный способ написать вашу программу:

#include <stdio.h>
#include <stdint.h>    

int main(void) {
    uint8_t a=0x05;
    uint8_t b=0x05;
    uint32_t tmp;

    // first operation
    tmp = (uint32_t)a << 7;
    tmp = tmp >> 7;
    a = (uint8_t)tmp;

    // second operation
    tmp = (uint32_t)b << 7;
    tmp = tmp >> 7;
    b = (uint8_t)tmp;

    printf("a=%X b=%X\n", a, b);
    return 0;
}