Операторы сдвига (<<, >>) арифметические или логические в C?

В C операторы сдвига ( <<, >> ) являются арифметическими или логическими?

Ответов (11)

Решение

Согласно K&R 2nd edition результаты зависят от реализации для сдвига вправо знаковых значений.

Википедия говорит, что C/C++ «обычно» реализует арифметический сдвиг значений со знаком.

По сути, вам нужно либо протестировать свой компилятор, либо не полагаться на него. Моя справка VS2008 для текущего компилятора MS C++ говорит, что их компилятор выполняет арифметический сдвиг.

Согласно многим компиляторам :

  1. << это арифметический сдвиг влево или побитовый сдвиг влево.
  2. >> - арифметический сдвиг вправо или побитовый сдвиг вправо.

Левый "шифт <<

Это как-то просто, и всякий раз, когда вы используете оператор сдвига, это всегда побитовая операция, поэтому мы не можем использовать ее с операциями double и float. Всякий раз, когда мы оставляем сдвиг на один ноль, он всегда добавляется к младшему значащему биту ( LSB ).

Но при сдвиге вправо >> мы должны следовать еще одному правилу, и это правило называется «копирование знакового бита». Значение «копирование знакового бита» состоит в том, что если установлен старший значащий бит ( MSB ), то после повторного сдвига вправо MSB он будет установлен, если он был сброшен, то он снова сбрасывается, это означает, что если предыдущее значение было нулевым, то после повторного сдвига бит равен нулю, если предыдущий бит был единицей, то после сдвига он снова равен единице. Это правило не действует для сдвига влево.

Самый важный пример со сдвигом вправо, если вы сдвинете любое отрицательное число на сдвиг вправо, затем после некоторого сдвига значение, наконец, достигнет нуля, а затем после этого, если сдвиньте это -1 любое количество раз, значение останется прежним. Пожалуйста, проверьте.

Когда вы это делаете - сдвиг влево на 1 вы умножаете на 2 - сдвиг вправо на 1 вы делите на 2

 x = 5
 x >> 1
 x = 2 ( x=5/2)

 x = 5
 x << 1
 x = 10 (x=5*2)

TL; DR

Считайте i и n левым и правым операндами соответственно оператора сдвига; тип i после целочисленного продвижения быть T . Предполагая, n что находится в [0, sizeof(i) * CHAR_BIT) - в противном случае не определено - у нас есть следующие случаи:

| Direction  |   Type   | Value (i) | Result                   |
| ---------- | -------- | --------- | ------------------------ |
| Right (>>) | unsigned |    ≥ 0    | −∞ ← (i ÷ 2ⁿ)            |
| Right      | signed   |    ≥ 0    | −∞ ← (i ÷ 2ⁿ)            |
| Right      | signed   |    < 0    | Implementation-defined†  |
| Left  (<<) | unsigned |    ≥ 0    | (i * 2ⁿ) % (T_MAX + 1)   |
| Left       | signed   |    ≥ 0    | (i * 2ⁿ) ‡               |
| Left       | signed   |    < 0    | Undefined                |

† большинство компиляторов реализуют это как арифметический сдвиг
‡ undefined, если значение превышает тип результата T; продвинутый тип i


Смещение

Во-первых, разница между логическим и арифметическим сдвигами с математической точки зрения, не беспокоясь о размере типа данных. Логический сдвиг всегда заполняет отброшенные биты нулями, в то время как арифметический сдвиг заполняет его нулями только для сдвига влево, но для сдвига вправо он копирует MSB, тем самым сохраняя знак операнда (предполагая кодирование с дополнительным двоичным кодом для отрицательных значений).

Другими словами, логический сдвиг рассматривает сдвинутый операнд как просто поток битов и перемещает их, не беспокоясь о знаке результирующего значения. Арифметический сдвиг рассматривает его как (подписанное) число и сохраняет знак при выполнении сдвигов.

Левый арифметический сдвиг числа X на n эквивалентен умножению X на 2 n и, таким образом, эквивалентен логическому сдвигу влево; логический сдвиг также даст тот же результат, поскольку MSB все равно отваливается от конца и нечего сохранять.

Арифметический сдвиг вправо числа X на n эквивалентен целочисленному делению X на 2 n, ТОЛЬКО если X неотрицательно! Целочисленное деление - это не что иное, как математическое деление и округление до 0 ( усечение ).

Для отрицательных чисел, представленных дополнительным кодированием до двух, сдвиг вправо на n битов приводит к математическому делению на 2 n и округлению в сторону -∞ ( пол ); таким образом, сдвиг вправо отличается для неотрицательных и отрицательных значений.

для X ≥ 0, X >> n = X / 2 n = trunc (X ÷ 2 n )

для X <0, X >> n = этаж (X ÷ 2 n )

где ÷ - математическое деление, / - целочисленное деление. Давайте посмотрим на пример:

37) 10 = 100 · 10 1) 2

37 ÷ 2 = 18,5

37/2 = 18 (округление 18,5 до 0) = 100 · 10) 2 [результат арифметического сдвига вправо]

-37) 10 = 11011011) 2 (с учетом 8-битного представления с дополнением до двух)

-37 ÷ 2 = -18,5

-37 / 2 = -18 (округление 18,5 до 0) = 11101110) 2 [НЕ результат арифметического сдвига вправо]

-37 >> 1 = -19 (округление 18,5 в сторону −∞) = 11101101) 2 [результат арифметического сдвига вправо]

Как заметил Гай Стил , это несоответствие привело к ошибкам более чем в одном компиляторе . Здесь неотрицательные (математические) могут быть сопоставлены с неотрицательными значениями без знака и со знаком (C); оба обрабатываются одинаково, и их сдвиг вправо выполняется целочисленным делением.

Таким образом, логика и арифметика эквивалентны при сдвиге влево и для неотрицательных значений при сдвиге вправо; они отличаются смещением отрицательных значений вправо.

Типы операндов и результатов

Стандарт C99 §6.5.7 :

Каждый из операндов должен иметь целочисленный тип.

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

short E1 = 1, E2 = 3;
int R = E1 << E2;

В приведенном выше фрагменте оба операнда становятся int (из-за целочисленного продвижения); если E2 было отрицательным, или E2 ≥ sizeof(int) * CHAR_BIT тогда операция не определена. Это потому, что сдвиг большего количества бит, чем доступно, наверняка приведет к переполнению. Если бы он R был объявлен как short, int результат операции сдвига неявно преобразовывался бы в short ; сужающее преобразование, которое может привести к поведению, определяемому реализацией, если значение не может быть представлено в целевом типе.

Левый "шифт

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

Поскольку сдвиги влево одинаковы для обоих, освободившиеся биты просто заполняются нулями. Затем он заявляет, что как для беззнаковых, так и для подписанных типов это арифметический сдвиг. Я интерпретирую это как арифметический сдвиг, поскольку логические сдвиги не заботятся о значении, представленном битами, он просто смотрит на него как на поток битов; но стандарт говорит не в терминах битов, а в терминах значения, полученного произведением E1 на 2 E2 .

Предостережение здесь в том, что для подписанных типов значение должно быть неотрицательным, а результирующее значение должно быть представлено в типе результата. В противном случае операция не определена. Тип результата будет типом E1 после применения интегрального продвижения, а не типом назначения (переменная, которая будет содержать результат). Результирующее значение неявно преобразуется в целевой тип; если он не может быть представлен в этом типе, то преобразование определяется реализацией (C99 §6.3.1.3 / 3).

Если E1 - это знаковый тип с отрицательным значением, то поведение сдвига влево не определено. Это простой путь к неопределенному поведению, которое можно легко упустить из виду.

Правый Shift

Результатом E1 >> E2 являются битовые позиции E2, сдвинутые вправо. Если E1 имеет беззнаковый тип или если E1 имеет знаковый тип и неотрицательное значение, значение результата является неотъемлемой частью частного E1 / 2 E2 . Если E1 имеет тип со знаком и отрицательное значение, результирующее значение определяется реализацией.

Сдвиг вправо для неотрицательных значений без знака и знака довольно прост; пустые биты заполняются нулями. Для отрицательных значений со знаком результат сдвига вправо определяется реализацией. Тем не менее, большинство реализаций, таких как GCC и Visual C++, реализуют сдвиг вправо как арифметический сдвиг, сохраняя бит знака.

Заключение

В отличие от Java, в которой есть специальный оператор >>> для логического сдвига, кроме обычного >> и <<, в C и C++ есть только арифметический сдвиг, при этом некоторые области остаются неопределенными и определяются реализацией. Причина, по которой я считаю их арифметическими, связана со стандартной математической формулировкой операции, а не с обработкой смещенного операнда как потока битов; возможно, это причина, по которой он оставляет эти области не определенными / реализацией вместо того, чтобы просто определять все случаи как логические сдвиги.

GCC делает

  1. для -ve -> Арифметический сдвиг

  2. For + ve -> Логический сдвиг

Я поискал это в Википедии , и они сказали следующее:

C, однако, имеет только один оператор сдвига вправо, >>. Многие компиляторы C выбирают, какой сдвиг вправо выполнять в зависимости от того, какое целое число сдвигается; часто целые числа со знаком сдвигаются с использованием арифметического сдвига, а целые числа без знака сдвигаются с использованием логического сдвига.

Похоже, это зависит от вашего компилятора. Также в этой статье обратите внимание, что сдвиг влево одинаков для арифметических и логических операций. Я бы порекомендовал провести простой тест с некоторыми знаковыми и беззнаковыми числами в граничном регистре (конечно, с высоким битом) и посмотреть, какой результат будет у вашего компилятора. Я также рекомендовал бы избегать зависимости от того, является ли это одним или другим, поскольку кажется, что C не имеет стандарта, по крайней мере, если это разумно и возможно избежать такой зависимости.

При сдвиге влево нет разницы между арифметическим и логическим сдвигом. При сдвиге вправо тип сдвига зависит от типа сдвигаемого значения.

(Для читателей, незнакомых с разницей, «логический» сдвиг вправо на 1 бит сдвигает все биты вправо и заполняет крайний левый бит нулевым значением. «Арифметический» сдвиг оставляет исходное значение в крайнем левом бите. . Разница становится важной при работе с отрицательными числами.)

При сдвиге беззнакового значения оператор >> в C является логическим сдвигом. При сдвиге значения со знаком оператор >> выполняет арифметический сдвиг.

Например, для 32-битной машины:

signed int x1 = 5;
assert((x1 >> 1) == 2);
signed int x2 = -5;
assert((x2 >> 1) == -3);
unsigned int x3 = (unsigned int)-5;
assert((x3 >> 1) == 0x7FFFFFFD);

Что касается типа сдвига, который вы получаете, важен тип сдвигаемого значения. Классический источник ошибок - это когда вы сдвигаете буквальное значение, например, чтобы замаскировать биты. Например, если вы хотите отбросить крайний левый бит целого числа без знака, вы можете попробовать это в качестве своей маски:

~0 >> 1

К сожалению, это доставит вам неприятности, потому что в маске будут установлены все биты, потому что сдвигаемое значение (~ 0) подписано, поэтому выполняется арифметический сдвиг. Вместо этого вы захотите принудительно выполнить логический сдвиг, явно объявив значение как беззнаковое, то есть сделав что-то вроде этого:

~0U >> 1;

обычно использует логические сдвиги для переменных без знака и сдвиги влево для переменных со знаком. Арифметический сдвиг вправо действительно важен, потому что он расширяет переменную.

будет использовать это, когда это применимо, как, вероятно, будут делать другие компиляторы.

Вот функции, гарантирующие логический сдвиг вправо и арифметический сдвиг вправо для int в C:

int logicalRightShift(int x, int n) {
    return (unsigned)x >> n;
}
int arithmeticRightShift(int x, int n) {
    if (x < 0 && n > 0)
        return x >> n | ~(~0U >> n);
    else
        return x >> n;
}