Чи є арифметичними чи логічними в C оператори зсуву (<<, >>)?


136

В C чи є оператори зсуву ( <<, >>) арифметичними чи логічними?


1
Яке значення арифметичного та логічного? Пов'язаний питання для знакозмінних інтсія: stackoverflow.com/questions/4009885 / ...
Чіро Сантіллі郝海东冠状病六四事件法轮功

Відповіді:


97

За версією другого видання K&R, результати залежать від реалізації для правильних зрушень підписаних значень.

У Вікіпедії сказано, що C / C ++ 'зазвичай' здійснює арифметичний зсув на підписані значення.

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


141

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

(Як тло для тих читачів, незнайомих з різницею, "логічний" правий зсув на 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);

57
Так близько, Грег. Ваше пояснення майже досконале, але зміщення виразу підписаного типу та негативного значення визначається реалізацією. Див. Розділ 6.5.7 ISO / IEC 9899: 1999.
Robᵩ

12
@Rob: Насправді, для зміни ліворуч та підписаного від’ємного числа поведінка не визначена.
JeremyP

5
Насправді, зсув ліворуч також призводить до невизначеного поведінки для позитивних підписаних значень, якщо отримане математичне значення (яке не обмежене розміром біт) не може бути представлене як позитивне значення у цьому підписаному типі. Суть полягає в тому, що вам потрібно обережно ступати під час зміщення підписаного значення праворуч.
Майкл Берр

3
@supercat: Я справді не знаю. Однак я знаю, що є задокументовані випадки, коли код із невизначеною поведінкою змушує компілятор робити дуже неінтуїтивні речі (як правило, завдяки агресивній оптимізації - наприклад, побачити стару помилку покажчика нульового вказівника Linux TUN / TAP: lwn.net / Статті / 342330 ). Якщо мені не потрібна заповнення знаків при правильному зрушенні (що я розумію, це поведінка, визначена реалізацією), я зазвичай намагаюся виконувати свої зсуви бітів, використовуючи непідписані значення, навіть якщо це означає, щоб використовувати туди касти.
Майкл Берр

2
@MichaelBurr: Я знаю, що гіпермодерністські компілятори використовують той факт, що поведінка, яка не була визначена стандартом C (навіть якщо це було визначено в 99% реалізацій ), є виправданням для перетворення програм, поведінка яких було б повністю визначено для всіх платформи, де можна було б очікувати їх запуску, в нікчемні пучки машинних інструкцій без корисної поведінки. Я визнаю, хоча (сарказм увімкнено) мене дивує, чому автори компілятора пропустили наймасовішу можливість оптимізації: опустіть будь-яку частину програми, яка, якщо вона буде досягнута, призведе до вкладення функцій ...
supercat

51

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                |

† більшість компіляторів реалізують це як арифметичний зсув
‡ невизначений, якщо значення переповнює тип результату T; рекламний тип i


Зсув

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

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

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

Правий арифметичний зсув числа X на n рівносильний цілому поділу X на 2 n ТІЛЬКИ, якщо X невід'ємний! Ціле ділення - це не що інше, як математичний поділ і округлення до 0 ( trunc ).

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

для X ≥ 0, X >> n = X / 2 n = магістраль (X ÷ 2 n )

для X <0, X >> n = підлога (X ÷ 2 n )

де ÷математичне ділення, /це ціле ділення. Розглянемо приклад:

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 [НЕ результат арифметичного правого зсуву]

-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 є репрезентативним у результаті результату, то це отримане значення; в іншому випадку поведінка не визначена.

Оскільки ліві зрушення однакові для обох, звільнені біти просто заповнюються нулями. Потім йдеться про те, що як для підписаних, так і для підписаних типів це арифметичний зсув. Я трактую це як арифметичний зсув, оскільки логічні зрушення не турбуються про значення, представлене бітами, він просто розглядає його як потік бітів; але стандарт розмовляє не з точки зору бітів, а шляхом визначення його за значенням, отриманим у добутку Е1 з 2 Е2 .

Тут заперечення полягає в тому, що для підписаних типів значення має бути невід’ємним, а отримане значення має бути представленим у типі результату. Інакше операція не визначена. Тип результату буде типом E1 після застосування інтегральної промоції, а не призначення (змінна, яка буде містити результат). Отримане значення неявно перетворюється на тип призначення; якщо воно не є репрезентативним у цьому типі, то перетворення визначено реалізацією (C99 §6.3.1.3 / 3).

Якщо E1 - це підписаний тип з негативним значенням, то поведінка зсуву ліворуч не визначена. Це простий шлях до невизначеної поведінки, який може легко не помітити.

Права зсув

Результат E1 >> E2 - це положення E1, зрушене праворуч E2. Якщо E1 має непідписаний тип або якщо E1 має підписаний тип і негативне значення, значення результату є невід'ємною частиною коефіцієнта E1 / 2 E2 . Якщо E1 має підписаний тип і негативне значення, отримане значення визначається реалізацією.

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

Висновок

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


1
Гарна відповідь. Що стосується округлення (у розділі під назвою Зсув ) - правильний зсув обертається в бік -Infяк для негативних, так і для позитивних чисел. Округлення до 0 додатного числа є приватним випадком округлення до -Inf. Під час обрізки ви завжди скидаєте позитивно зважені значення, отже, ви віднімаєте інакше точний результат.
ysap

1
@ysap Так, добре спостереження. В основному, округлення до 0 для додатних чисел є особливим випадком більш загального раунду до ∞; це можна побачити в таблиці, де і позитивні, і негативні числа я зазначив, що вони круглі до-.
legends2k

17

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

~0 >> 1

На жаль, це призведе до виникнення проблем, оскільки у маски будуть встановлені всі її біти, оскільки значення, що зміщується (~ 0), підписується, таким чином виконується арифметичний зсув. Натомість, ви хочете зробити логічний зсув, чітко оголосивши значення таким, що не підписане, тобто зробивши щось подібне:

~0U >> 1;

16

Ось функції, які гарантують логічний правий зсув та арифметичний правий зсув 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;
}

7

Коли ви робите - зсув ліворуч на 1, ви помножите на 2 - правий зсув на 1 ділите на 2

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

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

У x >> a і x << a, якщо умовою є a> 0, то відповідь x = x / 2 ^ a, x = x * 2 ^ a відповідно, Яка була б відповідь, якщо a <0?
JAVA

@sunny: a має бути не менше 0. Це невизначена поведінка у C.
Jeremy

4

Ну, я подивився це на wikipedia , і вони мають таке сказати:

C, однак, має лише один правий оператор зсуву >>. Багато компіляторів C вибирають, який правильний зсув виконувати, залежно від того, на який тип переміщується ціле число; часто підписані цілі числа зміщуються за допомогою арифметичного зсуву, а непідписані цілі числа зміщуються за допомогою логічного зсуву.

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


Хоча більшість компіляторів С використовували арифметичний зсув ліворуч для підписаних значень, схоже, така корисна поведінка застаріла. Нинішня філософія компілятора, здається, передбачає, що ефективність зміни лівої частини змінної дає змогу компілятору припустити, що змінна повинна бути негативною, і, таким чином, опустити будь-який інший код, який був би необхідний для правильної поведінки, якщо змінна була негативною .
supercat

0

Зміна вліво <<

Це якось просто і щоразу, коли ви використовуєте оператор зсуву, це завжди трохи розумна операція, тому ми не можемо використовувати його при операції подвійної та плаваючої операції. Щоразу, коли ми залишаємо зсув на один нуль, він завжди додається до найменш значущого біта (LSB ).

Але в правильному зрушенні >>ми повинні дотримуватися одного додаткового правила, і це правило називається "копія біт підпису". Значення "копії бітових знаків" означає, якщо встановлено найзначніший біт ( MSB), то після правого зсуву зновуMSB буде встановлено, якщо він був скинутий, то він знову скидається, тобто якщо попереднє значення було нульовим, то після повторного зміщення, біт дорівнює нулю, якщо попередній біт був один, то після зсуву він знову один. Це правило не застосовується для лівої зміни.

Найважливіший приклад правильного зсуву, якщо ви змістите будь-яке від’ємне число на правий зсув, то після деякого зміщення значення, нарешті, досягне нуля, а потім після цього, якщо змістити це -1 будь-яке число разів, значення буде залишатися однаковим. Будь ласка, перевірте.


0

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

буде використовувати це, коли це можливо, як це можливо, роблять інші компілятори.



-7

На думку багатьох компілятори:

  1. << - це арифметичний зсув ліворуч або розряд ліворуч.
  2. >> - це арифметичний правий зсув, побітовий правий зсув.

3
"Арифметичний зсув правого" та "бітовий правий зсув" різні. У цьому і полягає питання. Питання задається: " >>Арифметична чи побітна (логічна)?" Ви відповіли " >>арифметично чи порозрядно". Це не відповідає на запитання.
wchargin

Ні, <<і >>оператори логічні, а не арифметичні
shjeff
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.