В C операторы сдвига ( <<
, >>
) являются арифметическими или логическими?
В C операторы сдвига ( <<
, >>
) являются арифметическими или логическими?
Ответы:
Согласно K&R 2nd edition результаты зависят от реализации для сдвига вправо знаковых значений.
Википедия говорит, что C / C ++ «обычно» реализует арифметический сдвиг в знаковых значениях.
В основном вам нужно либо протестировать свой компилятор, либо не полагаться на него. Моя справка VS2008 для текущего компилятора MS 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);
Считайте i
и n
левым и правым операндами соответственно оператора сдвига; тип i
после целочисленного продвижения быть T
. Предполагая, n
что находится в [0, sizeof(i) * CHAR_BIT)
- в противном случае undefined - мы имеем следующие случаи:
| 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 - знаковый тип с отрицательным значением, то поведение сдвига влево не определено. Это простой путь к неопределенному поведению, которое можно легко упустить из виду.
Результатом E1 >> E2 являются битовые позиции E2, сдвинутые вправо. Если E1 имеет тип без знака или E1 имеет тип со знаком и неотрицательное значение, значение результата является неотъемлемой частью частного E1 / 2 E2 . Если E1 имеет тип со знаком и отрицательное значение, результирующее значение определяется реализацией.
Сдвиг вправо для неотрицательных значений без знака и со знаком довольно прост; пустые биты заполняются нулями. Для отрицательных значений со знаком результат сдвига вправо определяется реализацией. Тем не менее, большинство реализаций, таких как GCC и Visual C ++, реализуют сдвиг вправо как арифметический сдвиг, сохраняя бит знака.
В отличие от Java, в которой есть специальный оператор >>>
для логического сдвига, кроме обычного >>
и <<
, в C и C ++ есть только арифметический сдвиг, при этом некоторые области остаются неопределенными и определяются реализацией. Причина, по которой я считаю их арифметическими, связана со стандартной формулировкой операции математически, а не с обработкой смещенного операнда как потока битов; это, возможно, причина, по которой он оставляет эти области не определенными для реализации, а не просто определяет все случаи как логические сдвиги.
-Inf
как отрицательных, так и положительных чисел. Округление положительного числа в сторону 0 - это частный случай округления в сторону -Inf
. При усечении вы всегда отбрасываете положительно взвешенные значения, поэтому вы вычитаете из иначе точного результата.
Что касается типа сдвига, который вы получаете, важен тип сдвигаемого значения. Классический источник ошибок - это когда вы сдвигаете буквальное значение, например, чтобы скрыть биты. Например, если вы хотите удалить самый левый бит целого числа без знака, то вы можете попробовать это в качестве маски:
~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;
}
Когда вы это делаете - сдвиг влево на 1 умножается на 2 - сдвиг вправо на 1 вы делите на 2
x = 5
x >> 1
x = 2 ( x=5/2)
x = 5
x << 1
x = 10 (x=5*2)
Я поискал это в Википедии , и они сказали следующее:
C, однако, имеет только один оператор сдвига вправо, >>. Многие компиляторы C выбирают, какой сдвиг вправо выполнить в зависимости от того, какое целое число сдвигается; часто целые числа со знаком сдвигаются с использованием арифметического сдвига, а целые числа без знака сдвигаются с использованием логического сдвига.
Похоже, это зависит от вашего компилятора. Также в этой статье обратите внимание, что сдвиг влево одинаков для арифметики и логики. Я бы порекомендовал сделать простой тест с несколькими знаковыми и беззнаковыми числами на границе (конечно, с набором старших битов) и посмотреть, каков результат на вашем компиляторе. Я также рекомендовал бы избегать зависимости от того, является ли он одним или другим, поскольку кажется, что у С нет стандарта, по крайней мере, если это разумно и возможно избежать такой зависимости.
Левый "шифт <<
Это как-то легко, и всякий раз, когда вы используете оператор сдвига, это всегда побитовая операция, поэтому мы не можем использовать ее с операциями double и float. Каждый раз, когда мы оставляем сдвиг на один ноль, он всегда добавляется к младшему значащему биту ( LSB
).
Но при сдвиге вправо >>
мы должны следовать еще одному правилу, и это правило называется «копирование знакового бита». Значение «знакового копирования битов» означает, что если старший значащий бит ( MSB
) установлен, то после правого сдвига снова MSB
будет установлен, если он был сброшен, то он снова сброшен, означает, что если предыдущее значение было нулевым, то после повторного сдвига, бит равен нулю, если предыдущий бит был единицей, то после сдвига он снова равен единице. Это правило не действует для сдвига влево.
Самый важный пример со сдвигом вправо, если вы сдвинете любое отрицательное число на сдвиг вправо, затем после некоторого сдвига значение, наконец, достигнет нуля, а затем после этого, если сдвиньте это -1 любое количество раз, значение останется прежним. Пожалуйста, проверьте.
GCC делает
для -ve -> Арифметический сдвиг
For + ve -> Logical Shift
По мнению многих с составители:
<<
это арифметический сдвиг влево или побитовый сдвиг влево.>>
- арифметический сдвиг вправо или побитовый сдвиг вправо.>>
Арифметический или побитовый (логический)?» Вы ответили " >>
арифметически или поразрядно" Это не отвечает на вопрос.
<<
и >>
операторы логические, а не арифметические