티스토리 뷰
Table of Contents
1. 개요
C언어에는 변수를 비트 단위로 조작할 수 있는 연산자들이 존재합니다. 이들을 비트연산자라고 하며, 비트연산자로는 <<, >>, &, |, ^, ~ 등이 있습니다. 여기에서는 비트연산자들의 연산 과정과 함께 이진수의 음수 표현에 대해 소개합니다. 이진수에 대해 모르거나 잘 기억이 안 난다면 이진수와 비트단위 를 먼저 보고 오시는 걸 추천합니다.
2. 비트연산자 모음
각 비트연산자가 일으키는 연산을 간단하게 설명해놓은 표입니다. (아래의 예시에 쓰인 숫자들은 십진수가 아니라 이진수입니다.)
연산자 |
사용 형태 | 예시 |
예시 결과 |
설명 |
<< (shift) |
a<<b | 11011 << 2 |
1101100 |
a의 비트들을 b 만큼 왼쪽으로 이동 시킨다. |
>> (shift) |
a>>b | 11011 >> 2 |
110 |
a의 비트들을 b 만큼 오른쪽으로 이동 시킨다. |
& (and) |
a&b | 1001 & 1010 |
1000 |
두 수에서 모두 1인 비트 단위에만 1이 들어간다. |
| (or) |
a|b | 1001 & 1010 |
1011 |
한 수 이상에서 1인 비트 단위에만 1이 들어간다. |
^ (xor) |
a^b | 1001 & 1010 |
11 |
두 수가 서로 다른 비트 단위에만 1이 들어간다. |
~ (not) |
~a | ~00100100 |
11011011 |
원래 수에서 0인 비트단위에는 1, 1인 비트 단위엔 0이 들어간다. |
3. <<, >> (shift 연산)
1. <<
a<<b 는 'a의 비트들을 b 만큼 왼쪽으로 이동 시킨다.'는 뜻입니다. 이 때, 빈 곳이 되는 오른쪽의 비트 단위들에는 0이 채워집니다. 아래의 프로그램을 돌려보면 어떤 식으로 이루어지는 지 감을 익힐 수 있습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | //code by RiKang, weeklyps.com #include <stdio.h> int main(void) { int a = 3; // 3 = 11 printf("3<<1 = %d\n",a<<1); // 110 = 6 printf("3<<2 = %d\n",a<<2); // 1100 = 12 printf("3<<3 = %d\n",a<<3); // 11000 = 24 a = 5; // 5 = 101 printf("5<<1 = %d\n",a<<1); // 1010 = 10 printf("5<<2 = %d\n",a<<2); // 10100 = 20 printf("5<<3 = %d\n",a<<3); // 101000 = 40 return 0; } | cs |
이진수의 표현에서 대개 왼쪽 비트 단위는 오른쪽 비트단위의 2배이지요. 따라서 a의 비트들을 모두 왼쪽으로 하나씩 이동시키면 a가 2배가 되는 경우가 많습니다. 또한 이를 이용해 a * 2 를, a << 1로 대체하는 경우가 존재합니다. 같은 원리로 를 a<<b로 대체하기도 합니다.
a = a<<b; 는 a<<=b; 로 축약 가능합니다.
2. >>
a>>b 는 'a의 비트들을 b 만큼 오른쪽으로 이동 시킨다.'는 뜻입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | //code by RiKang, weeklyps.com #include <stdio.h> int main(void) { int a = 13; // 13 = 1101 printf("13>>1 = %d\n",a>>1); // 110 = 6 printf("13>>2 = %d\n",a>>2); // 11 = 3 printf("13>>3 = %d\n",a>>3); // 1 = 1 printf("13>>4 = %d\n",a>>4); // 0 = 0 a = 21; // 21 = 10101 printf("21>>1 = %d\n",a>>1); // 1010 = 10 printf("21>>2 = %d\n",a>>2); // 101 = 5 printf("21>>3 = %d\n",a>>3); // 10 = 2 printf("21>>4 = %d\n",a>>4); // 1 = 1 printf("21>>5 = %d\n",a>>5); // 0 = 0 return 0; } | cs |
<< 와는 반대로, a의 비트들을 모두 오른쪽으로 하나씩 이동시키면 a가 2로 나눠지는 변수들이 많이 있습니다. (나눗셈 연산에서도 정수형 변수 / 정수형 변수 일 경우. 나머지는 버려집니다.) 이를 이용해 a / 2 를, a >> 1로 대체하는 경우가 있으며, 같은 원리로 를 a>>b로 대체하기도 합니다.
a = a>>b; 는 a>>=b; 로 축약 가능합니다.
4. & (and 연산)
&를 하면 양쪽의 두 수에서 모두 1인 비트 단위에만 1이 들어갑니다. 아래는 1비트 숫자들의 & 연산 결과입니다.
예시 |
결과 |
1 & 1 |
1 |
1 & 0 |
0 |
0 & 1 |
0 |
0 & 0 |
0 |
여러 비트를 가진 숫자들의 연산은 각각의 비트 단위들에 대해 독립적으로 이루어집니다.
1 2 3 4 5 6 7 8 9 | //code by RiKang, weeklyps.com #include <stdio.h> int main(void) { printf("12&9 = %d\n", 12&9); // 1100 & 1001 = 1000 = 8 printf("12&3 = %d\n", 12&3); // 1100 & 0011 = 0000 = 0 printf("12&5 = %d\n", 12&5); // 1100 & 0101 = 0100 = 4 printf("12&7 = %d\n", 12&7); // 1100 & 0111 = 0100 = 4 return 0; } | cs |
&에 대해 알아두면 좋은 문제
두 자연수 a, b가 입력으로 주어질 때, a 의 b 번째 비트가 1이면 yes, 0이면 no를 출력하시오.
(* a 의 1의 단위를 0 번째 비트, 2의 단위를 1 번째 비트, 4의 단위를 2 번째 비트,... 라고 하자.)
(* shift 연산을 함께 사용하면 편하다.)
5. | (or 연산)
|를 하면 한 수 이상에서 1인 비트 단위에만 1이 들어갑니다. 아래는 1비트 숫자들의 | 연산 결과입니다.
예시 | 결과 |
1 | 1 | 1 |
1 | 0 | 1 |
0 | 1 | 1 |
0 | 0 | 0 |
위의 연산이 각각의 비트 단위들에 대해 이루어집니다.
1 2 3 4 5 6 7 8 9 | //code by RiKang, weeklyps.com #include <stdio.h> int main(void) { printf("12|9 = %d\n", 12|9); // 1100 | 1001 = 1101 = 13 printf("12|3 = %d\n", 12|3); // 1100 | 0011 = 1111 = 15 printf("12|5 = %d\n", 12|5); // 1100 | 0101 = 1101 = 13 printf("12|7 = %d\n", 12|7); // 1100 | 0111 = 1111 = 15 return 0; } | cs |
6. ^ (xor 연산)
^를 하면 두 수가 서로 다른 비트 단위에만 1이 들어갑니다. 아래는 1비트 숫자들의 |^연산 결과입니다.
예시 | 결과 |
1 ^ 1 | 0 |
1 ^ 0 | 1 |
0 ^ 1 | 1 |
0 ^ 0 | 0 |
위의 연산이 각각의 비트 단위들에 대해 이루어집니다.
1 2 3 4 5 6 7 8 9 | //code by RiKang, weeklyps.com #include <stdio.h> int main(void) { printf("12^9 = %d\n", 12^9); // 1100 ^ 1001 = 0101 = 5 printf("12^3 = %d\n", 12^3); // 1100 ^ 0011 = 1111 = 15 printf("12^5 = %d\n", 12^5); // 1100 ^ 0101 = 1001 = 9 printf("12^7 = %d\n", 12^7); // 1100 ^ 0111 = 1011 = 11 return 0; } | cs |
7. ~ (not 연산)
~를 하면 0은 1로, 1은 0으로 바뀌게 됩니다. 아래는 1비트 숫자들의 ~연산 결과입니다.
예시 | 결과 |
~ 0 | 1 |
~ 1 | 0 |
위의 연산이 각각의 비트 단위들에 대해 이루어집니다.
1 2 3 4 5 6 7 8 9 | //code by RiKang, weeklyps.com #include <stdio.h> int main(void) { int a = 12; // 0000 0000 ....0000 1100 printf("(int) ~12 = %d\n", ~a); // 1111 1111 ....1111 0011 unsigned int b = 12; // 0000 0000 ....0000 1100 printf("(unsigned int) ~12 = %u\n", ~b); // 1111 1111 ....1111 0011 return 0; } | cs |
출력값
(int) ~12 = -13 (unsigned int) ~12 = 4294967283
위의 출력값을 살펴보도록 하겠습니다. 우선 int 와 unsigned int 는 4 바이트, 즉, 32 비트를 사용하며 12를 넣으면 8보다 큰 단위의 비트들은 모두 0으로 되어있습니다. ~을 쓰면 그 큰 단위의 0들이 모두 1로 바뀌니, ~b 가 큰 수로 출력되는 것은 당연한 이치입니다.
그런데 ~a 는 큰 수는 커녕 -13 으로 출력되었습니다. 이를 이해하기 위해선 이진수의 음수 표현에 쓰이는 보수의 개념에 대해 알아야 합니다.
8. 이진수의 음수 표현 : 보수
C언어에서는 부호가 있는 변수에서 양수를 표현할 때, 가장 큰 비트는 0으로 고정시킵니다. 그렇게 함으로서 가장 큰 비트가 0 이면 양수, 1 이면 음수로 간단하게 판별할 수 있는 것이지요. 인간이 이해하도록 쉽게 이 방식을 적용하면 아래와 같이 될 것입니다.
0000 0000 .... 0000 1100 = 12
1000 0000 .... 0000 1100 = -12
하지만 이런 방식은, 컴퓨터가 사칙연산을 수행할 때 비효율적이기 때문에 잘 쓰이지 않습니다. 게다가 0을 표현하는 이진수가 2개가 된다는 단점도 있지요. ( 0000 0000 = 0 = -0 = 1000 000 )
그 대신에 C언어에서는 를
로 인식하는 방법을 취합니다. (32bit 부호가 있는 정수, int 의 예시입니다.) 이렇게 하면 가장 큰 비트가 0 이면 양수, 1 이면 음수로 판별할 수 있는 건 같지만, 다른 비트 단위에서의 값이 달라지게 됩니다. 아래는 12에 대한 예시입니다.
(가 감이 안잡힌다면 대략
(64 - a) 정도로 계산해보는 것을 추천합니다. 어차피 큰 비트들은 다 1이 되니까요.)
0000 0000 .... 0000 1100 = 12
1111 1111 .... 1111 0100 = -12
(2^32 - 12 를 -12로 인식합니다.)
이러한 방식을 2의 보수라고 합니다. 이를 통해 왜 ~12를 출력하였을 때 -13이 나왔는지 알아보겠습니다.
12 = 0000 0000 .... 0000 1100
~12 = 1111 1111 .... 1111 0011 (not 연산으로 0->1, 1->0 으로 변환되었습니다.)
13 = 0000 0000 .... 0000 1101
-13 = 1111 1111 .... 1111 0011 (2^32 - 13으로 계산한 결과입니다.)
위의 결과에서 알 수 있듯이, int로 저장할 때 ~12 와 -13은 같은 수가 됩니다. 이러한 특징을 이용해 2의 보수를 계산할 땐, 0을 1로 1을 0으로 모두 바꾼 후, 1을 더해주면 편하게 계산할 수 있습니다.
-12 =
~12 +1 =
~(0000 0000 .... 0000 1100) + 1 =
1111 1111 .... 1111 0011 + 1 =
1111 1111 .... 1111 0100
'C, C++' 카테고리의 다른 글
[ C언어 ] 13. 연산자 우선순위 (0) | 2017.11.27 |
---|---|
[ C언어 ] 12. 배열 (0) | 2017.11.25 |
[ C언어 ] 10. 다중 반복문 (0) | 2017.11.22 |
[ C언어 ] 9. 변수와 상수 (0) | 2017.11.20 |
[ C언어 ] 8. 반복문 (for, while, do while ) (0) | 2017.11.20 |