[P4C] C언어 코딩 도장 : UNIT 23 ~ UNIT 25
Unit 23. 비트 연산자 사용하기
비트 연산자는 바이트 단위보다 더 작은 비트 단위로 연산하는 연산자이다.
- 비트(Bit): 2진수를 저장하는 단위다. 컴퓨터에서 사용할 수 있는 최소 단위이며 0과 1을 나타낸다.
- 바이트(Byte): 8비트 크기의 단위이다.
비트 연산자는 비트로 옵션을 설정할 때 주로 사용하며 저장 공간을 아낄 수 있는 장점이 있다.
특히 이런 방식을 플래그(flag)라고 부른다.
23.1 비트 AND,OR,XOR 연산자 사용하기
- a & b
- a | b
- a ^ b
#include <stdio.h>
int main()
{
unsigned char num1 = 1; // 0000 0001
unsigned char num2 = 3; // 0000 0011
printf("%d\n", num1 & num2); // 0000 0001: 01과 11을 비트 AND하면 01이 됨
printf("%d\n", num1 | num2); // 0000 0011: 01과 11을 비트 OR하면 11이 됨
printf("%d\n", num1 ^ num2); // 0000 0010: 01과 11을 비트 XOR하면 10이 됨
return 0;
}
unsigned char 자료형에 1을 할당했을 때 비트로 표현하면 0000 0001이다. 마찬가지로 3은 0000 0011이다.
비트 연산은 두 값을 비트 단위로 나열한 뒤 각 자릿수를 비트 연산자로 연산한다.
& 연산자는 비트 AND이므로 두 비트가 모두 1일 때 1다. 따라서 하나라도 0이면 0이 나온다.
| 연산자는 비트 OR이므로 두 비트 중 하나만 1이라도 1이다.
^ 연산자는 비트 XOR이므로 두 비트가 다를 때 1이다.
23.2 비트 NOT 연산자 사용하기
- ~x
#include <stdio.h>
int main()
{
unsigned char num1 = 162; // 162: 1010 0010
unsigned char num2;
num2 = ~num1;
printf("%u\n", num2); // 93: 0101 1101: num1의 비트 값을 뒤집음
return 0;
}
~ 연산자는 비트 NOT 연산자이다. 간단하게 0은 1로 1은 0으로 바꾸며 "비트를 뒤집는다" 또는 "비트 반전"이라고 말한다.
1010 0010의 각 비트를 뒤집으면 0101 1101이 되고, 10진수로 93이다. 즉, ~162는 93이다.
23.3 시프트 연산자 사용하기
- a << b
- a >> b
#include <stdio.h>
int main()
{
unsigned char num1 = 3; // 3: 0000 0011
unsigned char num2 = 24; // 24: 0001 1000
printf("%u\n", num1 << 3); // 24: 0001 1000: num1의 비트 값을 왼쪽으로 3번 이동
printf("%u\n", num2 >> 2); // 6: 0000 0110: num2의 비트 값을 오른쪽으로 2번 이동
return 0;
}
시프트 연산자를 사용하여 각 변수의 비트를 지정한 횟수대로 이동하였다. 실행 결과는 다음과 같다.
24
6
시프트 연산은 변수 << 이동할 비트 수 또는 변수 >> 이동할 비트 수 형식으로 사용한다. 즉 지정한 횟수대로 비트를 이동시키며 모자라는 공간은 0으로 채운다.
이때, 시프트 연산 <<은 2의 거듭제곱을 곱하기, >>은 2의 거듭제곱을 나누기와 같은 효과를 낸다.
23.4 비트 연산 후 할당하기
- a &= b
- a |= b
- a ^= b
- a <<= b
- a >>= b
#include <stdio.h>
int main()
{
unsigned char num1 = 4; // 4: 0000 0100
unsigned char num2 = 4; // 4: 0000 0100
unsigned char num3 = 4; // 4: 0000 0100
unsigned char num4 = 4; // 4: 0000 0100
unsigned char num5 = 4; // 4: 0000 0100
num1 &= 5; // 5(0000 0101) AND 연산 후 할당
num2 |= 2; // 2(0000 0010) OR 연산 후 할당
num3 ^= 3; // 3(0000 0011) XOR 연산 후 할당
num4 <<= 2; // 비트를 왼쪽으로 2번 이동한 후 할당
num5 >>= 2; // 비트를 오른쪽으로 2번 이동한 후 할당
printf("%u\n", num1); // 4: 0000 0100: 100과 101을 비트 AND하면 100이 됨
printf("%u\n", num2); // 6: 0000 0110: 100과 010을 비트 OR하면 110이 됨
printf("%u\n", num3); // 7: 0000 0111: 100과 011을 비트 XOR하면 111이 됨
printf("%u\n", num4); // 16: 0001 0000: 100을 왼쪽으로 2번 이동하면 10000이 됨
printf("%u\n", num5); // 1: 0000 0001: 100을 오른쪽으로 2번 이동하면 1이 됨
return 0;
}
&=는 다른 값과 비트 AND 연산을 한 뒤 다시 자기 자신에게 할당한다는 뜻이다. 다른 연산자도 마찬가지로 해당 연산을 수행한 뒤 다시 자기 자신에게 할당한다.
Unit 24. 비트 연산자 응용하기
24.1 시프트 연산과 2의 거듭제곱 알아보기
시프트 연산자는 2의 거듭제곱인 숫자를 빠르게 구할 때 유용하다.
#include <stdio.h>
int main()
{
unsigned char num1 = 1; // 1: 0000 0001
printf("%u\n", num1 << 1); // 2: 0000 0010: 2
printf("%u\n", num1 << 2); // 4: 0000 0100: 22
printf("%u\n", num1 << 3); // 8: 0000 1000: 23
printf("%u\n", num1 << 4); // 16: 0001 0000: 24
printf("%u\n", num1 << 5); // 32: 0010 0000: 25
printf("%u\n", num1 << 6); // 64: 0100 0000: 26
printf("%u\n", num1 << 7); // 128: 1000 0000: 27
return 0;
}
왼쪽으로 한 번씩 이동하면 2의 거듭제곱으로 수가 늘어난다.
즉, 비트의 각 자릿수는 2의 거듭제곱을 뜻하므로 비트의 이동 횟수는 지수(exponent)라 할 수 있다.
24.2 시프트 연산으로 자릿수를 넘어서는 경우 알아보기
#include <stdio.h>
int main()
{
unsigned char num1 = 240; // 240: 1111 0000
unsigned char num2 = 15; // 15: 0000 1111
unsigned char num3, num4;
num3 = num1 << 2; // num1의 비트 값을 왼쪽으로 2번 이동
num4 = num2 >> 2; // num2의 비트 값을 오른쪽으로 2번 이동
printf("%u\n", num3); // 192: 1100 0000: 맨 앞의 11이 사라져서 11000000이 됨
printf("%u\n", num4); // 3: 0000 0011: 맨 뒤의 11이 사라져서 00000011이 됨
return 0;
}
위와 같이 비트에서 첫째 자리나 마지막 자리를 넘어서는 비트는 그대로 사라진다.
24.3 부호 있는 자료형의 비트 연산 알아보기
부호 있는 자료형을 비트 연산할 때는 부호 비트를 조심해야 한다.
다음은 부호 없는 자료형과 부호 있는 자료형에 >> 연산을 해본 것이다.
#include <stdio.h>
int main()
{
unsigned char num1 = 131; // 131: 1000 0011
char num2 = -125; // -125: 1000 0011
unsigned char num3;
char num4;
num3 = num1 >> 5; // num1의 비트 값을 오른쪽으로 5번 이동
num4 = num2 >> 5; // num2의 비트 값을 오른쪽으로 5번 이동
printf("%u\n", num3); // 4: 0000 0100: 맨 뒤의 11은 사라지고 0000 0100이 됨
printf("%d\n", num4); // -4: 1111 1100: 모자라는 공간은 부호 비트의 값인 1로
// 채워지므로 1111 1100이 됨
return 0;
}
실행 결과는 다음과 같다.
4
-4
부호 있는 자료형의 첫 번째 비트는 부호 비트라고 하는데 이 비트가 1이면 음수, 0이면 양수이다.
부호 있는 자료형에 저장된 1000 0011은 첫 번째 비트가 1이므로 음수이고 10진수로는 -125가 된다. 이 비트들을 오른쪽으로 5번 이동시키면 모자라는 공간은 모두 부호 비트의 값으로 채워지기 때문에 1111 1100(-4)가 된다.
하지만 부호 없는 자료형은 비트를 오른쪽으로 이동해도 모자라는 공간은 모두 0으로 채워진다.
즉, 비트 연산자는 부호 있는 자료형과 부호 없는 자료형이 다르게 동작한다.
※ 부호 있는 자료형에서 비트를 왼쪽으로 이동시켰을 때는 부호 비트에 위치한 숫자에 따라 양수, 음수가 결정된다.
-> 따라서 부호 있는 자료형에 시프트 연산을 할 때는 의도치 않은 결과가 나올 수 있으므로 항상 부호 비트를 생각해야 한다.
24.4 비트 연산자로 플래그 처리하기
플래그(flag)는 깃발에서 유래한 용어이다. 보통 깃발을 위로 올리면 on, 아래로 내리면 off을 뜻한다. 이걸 정수의 비트에 활용하는 건데 비트가 1이면 on, 0이면 off를 나타낸다.
다음과 같이 8비트(1바이트) 크기의 자료형은 비트가 8개가 들어가므로 8가지 상태를 저장할 수 있다. 여기서는 두 번째 비트와 여덟 번째 비트가 켜진 상태이다.
0100 0001 // 두 번째 비트와 여덟 번째 비트가 켜진 상태(on)
* 플래그를 사용하는 이유
플래그는 적은 공간에 정보를 저장해야 하고, 빠른 속도가 필요할 때 사용한다. 가장 대표적인 장치가 CPU이다. CPU는 내부 저장 공간이 매우 작기 때문에 각종 상태를 비트로 저장한다.
* 특정 비트를 켜는 방법
- 플래그 |= 마스크
#include <stdio.h>
int main()
{
unsigned char flag = 0;
flag |= 1; // 0000 0001 마스크와 비트 OR로 여덟 번째 비트를 켬
flag |= 2; // 0000 0010 마스크와 비트 OR로 일곱 번째 비트를 켬
flag |= 4; // 0000 0100 마스크와 비트 OR로 여섯 번째 비트를 켬
printf("%u\n", flag); // 7: 0000 0111
if (flag & 1) // & 연산자로 0000 0001 비트가 켜져 있는지 확인
printf("0000 0001은 켜져 있음\n");
else
printf("0000 0001은 꺼져 있음\n");
if (flag & 2) // & 연산자로 0000 0010 비트가 켜져 있는지 확인
printf("0000 0010은 켜져 있음\n");
else
printf("0000 0010은 꺼져 있음\n");
if (flag & 4) // & 연산자로 0000 0100 비트가 켜져 있는지 확인
printf("0000 0100은 켜져 있음\n");
else
printf("0000 0100은 꺼져 있음\n");
return 0;
}
플래그로 사용할 변수에 |= 연산자와 숫자를 사용하여 특정 비트를 켠다. 여기서 플래그의 비트를 조작하거나 검사할 때 사용하는 숫자를 마스크(mask)라고 부른다.
플래그의 비트를 켜는 동작은 비트 OR 연산의 특성을 활용한 것인데 0 | 1과 1 | 1은 1이므로 flag의 비트가 꺼져있으면 비트를 켜고, 켜져 있으면 그대로 유지한다.
+ 플래그의 특정 비트가 켜져 있는지 검사하려면 & 연산자를 사용하면 된다. & 연산자는 두 비트가 모두 1이라야 1이다.
* 플래그의 비트를 끄는 방법
- 플래그 &= ~마스크
#include <stdio.h>
int main()
{
unsigned char flag = 7; // 7: 0000 0111
flag &= ~2; // 1111 1101 마스크 값 2의 비트를 뒤집은 뒤 비트 AND로 일곱 번째 비트를 끔
printf("%u\n", flag); // 5: 0000 0101
if (flag & 1) // & 연산자로 0000 0001 비트가 켜져 있는지 확인
printf("0000 0001은 켜져 있음\n");
else
printf("0000 0001은 꺼져 있음\n");
if (flag & 2) // & 연산자로 0000 0010 비트가 켜져 있는지 확인
printf("0000 0010은 켜져 있음\n");
else
printf("0000 0010은 꺼져 있음\n");
if (flag & 4) // & 연산자로 0000 0100 비트가 켜져 있는지 확인
printf("0000 0100은 켜져 있음\n");
else
printf("0000 0100은 꺼져 있음\n");
return 0;
}
마스크 값을 ~ 연산자로 비트를 뒤집은 뒤 &= 연산자를 사용하여 특정 비트를 끈다.
즉, 1111 1101에서 1은 flag의 원래 있던 비트 값을 유지한다. 비트 AND 연산이므로 0이었다면 그대로 0이되고, 1이었다면 그대로 1이 됩니다. 그리고 1111 1101에서 0은 비트 AND 연산을 했을 때 원래 비트가 1이든 0이든 항상 0이되므로 원하는 비트를 끄게 된다.
Unit 25. 연산자 우선순위 알아보기
C 언어의 경우 다음과 같이 다양한 연산자들끼리 우선순위가 정해져 있다.
1 |
x++ x-- ( ) [ ] . -> (자료형){값} |
증가 연산자(뒤, 후위) 감소 연산자(뒤, 후위) 함수 호출 배열 첨자 구조체/공용체 멤버 접근 포인터로 구조체/공용체 멤버 접근 복합 리터럴 |
→ |
2 |
++x --x +x -x ! ~ (자료형) *x &x sizeof |
증가 연산자(앞, 전위) 감소 연산자(앞, 전위) 단항 덧셈(양의 부호) 단항 뺄셈(음의 부호) 논리 NOT 비트 NOT 자료형 캐스팅(자료형 변환) 포인터 x 역참조 x의 주소 자료형의 크기 |
← |
3 |
* / % |
곱셈 나눗셈 나머지 |
→ |
4 |
+ - |
덧셈 뺄셈 |
→ |
5 |
<< >> |
비트를 왼쪽으로 시프트 비트를 오른쪽으로 시프트 |
→ |
6 |
< <= > >= |
작음 작거나 같음 큼 크거나 같음 |
→ |
7 |
== != |
같음 다름 |
→ |
8 | & | 비트 AND | → |
9 | ^ | 비트 XOR | → |
10 | | | 비트 OR | → |
11 | && | 논리 AND | → |
12 | || | 논리 OR | → |
13 | ? : | 삼항 연산자 | ← |
14 |
= += -= *= /= %= <<= >>= &= ^= |= |
할당 덧셈 후 할당 뺄셈 후 할당 곱셈 후 할당 나눗셈 후 할당 나머지 연산 후 할당 비트를 왼쪽으로 시프트한 후 할당 비트를 오른쪽으로 시프트한 후 할당 비트 AND 연산 후 할당 비트 XOR 연산 후 할당 비트 OR 연산 후 할당 |
← |
15 | , | 쉼표(콤마) 연산자 | → |
+ 실무에서는 연산자의 계산 순서를 ( ) (괄호)로 명확하게 나타내는 것을 선호한다.
25.1 괄호 사용하기
#include <stdio.h>
int main()
{
int num1;
num1 = (35 + 1) * 2; // 괄호를 사용하여 35 + 1을 먼저 계산한 뒤 2를 곱함
printf("%d\n", num1); // 72
return 0;
}
연산자 우선순위가 낮지만 먼저 계산해야 할 식은 ( ) (괄호)로 묶어준다.
25.2 연산자의 결합 방향 알아보기
#include <stdio.h>
int main()
{
int num1 = 1;
int num2;
num2 = ++num1; // 변수를 먼저 평가하고 앞에 있는 ++을 계산
printf("%d\n", num2); // 2
return 0;
}
보통 연산자는 → 방향으로 계산을 하지만 ← 방향인 것들도 있다.
대표적으로 변수 앞에 붙는 ++와 = 연산자가 ← 방향이다. ++, --와 =뿐만 아니라 +(양의 부호), -(음의 부호), !, ~ 등 변수나 숫자 앞쪽에 붙는 연산자도 ← 방향이다.
25.3 결합 방향이 다른 연산자와 괄호 사용하기
#include <stdio.h>
int main()
{
int num1;
int num2 = 3;
num1 = 10 + 2 / (5 - 3) * ++num2; // 괄호와 증가 연산자를 먼저 계산
printf("%d\n", num1); // 14
return 0;
}
다음은 결합 방향이 다른 연산자를 좀 더 많이 사용해본 것이다.
먼저 연산자 우선순위에 상관 없이 괄호부터 먼저 계산한 뒤 그리고 연산자 우선순위에 따라 순서대로 계산한다.
즉, 여기서는 괄호로 감싼 (5 - 3)이 먼저 계산된다. 그리고 연산자 우선순위가 가장 높은 ++ 증가 연산자(앞)가 계산된 뒤 *, /이 차례대로 계산된다.
25.4 논리, 비교, 시프트 연산자에 괄호 사용하기
산술 연산자와 마찬가지로 논리 연산자도 우선순위가 있고, 괄호로 묶을 수 있다.
#include <stdio.h>
#include <stdbool.h>
int main()
{
bool b1;
b1 = (false || false) && !false || false; // 논리 연산자의 우선순위는 !,&& , || 순
printf("%d\n", b1); // 0: false AND true 이므로 0
return 0;
}
논리 연산자의 우선순위는 !, &&, || 순이다.
#include <stdio.h>
int main()
{
int num1;
num1 = 5 == 5 < 10; // ==보다 <의 우선순위가 높음
printf("%d\n", num1); // 0
return 0;
}
비교 연산자 중 ==보다 <의 우선순위가 높다.
#include <stdio.h>
int main()
{
int num1 = 1;
int num2 = 2;
int num3;
num3 = num1 << 2 + num2 << 1; // <<보다 +의 우선순위가 높음
printf("%d\n", num3); // 32
return 0;
}
시프트 연산자와 산술 연산자 중 산술 연산자의 우선순위가 더 높다.
※ 연산자 우선순위가 신경쓰이면 무조건 괄호를 사용하기