В C чи є оператори зсуву ( <<
, >>
) арифметичними чи логічними?
В C чи є оператори зсуву ( <<
, >>
) арифметичними чи логічними?
Відповіді:
За версією другого видання K&R, результати залежать від реалізації для правильних зрушень підписаних значень.
У Вікіпедії сказано, що 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);
Розглянемо 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 ++ мають лише арифметичне зсув з деякими областями, залишеними невизначеними та визначеними реалізацією. Причина, яку я вважаю їх арифметичною, пояснюється стандартним формулюванням операції математично, а не трактуванням зміщеного операнда як потоком бітів; це, мабуть, причина, чому він залишає ці області не визначеними / впровадженими, а не просто визначає всі випадки як логічні зрушення.
-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)
Ну, я подивився це на wikipedia , і вони мають таке сказати:
C, однак, має лише один правий оператор зсуву >>. Багато компіляторів C вибирають, який правильний зсув виконувати, залежно від того, на який тип переміщується ціле число; часто підписані цілі числа зміщуються за допомогою арифметичного зсуву, а непідписані цілі числа зміщуються за допомогою логічного зсуву.
Тож це звучить так, як це залежить від вашого компілятора. Також у цій статті зауважте, що зсув ліворуч є однаковим для арифметичних та логічних. Я рекомендую зробити простий тест з деякими підписаними та непідписаними номерами у випадку з кордоном (звичайно високий набір бітів) і подивитися, який результат має ваш компілятор. Я також рекомендую уникати залежно від того, чи є той чи інший, оскільки, здається, C не має стандарту, принаймні, якщо це розумно і можливо уникнути такої залежності.
Зміна вліво <<
Це якось просто і щоразу, коли ви використовуєте оператор зсуву, це завжди трохи розумна операція, тому ми не можемо використовувати його при операції подвійної та плаваючої операції. Щоразу, коли ми залишаємо зсув на один нуль, він завжди додається до найменш значущого біта (LSB
).
Але в правильному зрушенні >>
ми повинні дотримуватися одного додаткового правила, і це правило називається "копія біт підпису". Значення "копії бітових знаків" означає, якщо встановлено найзначніший біт ( MSB
), то після правого зсуву зновуMSB
буде встановлено, якщо він був скинутий, то він знову скидається, тобто якщо попереднє значення було нульовим, то після повторного зміщення, біт дорівнює нулю, якщо попередній біт був один, то після зсуву він знову один. Це правило не застосовується для лівої зміни.
Найважливіший приклад правильного зсуву, якщо ви змістите будь-яке від’ємне число на правий зсув, то після деякого зміщення значення, нарешті, досягне нуля, а потім після цього, якщо змістити це -1 будь-яке число разів, значення буде залишатися однаковим. Будь ласка, перевірте.
GCC робить
for -ve -> Арифметичний зсув
Для + ve -> логічний зсув
На думку багатьох c компілятори:
<<
- це арифметичний зсув ліворуч або розряд ліворуч.>>
- це арифметичний правий зсув, побітовий правий зсув.>>
Арифметична чи побітна (логічна)?" Ви відповіли " >>
арифметично чи порозрядно". Це не відповідає на запитання.
<<
і >>
оператори логічні, а не арифметичні