C에서 시프트 연산자 (<<, >>)가 산술적이거나 논리적입니까?


답변:


97

에 따르면 K & R 2 판 결과는 구현에 의존 서명 값 오른쪽 변화에 대한 있습니다.

Wikipedia에 따르면 C / C ++ '보통'은 부호있는 값에 대한 산술 이동을 구현한다고합니다.

기본적으로 컴파일러를 테스트하거나 의존하지 않아야합니다. 현재 MS C ++ 컴파일러에 대한 VS2008의 도움에 따르면 컴파일러는 산술 시프트를 수행한다고합니다.


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
너무 가까이, 그렉 설명은 거의 완벽하지만 부호있는 유형과 음수 값의 표현을 변경하는 것은 구현에 따라 다릅니다. ISO / IEC 9899 : 1999 섹션 6.5.7을 참조하십시오.
Robᵩ

12
@Rob : 실제로 왼쪽 시프트 및 부호있는 음수의 경우 동작이 정의되지 않습니다.
JeremyP

5
실제로, 결과 수학 값 (비트 크기에 제한되지 않음)을 해당 부호있는 유형의 양수 값으로 표시 할 수없는 경우 왼쪽 시프트는 양의 부호있는 값에 대해 정의되지 않은 동작을 초래합니다. 결론은 부호있는 값을 올바르게 이동할 때 조심스럽게 밟아야한다는 것입니다.
Michael Burr

3
@ supercat : 나는 정말로 모른다. 그러나 정의되지 않은 동작을 가진 코드로 인해 컴파일러가 매우 직관적이지 않은 작업을 수행하는 문서화 된 사례가 있음을 알고 있습니다 (일반적으로 공격적인 최적화로 인해 예를 들어 이전 Linux TUN / TAP 드라이버 null 포인터 버그 참조 : lwn.net / Articles / 342330 ). 올바른 시프트 (구현 정의 동작이라는 것을 깨달았습니다)에서 부호 채우기가 필요하지 않은 경우 일반적으로 캐스트를 사용하여 거기에 도달한다는 의미가 있더라도 부호없는 값을 사용하여 비트 시프트를 수행하려고합니다.
Michael Burr

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의 승격 된 유형


이동

첫 번째는 데이터 유형 크기에 대해 걱정하지 않고 수학 관점에서 논리 및 산술 시프트의 차이점입니다. 논리 시프트는 항상 폐기 된 비트를 0으로 채우고 산술 시프트는 왼쪽 시프트의 경우에만 0으로 채우지 만 오른쪽 시프트의 경우 MSB를 복사하여 피연산자의 부호를 유지합니다 ( 음수 값에 대한 2의 보수 인코딩 가정 ).

다시 말해, 논리 시프트는 시프트 된 피연산자를 비트 스트림으로 간주하여 결과 값의 부호를 신경 쓰지 않고 이동시킵니다. 산술 시프트는이를 부호있는 숫자로보고 시프트가 수행 될 때 부호를 유지합니다.

N에 의해 번호 X의 좌측 시프트 연산 (2)에 의해 X를 승산에 상당 N 논리 왼쪽 시프트에 따라서 동일하다; 어쨌든 MSB가 끝에서 떨어지고 보존 할 것이 없기 때문에 논리적 이동도 동일한 결과를 제공합니다.

N에 의해 번호 X의 우측 시프트 연산에 의해 X 2의 정수 나눗셈 동등 N ONLY X는 음이 아닌 경우! 정수 나누기는 수학 나누기 일 뿐이며 0 ( trunc )을 향해 반올림 됩니다 .

2의 보수 인코딩으로 표현 된 음수의 경우, n 비트만큼 오른쪽으로 이동하면 수학적으로 2 n으로 나누고 -∞ ( floor ) 쪽으로 반올림 하는 효과가 있습니다 . 따라서 오른쪽 음의 이동은 음이 아닌 값과 음의 값이 다릅니다.

X ≥ 0 인 경우 X >> n = X / 2 n = trunc (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 (2의 보수, 8 비트 표현 고려)

-37 ÷ 2 = -18.5

-37/2 = -18 (18.5를 0으로 반올림) = 11101110) 2 [산술 오른쪽 이동 결과 아님]

-37 >> 1 = -19 (-1∞를 -∞쪽으로 반올림) = 11101101) 2 [산술 오른쪽 이동 결과]

가이 스틸 지적 이 불일치하게되었다 이상의 컴파일러 버그 . 여기서 음이 아닌 값 (수학)은 부호가없고 부호가있는 음이 아닌 값 (C)에 매핑 될 수 있습니다. 둘 다 동일하게 취급되며 오른쪽 이동은 정수 나누기에 의해 수행됩니다.

따라서 논리 및 산술은 왼쪽 이동과 오른쪽 이동의 음이 아닌 값에 해당합니다. 음수 값이 다르면 올바르게 이동합니다.

피연산자와 결과 유형

표준 C99 §6.5.7 :

각 피연산자는 정수 유형을 가져야합니다.

정수 승격은 각 피연산자에서 수행됩니다. 결과의 유형은 승격 된 왼쪽 피연산자의 유형입니다. 오른쪽 피연산자의 값이 음수이거나 승격 된 왼쪽 피연산자의 너비보다 크거나 같으면 동작이 정의되지 않습니다.

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

위의 스 니펫에서 두 피연산자는 int(정수 승격으로 인해)됩니다. 경우는 E2제외 하였다 또는 E2 ≥ sizeof(int) * CHAR_BIT다음 동작이 정의되지 않는다. 사용 가능한 비트 수보다 많이 시프트하면 오버플로가 발생하기 때문입니다. 했다 R선언 된 short1, int시프트 연산의 결과가 암시 적으로 변환 될 것입니다 short; 대상 유형에서 값을 표현할 수없는 경우 구현이 정의 된 동작으로 이어질 수있는 축소 변환

왼쪽 시프트

E1 << E2의 결과는 E1 좌측-시프트 된 E2 비트 위치이고; 비워진 비트는 0으로 채워집니다. E1에 부호없는 유형이있는 경우 결과 값은 E1 × 2 E2 이며 결과 유형에 표시 할 수있는 최대 값보다 모듈로가 하나 더 줄어 듭니다. E1에 부호있는 유형과 음수가 아닌 값이 있고 E1 × 2 E2 가 결과 유형으로 표시 될 수 있으면 결과 값입니다. 그렇지 않으면 동작이 정의되지 않습니다.

왼쪽 시프트는 두 가지 모두 동일하므로 비어있는 비트는 단순히 0으로 채워집니다. 그런 다음 부호없는 유형과 부호있는 유형 모두 산술적 전환이라고 말합니다. 논리적 시프트는 비트가 나타내는 값을 신경 쓰지 않고 비트 스트림으로 간주하기 때문에 산술 시프트로 해석합니다. 그러나 표준은 비트 단위가 아니라 E1과 2 E2 의 곱으로 얻은 값으로 정의하여 말합니다 .

여기서주의 할 점은 부호있는 유형의 경우 값이 음수가 아니어야하며 결과 유형에서 결과 값을 표현할 수 있어야합니다. 그렇지 않으면 작업이 정의되지 않습니다. 결과 유형은 대상 (결과를 보유 할 변수) 유형이 아니라 통합 승격을 적용한 후 E1의 유형입니다. 결과 값은 암시 적으로 대상 유형으로 변환됩니다. 해당 유형으로 표현할 수없는 경우 변환이 구현 정의됩니다 (C99 §6.3.1.3 / 3).

E1이 음수 값을 갖는 부호있는 유형 인 경우 왼쪽 이동 동작은 정의되지 않습니다. 이것은 쉽게 간과 될 수있는 정의되지 않은 동작으로의 쉬운 경로입니다.

오른쪽 교대

E1 >> E2의 결과는 E1 오른쪽으로 이동 된 E2 비트 위치입니다. E1에 부호없는 유형이 있거나 E1에 부호있는 유형과 음수가 아닌 값이있는 경우 결과 값은 E1 / 2 E2 몫의 정수 부분입니다 . E1에 부호있는 유형과 음수 값이있는 경우 결과 값은 구현 정의됩니다.

부호가없고 부호가있는 음이 아닌 값에 대한 오른쪽 이동은 매우 간단합니다. 빈 비트는 0으로 채워집니다. 부호있는 음수 값의 경우 오른쪽 이동 결과는 구현 정의됩니다. 즉, GCC 및 Visual C ++ 와 같은 대부분의 구현 은 부호 비트를 보존하여 산술 이동으로 오른쪽 이동을 구현합니다.

결론

>>>평소 >>와 는 다른 논리 시프트를위한 특수 연산자가있는 Java와 달리 <<C 및 C ++에는 일부 영역이 정의되지 않고 구현 정의 된 상태로만 산술 시프트 만 있습니다. 내가 산술로 간주하는 이유는 시프트 된 피연산자를 비트 스트림으로 처리하지 않고 수학적으로 연산으로 표현하는 표준 때문입니다. 이것은 아마도 모든 경우를 논리적 이동으로 정의하는 대신 해당 영역을 구현하지 않고 정의되지 않은 이유 일 것입니다.


1
좋은 대답입니다. 반올림과 관련하여 ( Shifting 섹션에서 ) 오른쪽 시프트 -Inf는 음수와 양수 모두를 향해 반올림 합니다. 양수의 0을 향한 반올림은을 향한 반올림의 경우입니다 -Inf. 잘릴 때 항상 양의 가중치를 적용하므로 정확한 결과에서 빼십시오.
ysap

1
@ysap 그래, 좋은 관찰. 기본적으로 양수의 경우 0을 향한 반올림은 -∞를 향한보다 일반적인 반올림의 특수한 경우입니다. 이것은 표에서 볼 수 있습니다. 양수와 음수 모두 −∞쪽으로 반올림했습니다.
legends2k

17

당신이 얻는 변화의 유형과 관련하여, 중요한 것은 당신이 변화시키고있는 가치의 유형입니다. 버그의 고전적인 소스는 리터럴을 비트 마스크로 바꿀 때입니다. 예를 들어, 부호없는 정수의 가장 왼쪽 비트를 삭제하려면 마스크로 시도하십시오.

~0 >> 1

불행하게도, 이것은 시프트되는 값 (~ 0)이 서명되어 마스크가 모든 비트를 설정하므로 산술 시프트가 수행되기 때문에 문제가 발생할 수 있습니다. 대신, 값을 명시 적으로 부호없는 것으로 선언하여, 예를 들어 다음과 같은 방법으로 논리적 이동을 강제하고 싶을 것입니다.

~0U >> 1;

16

다음은 C에서 int의 논리적 오른쪽 시프트 및 산술 오른쪽 시프트를 보장하는 함수입니다.

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이면 응답은 무엇입니까?
자바

@sunny : a는 0보다 작아서는 안됩니다. C에서는 정의되지 않은 동작입니다.
Jeremy

4

글쎄, 나는 Wikipedia 에서 그것을 찾았다 .

그러나 C에는 오른쪽 시프트 연산자가 하나만 있습니다 >>. 많은 C 컴파일러는 어떤 유형의 정수가 시프트되는지에 따라 수행 할 오른쪽 시프트를 선택합니다. 종종 부호있는 정수는 산술 시프트를 사용하여 시프트되고 부호없는 정수는 논리 시프트를 사용하여 시프트됩니다.

따라서 컴파일러에 따라 달라집니다. 이 기사에서도 왼쪽 시프트는 산술 및 논리에 동일합니다. 테두리 케이스 (높은 비트 세트)에서 부호있는 숫자와 부호없는 숫자로 간단한 테스트를 수행하고 컴파일러에서 결과가 무엇인지 확인하는 것이 좋습니다. 적어도 C가 표준을 가지고 있지 않은 것처럼 보이기 때문에 적어도 의존성을 피하는 것이 합리적이고 가능하다면 피할 것을 권장합니다.


대부분의 C 컴파일러가 부호있는 값에 대해 산술 왼쪽 시프트를 사용했지만 이러한 유용한 동작은 더 이상 사용되지 않는 것 같습니다. 현재의 컴파일러 철학은 변수에 대한 왼쪽 시프트의 성능이 변수가 음이 아니어야한다고 가정하여 변수가 음수 인 경우 올바른 동작에 필요한 코드를 생략하도록 가정하는 것으로 보입니다. .
supercat

0

왼쪽 교대 <<

이것은 어쨌든 쉽고 시프트 연산자를 사용할 때마다 항상 비트 단위 연산이므로 double 및 float 연산과 함께 사용할 수 없습니다. 우리가 1을 0으로 옮길 때마다 항상 최하위 비트 ( LSB)에 추가됩니다 .

그러나 올바른 교대조 >>에서는 하나의 추가 규칙을 따라야하며이 규칙을 "사인 비트 복사"라고합니다. "부호 비트 복사"의 의미는 최상위 비트 ( MSB)가 설정되고 오른쪽 시프트 후 MSB다시 재설정되면 재설정되고 다시 재설정되면 이전 값이 0이면 다시 시프트 된 후 이전 비트가 1이면 시프트 후 비트는 다시 1입니다. 이 규칙은 왼쪽 이동에는 적용되지 않습니다.

음수를 오른쪽 시프트로 이동하면 오른쪽 시프트에 대한 가장 중요한 예는 값을 일부 시프트 한 후 마지막으로 0에 도달 한 다음이 값을 -1로 여러 번 시프트하면 값이 동일하게 유지됩니다. 확인해주십시오.


0

일반적으로 부호없는 변수와 부호있는 변수의 왼쪽 시프트에는 논리적 이동을 사용합니다. 산술 오른쪽 이동은 변수를 확장 할 수 있기 때문에 매우 중요합니다.

적용 가능한 경우 다른 컴파일러와 마찬가지로 이것을 사용합니다.



-7

많은 사람들에 따르면 컴파일러 :

  1. << 산술 왼쪽 쉬프트 또는 비트 왼쪽 쉬프트입니다.
  2. >> 산술 오른쪽 쉬프터 비트 오른쪽 쉬프트입니다.

3
"산술 오른쪽 시프트"와 "비트 오른쪽 시프트"는 다릅니다. 그것이 문제의 요점입니다. 그 질문은 " >>산술적이거나 비트 단위 (논리)입니까?" " >>산술 또는 비트 단위입니다."라고 대답했습니다 . 그것은 질문에 대답하지 않습니다.
wchargin

아니, <<>>연산자는 논리가 아닌 산술
shjeff
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.