C 언어 코딩 도장 7
비트 연산자 사용하기
지금까지 자료형을 바이트 단위로 구분하여 사용하였다면
비트 연산자는 비트 단위로 연산하는 연산자이다.
비트(Bit) : 2진수를 저장하는 단위, 컴퓨터에서 사용할 수 있는 최소 단위이며, 0과 1을 나타낸다.
바이트(Byte) : 8비트 크기의 단위이다.
연산자 | 설명 |
& | 비트 AND |
| | 비트 OR |
^ | 비트 XOR(배타적 OR, eXclusive OR) |
~ | 비트 NOT |
<< | 비트를 왼쪽으로 시프트 |
>> | 비트를 오른쪽으로 시프트 |
&= | 비트 AND 연산 후 할당 |
|= | 비트 OR 연산 후 할당 |
^= | 비트 XOR 연산 후 할당 |
<<= | 비트를 왼쪽으로 시프트한 후 할당 |
>>= | 비트를 오른쪽으로 시프트한 후 할당 |
비트 연산자는 비트로 옵션을 설정할 때 주로 사용한다.
플래그 : 비트로 옵션을 설정하는 방식
깃발을 올리면 on(1) 깃발을 내리면 off(0)
비트를 사용하면 저장 공간을 아낄 수 있다.
두 가지 상태만 나타내야 하는데, int나 long같이 바이트 단위로 저장하면 저장 공간을 낭비하게 된다.
비트 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
printf("%d\n", num1 | num2); //0000 0011
printf("%d\n", num1 ^ num2); //0000 0010
return 0;
}
프로그래머용 계산기 실행하는 방법
계산기 > 보기(V) > 프로그래머용(P)
https://dojang.io/mod/page/view.php?id=301
C 언어 코딩 도장: 36.9 배열을 활용하여 10진수를 2진수로 변환하기
이번에는 배열을 응용해서 10진수를 2진수로 변환한 뒤 배열에 넣어보겠습니다. 그런데 10진수에서 2진수로는 어떻게 변환할까요? 방법은 간단합니다. 10진수를 0이 될 때까지 2로 계속 나눈 뒤 나
dojang.io
https://dojang.io/mod/page/view.php?id=742
C 언어 코딩 도장: 85.9 10진수 - 16진수 - 2진수 변환표
Unit 28. while 반복문으로 Hello, world! 100번 출력하기
dojang.io
비트 연산은 두 값을 비트 단위로 나열한 뒤 각 자릿수를 비트 연산자로 연산한다. 각 자릿수의 연산은 독립적이며 다른 자릿수에 영향을 주지 않는다. 비트 단위로 연산한 각 자릿수를 모두 모으면 최종 결과가 된다.
& 연산자는 비트 AND이므로 두 비트가 모두 1일 때 1이다.
| 연산자는 비트 OR이므로 두 비트가 모두 0일 때만 0이다.
^ 연산자는 비트 XOR이므로 두 비트가 다를 때 1이다.
비트 NOT 연산자 사용하기
- ~x
#include <stdio.h>
int main()
{
unsigned char num1 = 162; //1010 0010
unsigned char num2;
num2 = ~num1;
printf("%u\n", num2); //0101 1101
return 0;
}
비트 NOT 연산자는 0은 1로 바꾸고, 1은 0으로 바꾼다. '비트 반전' 또는 '비트를 뒤집는다.' 라고도 한다.
unsigned char 자료형을 사용하는 이유는?
unsigned char 는 부호 없는 정수이며 1바이트 크기이다. 비트 연산으로 인해 부호 비트가 영향을 받지 않도록 부호 없는 자료형을 사용한다.
시프트 연산자 사용하기
C 언어에서 비트의 논리 연산 뿐만 아니라 비트의 각 자리를 이동시킬 수도 있다.
- a << b (a를 b 횟수만큼 왼쪽으로 이동시킨다.)
- a >> b (a를 b 횟수만큼 오른쪽으로 이동시킨다.)
#include <stdio.h>
int main()
{
unsigned char num1 = 3; //0000 0011
unsigned char num2 = 24; //0001 1000
printf("%u\n", num1 << 3); //0001 1000
printf("%u\n", num2 >> 2); //0000 0110
return 0;
}
3 << 3은 3 * 2^3과 같으므로 24가 되고, 24 >> 2는 24/2^2와 같으므로 6이 된다.
즉, 시프트 연산 <<은 2의 거듭제곱을 곱하기, >>은 2의 거듭제곱을 나누기이다.
비트 연산 후 할당하기
비트 연산자 뒤에 할당 연산자를 붙이면 a와 b를 비트 연산한 다음에 결과를 a에 할당한다.
- a &= b
- a |= b
- a ^= b
- a <<= b
- a >>= b
#include <stdio.h>
int main()
{
unsigned char num1 = 4; //0000 0100
unsigned char num2 = 4;
unsigned char num3 = 4;
unsigned char num4 = 4;
unsigned char num5 = 4;
num1 &= 5; //0000 0101(5)와 AND 연산 후 할당
num2 |= 2; //0000 0010(2)와 OR 연산 후 할당
num3 ^= 3; //0000 0011(3)과 XOR 연산 후 할당
num4 <<= 2; //비트를 왼쪽으로 두 번 이동한 후 할당
num5 >>= 2; //비트를 오른쪽으로 두 번 이동한 후 할당
printf("%u\n", num1);
printf("%u\n", num2);
printf("%u\n", num3);
printf("%u\n", num4);
printf("%u\n", num5);
return 0;
}
특히 비트 연산에서 연산과 할당이 한꺼번에 이루어지는 연산자는 플래그를 켜거나 끌 때 유용하게 활용된다.
23.5 Quiz 완료
#include <stdio.h>
int main()
{
unsigned char num1 = 1;
unsigned char num2 = 5;
printf("%u\n", num1 | num2);
printf("%u\n", num1 ^ num2);
printf("%u\n", num1 & num2);
num1 = ~num2;
printf("%u\n", num1);
return 0;
}
#include <stdio.h>
int main()
{
unsigned char num1 = 32;
num1 = num1 >> 4 << 1;
printf("%u\n", num1);
return 0;
}
23.8 심사문제 완료
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
unsigned int num1;
unsigned int num2;
scanf("%d %d", &num1, &num2);
printf("%u\n", num1 ^ num2);
printf("%u\n", num1 | num2);
printf("%u\n", num1 & num2);
printf("%u\n", ~num1);
return 0;
}
▲ 정답으로 제출한 코드
23.9 심사문제 완료
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
unsigned long long num1;
scanf("%llu", &num1);
num1 = num1 << 20 >> 4;
printf("%llu", num1);
return 0;
}
▲ 정답으로 제출한 코드
비트 연산자 응용하기
C 언어의 자료형은 부호 있는 정수와 부호 없는 정수 두 가지가 있다.
부호 있는 정수와 부호 없는 정수는 비트 연산의 결과가 다르다.
시프트 연산과 2의 거듭제곱 알아보
#include <stdio.h>
int main()
{
unsigned char num1 = 1;
printf("%u\n", num1 << 1);
printf("%u\n", num1 << 2);
printf("%u\n", num1 << 3);
printf("%u\n", num1 << 4);
printf("%u\n", num1 << 5);
printf("%u\n", num1 << 6);
printf("%u\n", num1 << 7);
return 0;
}
시프트 연산자는 2의 거듭제곱인 숫자를 빠르게 구할 때 유용하다.
0000 0001을 왼쪽으로 한 번씩 이동하면 2의 거듭제곱으로 수가 늘어난다. 여기서 비트의 각 자릿수는 2의 거듭제곱을 뜻하므로 비트의 이동 횟수는 지수(exponent)라고 할 수 있다. 컴퓨터에서 2의 거듭제곱은 매우 광범위하게 쓰이므로 비트 연산자가 유용하게 사용된다.
시프트 연산으로 자릿수를 넘어서는 경우 알아보기
시프트 연산자를 사용하여 비트가 첫째 자리나 마지막 자리를 넘어설 때까지 이동하면 어떻게 되는지 알아보자.
#include <stdio.h>
int main()
{
unsigned char num1 = 240; //1111 0000
unsigned char num2 = 15; //0000 1111
unsigned char num3, num4;
num3 = num1 << 2;
num4 = num2 >> 2;
printf("%u\n", num3);
printf("%u\n", num4);
return 0;
}
1111 0000(240)을 왼쪽으로 2번 이동시키면 맨 앞의 11은 사라져서 1100 0000(192)이 된다.
0000 1111(15)을 오른쪽으로 2번 이동시키면 맨 뒤의 11은 사라져서 0000 0011(3)이 된다.
즉, 비트에서 첫째 자리나 마지막 자리를 넘어서는 비트는 그대로 사라진다.
최상위 비트, 최하위 비트
최상위 비트(MSB : Most Significant Bit)
최하위 비트(LSB : Least Significant Bit)

부호 있는 자료형의 비트 연산 알아보기
부호 있는 자료형을 비트 연산할 때는 부호 비트를 조심해야 한다.
#include <stdio.h>
int main()
{
unsigned char num1 = 131; //1000 0011
char num2 = -125; //1000 0011
unsigned char num3;
char num4;
num3 = num1 >> 5; //0000 0100
num4 = num2 >> 5; //1111 1100
printf("%u\n", num3);
printf("%d\n", num4);
return 0;
}
부호 있는 자료형의 첫 번째 비트는 부호 비트라고 하고, 1이면 음수, 0이면 양수이다.
부호 있는 자료형의 비트를 오른쪽으로 이동시키면 모자라는 공간은 부호 비트의 값인 1로 채워진다.
비트 연산자는 부호 있는 자료형과 부호 없는 자료형이 다르게 동작한다.
#include <stdio.h>
int main()
{
char num1 = 67; //0100 0011
char num2;
num2 = num1 >> 5;
printf("%d\n", num2);
return 0;
}
부호있는 자료형에서 부호 비트가 0이기 때문에 오른쪽으로 이동시킬 때 모자라는 공간은 부호 비트의 값인 0으로 채운다.
#include <stdio.h>
int main()
{
char num1 = 113; //0111 0001
char num2 = -15; //1111 0001
char num3, num4, num5, num6;
num3 = num1 << 2;
num4 = num2 << 2;
num5 = num1 << 4;
num6 = num2 << 4;
printf("%d\n", num3); //1100 0100 부호 비트를 덮어쓰게 되므로 양수에서 음수가 됨
printf("%d\n", num4); //1100 0100
printf("%d\n", num5); //0001 0000
printf("%d\n", num6); //0001 0000 부호 비트를 덮어쓰게 되므로 음수에서 양수가 됨
return 0;
}
부호 있는 자료형에서 비트를 왼쪽으로 이동시켰을 때, 부호 비트에 위치한 숫자에 따라서 양수, 음수가 결정되기 때문에, 부호 있는 자료형에 시프트 연산을 할 때는 의도하지 않은 결과가 나올 수 있기 때문에 항상 부호비트를 생각해야 한다.
비트 연산자로 플래그 처리하기
플래그(flag)는 깃발에서 유래한 용어이다.
깃발을 위로 올리면 on, 아래로 내리면 off
비트를 활용하여 1이면 on, 0이면 off
8비트 크기의 자료형은 비트가 8개 들어가므로 8가지 상태를 저장할 수 있다.
int와 같은 4바이트 크기의 자료형은 32개의 상태를 저장할 수 있다.
특정 비트를 켜는 방법
- 플래그 |= 마스크
#include <stdio.h>
int main()
{
unsigned char flag = 0;
flag |= 1;
flag |= 2;
flag |= 4;
printf("%u\n", flag);
if (flag & 1)
printf("0000 0001은 켜져 있음\n");
else
printf("0000 0001은 꺼져 있음\n");
if (flag & 2)
printf("0000 0010은 켜져 있음\n");
else
printf("0000 0010은 꺼져 있음\n");
if (flag & 4)
printf("0000 0100은 켜져 있음\n");
else
printf("0000 0100은 꺼져 있음\n");
return 0;
}
마스크 : 플래그의 비트를 조작하거나 검사할 때 사용하는 숫자
플래그에 비트를 켜는 동작은 비트 OR 연산의 특성을 활용한 것이다.
플래그를 사용하는 곳은?
플래그는 적은 공간에 정보를 저장해야 하고, 빠른 속도가 필요할 때 사용한다.
가장 대표적인 장치가 CPU이다. CPU는 내부 저장 공간이 매우 작기 때문에 각종 상태를 비트로 저장한다.
플래그의 비트를 끄는 방법
- 플래스 &= ~마스크
#include <stdio.h>
int main()
{
unsigned char flag = 7;
flag &= ~2;
printf("%u\n", flag);
if (flag & 1)
printf("0000 0001은 켜져 있음\n");
else
printf("0000 0001은 꺼져 있음\n");
if (flag & 2)
printf("0000 0010은 켜져 있음\n");
else
printf("0000 0010은 꺼져 있음\n");
if (flag & 4)
printf("0000 0100은 켜져 있음\n");
else
printf("0000 0100은 꺼져 있음\n");
return 0;
}
플래그에 비트를 끄는 동작은 비트 AND 연산의 특성을 활용한 것이다.
비트가 켜져있으면 끄고, 꺼져있으면 키는 방법 = 토글(Toggle)
- 플래그 ^= 마스크
#include <stdio.h>
int main()
{
unsigned char flag = 7; //0000 0111
flag ^= 2; //0000 0010
flag ^= 8; //0000 1000
printf("%u\n", flag);
if (flag & 1)
printf("0000 0001은 켜져 있음\n");
else
printf("0000 0001은 꺼져 있음\n");
if (flag & 2)
printf("0000 0010은 켜져 있음\n");
else
printf("0000 0010은 꺼져 있음\n");
if (flag & 4)
printf("0000 0100은 켜져 있음\n");
else
printf("0000 0100은 꺼져 있음\n");
if (flag & 8)
printf("0000 1000은 켜져 있음\n");
else
printf("0000 1000은 꺼져 있음\n");
return 0;
}
플래그에 비트를 토글하는 동작은 비트 XOR 연산의 특성을 활용한 것이다.
24.5 Quiz 완료
#include <stdio.h>
int main()
{
unsigned char flag1 = 1 << 7;
unsigned char flag2 = 1 << 3;
flag1 |= 1 << 2;
flag1 &= ~(1 << 7);
flag2 ^= 1 << 3;
printf("%u %u\n", flag1, flag2);
return 0;
}
24.7 심사문제 완료
flag |= num1 << 3;
flag &= ~(num2 >> 2);
flag ^= 1 << 7;
▲ 정답으로 제출한 코드
연산자 우선순위 알아보기
모든 연산자는 우선순위가 정해져있다.
연산자의 계산 순서를 괄호()로 명확하게 나타내는 것이 좋다.
괄호 사용하기
#include <stdio.h>
int main()
{
int num1;
num1 = 35 + 1 * 2;
printf("%d\n", num1);
return 0;
}
연산자 우선순위에 따라 곱셈이 덧셈보다 우선하므로 1 * 2가 먼저 계산된다.
#include <stdio.h>
int main()
{
int num1;
num1 = (35 + 1) * 2;
printf("%d\n", num1);
return 0;
}
덧셈을 괄호()로 묶어주면 35 + 1이 먼저 계산되는 것을 볼 수 있다.
괄호를 사용한 계산식의 계산 순서
- 괄호를 사용한 연산자
- 우선순위가 높은 연산자
- 결합방향에 따라 순서대로 계산(주로 왼쪽에서 오른쪽)
보통 계산식의 의도를 명확하게 나타내기 위해 우선순위가 높은 연산자여도 괄호로 묶어줄 때가 많다.
연산자의 결합 방향 알아보기
보통 연산자는 → 방향으로 계산을 하지만 ← 방향인 것들도 있다.
대표적으로 변수 앞에 붙는 ++와 = 연산자가 ← 방향이다.
#include <stdio.h>
int main()
{
int num1 = 1;
int num2;
num2 = ++num1; //변수를 먼저 평가하고 앞에 있는 ++을 계산
printf("%d\n", num2);
return 0;
}
++, --와 = 뿐만 아니라 +, -, !, ~ 등 변수나 숫자 앞쪽에 붙는 연산자도 ← 방향이다. 단, 변수 뒤에 붙는 ++, --는 연산 방향이 → 방향이다.
결합 방향이 다른 연산자와 괄호 사용하기
#include <stdio.h>
int main()
{
int num1;
int num2 = 3;
num1 = 10 + 2 / (5 - 3) * ++num2;
printf("%d\n", num1);
return 0;
}
연산 순서
- (5 - 3)
- ++num2
- 2 / 2
- 1 * 4
- 10 + 4
논리, 비교, 시프트 연산자에 괄호 사용하기
산술 연산자와 마찬가지로 논리 연산자도 우선순위가 있고, 괄호로 묶을 수 있다.
#include <stdio.h>
#include <stdbool.h>
int main()
{
bool b1;
b1 = (false || false) && !false || false;
printf("%d\n", b1);
return 0;
}
연산 순서
- (false || false)
- !false
- false && true
- false || false
AND 연산자와 OR 연산자 중 어떤 것을 먼저 계산하더라도 OR 연산자의 특성 때문에 결과는 언제나 같지만 우선순위 규칙으로는 OR 연산자보다 AND 연산자가 높다.
비교 연산자를 사용할 때도 우선순위를 잘 따져야 한다.
#include <stdio.h>
int main()
{
int num1;
num1 = 5 == 5 < 10; // ==보다 <의 우선순위가 높음
printf("%d\n", num1);
return 0;
}
비교 연산자 중 ==보다 < 의 우선순위가 높다.
시프트 연산자와 산술 연산자를 섞어서 사용할 때도 연산자 우선순위에 주의해야 한다.
#include <stdio.h>
int main()
{
int num1 = 1;
int num2 = 2;
int num3;
num3 = num1 << 2 + num2 << 1;
printf("%d\n", num3);
return 0;
}
시프트 연산자와 산술 연산자 중 산술 연산자의 우선순위가 더 높다.
실무에서는 괄호로 계산 의도를 명확하게 나타낸다.
#include <stdio.h>
int main()
{
int num1 = 1;
int num2 = 2;
int num3;
num3 = (num1 << 2) + (num2 << 1);
printf("%d\n", num3);
return 0;
}
연산자는 우선순위가 있고, 우선순위에 따라 결과가 달라진다는 점만 기억하면 된다. 특히 연산자 우선순위가 신경 쓰이면 무조건 괄호를 사용하는 것이 좋다.
25.5 Quiz 완료
#include <stdio.h>
int main()
{
int num1 = 1;
int num2 = 1;
int num3;
num3 = 2 * ((1 << num1) + (2 >> num2));
printf("%d\n", num3);
return 0;
}
25.7 심사문제 완료
(num1 + num2) * 10 - num3
▲ 정답으로 제출한 코드