Операторы сдвига (

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

11 ответов


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

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

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


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

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

при сдвиге значения без знака оператор >> в 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);

TL; DR

считают i и n быть левым и правым операндами соответственно оператора сдвига; тип i, после целочисленного продвижения, be 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                |

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


сдвиг

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

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

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

правый арифметический сдвиг числа X на n эквивалентен целочисленному делению X на 2n только если X неотрицателен! Деление целых чисел является математической отдела и круглые к 0 (trunc).

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

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

для X > n = пол(X ÷ 2n)

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

37)10 = 100101)2

37 ÷ 2 = 18.5

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

-37)10 = 11011011)2 (учитывая дополнение двойки, 8-битное представление)

-37 ÷ 2 = -18.5

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

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

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

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

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

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

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

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

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

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

Сдвиг Влево

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

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

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

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

Сдвиг Вправо

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

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

вывод

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


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

~0 >> 1

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

~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 выбирают какой правый сдвиг выполнить в зависимости о том, какой тип integer является сдвинуты; часто целые числа со знаком shifted с использованием арифметического сдвига, и целые числа без знака смещаются используя логический сдвиг.

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


сдвиг влево <<

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

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

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


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

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


GCC выполняет

  1. for-ve - > арифметический сдвиг

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


по мнению многих c составители:

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