비트 연산자
보통 자료형을 사용할 땐 바이트(Byte)의 단위로 사용한다. 이를 비트(bit)의 단위로 쪼개서 사용하기 위해 비트 연산자를 사용한다. 비트 연산자의 종류는 다음과 같다.
연산자 | 설명 |
& | 비트 AND |
| | 비트 OR |
^ | 비트 XOR(배타적 OR, Exclusive OR) |
~ | 비트 NOT |
<< | 비트를 왼쪽으로 시프트 |
>> | 비트를 오른쪽으로 시프트 |
&= | 비트 AND 연산 후 할당 |
|= | 비트 OR 연산 후 할당 |
^= | 비트 XOR 연산 후 할당 |
<<= | 비트를 왼쪽으로 시프트한 후 할당 |
>>= | 비트를 오른쪽으로 시프트한 후 할당 |
비트 단위 연산자는 2진수에 관련한 연산이기 때문에 지금부터는 모든 변수의 값을 2진수로 표현한다.
문자형 변수 x, y에 각각 10, 20이 저장되어 있다고 한다. 문자형 변수를 사용하는 이유는 정수형 변수는 값이 너무 크기에 편의상 문자형 변수를 사용한다. 이를 이진수로 표현하면 각각을
x -> 0000 1010
y -> 0001 0100
이 된다. 이 둘을 x&y(비트 AND 연산)해주면 각각의 자리수에 대해 AND 연산이 이루어지게 된다. AND는 둘 다 1일 경우만 1이 되고, 나머지는 0이 된다.
x -> 0000 1010
& y -> 0001 0100
----------------------
x&y -> 0000 0000
결과 값은 0이 된다. 만약 |(OR)연산을 해준다면 OR은 둘 중 하나만 1이면 1이 되므로,
x -> 0000 1010
| y -> 0001 0100
----------------------
x | y -> 0001 1110
결과 값은 30이 된다. 이와 같은 원리로 나머지 연산자에 대해서도 연산이 이루어진다. 진리표는 다음과 같다.
연산자 | 비트1 | 비트2 | 결과 |
& | 0 | 0 | 0 |
0 | 1 | 0 | |
1 | 0 | 0 | |
1 | 1 | 1 | |
| | 0 | 0 | 0 |
0 | 1 | 1 | |
1 | 0 | 1 | |
1 | 1 | 1 | |
^ | 0 | 0 | 0 |
0 | 1 | 1 | |
1 | 0 | 1 | |
1 | 1 | 0 |
연산자 | 비트 | 결과 |
~ | 0 | 1 |
1 | 0 |
시프트 연산자
시프트 연산자는 비트를 이동시키는 연산자이다. 예를 들어 x=10이면, x를 2진수로 표현하면 다음과 같다.
0000 1010
이를 x<<1 해주면 모든 비트를 왼쪽으로 1씩 이동한다는 의미가 된다. 만약 맨 왼쪽의 숫자가 1이라면, 이는 사라지게 되고, 한칸씩 이동 후에 맨 오른쪽 칸은 0으로 채워준다.
x -> 0000 1010
x << 1 -> 0001 0100
반대로 x>>1을 해주면 오른쪽으로 한 칸씩 이동한다. 이 때, 제일 오른쪽에 있던 값은 버려지고, 왼쪽에서 0이 하나 추가 된다.
왼쪽 시프트 연산을 한번 해주면 원래 값에 2배, 오른쪽 시프트 연산을 한번 해주면 1/2배가 된다. 이를 일반화 하면 x << a는 x * 2^a을 계산한 값과 같고 x >> a는 x * (1/2)^a을 계산한 값과 같다.
비트 연산자와 할당 연산자를 함께 사용해 주는 경우가 있다. 예를 들어 x &=2 라는 식이 있는데, 이는 x = x&2를 줄여 쓴것과 같다. x ^= 2, x |= 2, x <<= 2는 각각 x = x^2, x = x|2, x = x<< 2와 같다.
※부호가 있는 정수의 시프트 연산
부호가 있는 정수는 최상위 비트를 부호비트로서 사용한다. 0이면 양수, 1이면 음수임을 뜻하는데, 음수인 정수를 이진수로 나타내면 대략 1000 0101 이런식으로 표현이 된다. 이를 오른쪽 시프트 연산(>>)을 수행하면, 부호를 유지하기 위해 맨 왼쪽에 0이 아닌 1이 추가되게 된다. 이를 오른쪽 시프트 연산을 해주면 1100 0010과 같이 된다.
부호가 있는 정수 64를 이진수로 표현하면 0100 0000이 된다. 이를 왼쪽 시프트 연산(<<)을 수행하면, 1000 0000이 되는데, 이때의 수는 2배인 128이 아닌 -128이 된다. 부호비트가 바뀌었기 때문이다. 이런 상황을 오버플로우라고 하는데, 시프트 연산을 할 때 이러한 문제가 일어나지 않도록 코드를 작성해야 한다.
플래그(flag)
깃발을 위로 올리면 on, 아래로 내리면 off를 뜻하는 데에서 의미를 가져옴. 해당 비트의 상태에 대해 flag와 비트 연산을 통해 비트를 켜고 끄는 동작을 수행할 수 있다. 플래그는 비교적 적은 공간을 사용해 정보를 저장해야 하고, 빠른 연산을 필요로 할때 사용된다. 대표적으로 CPU가 해당 기능을 통해 정보를 저장한다. 플래그를 이용한 기능은 아래와 같다. 비트를 조작할 때 사용하는 변수를 mask라고 한다.
flag를 통해 비트 켜기: flag |= mask
flag를 통해 비트 끄기: flag &= ~mask
flag를 통해 비트 토글: flag ^= mask
연산의 우선순위
C언어의 연산자들에는 우선순위가 존재한다. 우선순위가 낮은 연산을 먼저 시행하고 싶을 땐 ( )를 이용해 묶어주면 된다. 예를 들어 10 + 5 * 2는 5 * 2가 먼저 실행된 후에 계산된 값과 10을 더하는 순서로 진행되는데, 더하기를 먼저 해주고 싶다면 (10 + 5) * 2와 같이 괄호로 묶어주면 괄호 안에 있는 덧셈 연산이 먼저 실행되게 된다. 연산자의 종류와 우선수위는 다음과 같다.
우선순위 | 연산자 | 설명 | 결합 법칙(방향) |
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 | , | 쉼표(콤마) 연산자 | -> |
각 연산들에는 결합방향 이라는 게 있다. 이는 쉽게 말해 어느 방향으로 계산이 진행되는지를 나타낸 것이다. 보통은 좌에서 우로 진행되어서 ->이지만, 우에서 좌로 진행되는 경우도 있다. 전위 증가연산자를 예로 들면, ++a는 a의 값을 먼저 확인한 후에 ++연산을 실행하므로, 우에서 좌로(<-) 연산이 이루어진다. 할당 연산자도 마찬가지로 a = b에서 b의 값을 a에 할당하므로 우에서 좌로 연산이 이루어진다. 괄호로 인한 우선순위 변경은 산술 연산자 뿐만 아니라 논리 연산자에도 적용할 수 있다.
#23.5 퀴즈
4 -> 0000 0100
2 -> 0000 0010
둘을 &연산 하면 0000 0000(0)이 된다. 답은 a.
8 -> 0000 1000
3 -> 0000 0011
둘을 |연산 하면 0000 1011(11)이 된다. 답은 e.
^ (XOR)은 서로 값이 다르면 1, 같으면 0이다.
7 -> 0000 0111
10 -> 0000 1010
둘을 ^연산 하면 0000 1101(13)이 된다. 답은 c.
~(NOT)연산은 0은 1로, 1은 0으로 바꾸어준다.
15 -> 0000 1111
이를 ~연산하면 1111 0000(240)이 된다. 답은 d.
2 -> 0000 0010
이를 왼쪽으로 3번 시프트 하면 0001 0000이고 이를 오른쪽으로 한 번 시프트하면 0000 1000(8)이 된다. 답은 b.
#23.6 연습문제: 비트 논리 연산자 사용하기
다음 소스 코드를 완성하여 5, 4, 1, 250이 각 줄에 출력되게 만드세요.
#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;
}
실행 결과
5
4
1
250
num1 -> 0000 0001
num2 -> 0000 0101
1.
5 -> 0000 0101
num1과 num2를 |연산해주면 해당 값을 얻을 수 있다.
2.
4 -> 0000 0100
둘을 ^연산 해주면 맨 뒤의 1은 0이 되고 3번째 1은 그대로 1이 된다.
3.
1 -> 0000 0001
둘을 &연산 해주면 1만 남고 나머지가 0이 되어서 1이 된다.
4.
250 -> 1111 1010
num2의 값을 뒤집은 것과 일치하다. ~연산을 이용하면 된다.
#23.7 연습문제: 시프트 연산자 사용하기
#include <stdio.h>
int main()
{
unsigned char num1 = 32;
num1 = num1 >> 4 << ____;
printf("%u\n", num1);
return 0;
}
실행 결과
4
32 -> 0010 0000
32 >> 4 -> 0000 0010 (2)
4가 되게 하려면 왼쪽 시프트 연산을 1번 해주면 된다.
#23.8 심사문제: 비트 논리 연산자 사용하기
#include <stdio.h>
int main(){
unsigned int a, b;
scanf("%d %d", &a, &b);
printf("%u \n", a^b);
printf("%u \n", a|b);
printf("%u \n", a&b);
printf("%u \n", ~a);
return 0;
}
#23.9 심사문제: 시프트 연산자 사용하기
#include <stdio.h>
int main(){
unsigned long long a;
scanf("%lld", &a);
printf("%llu \n", a<<20>>4);
return 0;
}
시프트 연산자를 사용하면 된다. unsigned long long 형태이기 때문에 입력은 %lld, 출력은 %llu로 해주어야 한다.
8192 2^13
4096
2048
1024
#24.5 퀴즈
8192는 2^13이다. 1을 13번 왼쪽으로 옮기면 된다. 답은 c.
0011 1000을 왼쪽으로 2번 시프트하면 1110 0000이 된다. 이 때 한번 더 왼쪽으로 이동하면 맨 앞의 1은 버려지고 1100 0000만 남게된다. 답은 a.
아무 말이 없었으므로 부호가 있는 자료형이다. 부호가 있는 자료형은 오른쪽으로 시프트 할 때, 맨 앞에는 부호비트의 값이 추가 된다. 해당 변수는 부호비트가 1이므로 2번 이동하면 1110 0000이 된다. 답은 d.
부호비트가 0이므로 0을 추가하면서 오른쪽으로 3번 이동하면 된다. 3번 이동하면 0000 0110이 된다. 답은 b.
flag를 통해 비트를 끄는 방법은 마스크를 ~연산으로 뒤집어서 끄고자 하는 비트만 0으로 만든 후에 flag와 &연산을 해주는 것이다. 답은 b.
#24.6 연습문제: 시프트 연산과 플래그 활용하기
#include <stdio.h>
int main()
{
unsigned char flag1 = 1 << 7;
unsigned char flag2 = 1 << 3;
flag1 |= ①____<<____;
flag1 &= ②____<<____;
flag2 ^= ③____<<____;
printf("%u %u\n", flag1, flag2);
return 0;
}
실행 결과
4 0
flag1 -> 1000 0000
flag2 -> 0000 1000
flag1, flag2에 각각 위와 같이 저장되어 있다. flag1부터 보면, flag1이 4가 되려면 0000 0100이 되어야 한다. 그렇다면 첫번째 비트를 끄고 6번째 비트를 켜야 한다. 1번은 | 연산이 있기 때문에 비트 켜기에 관련된 문장이 들어가야 한다. 6번째 비트를 켜야하므로 1<<2를 작성해주면 된다. 2번은 비트 끄기에 관련된 연산이기 때문에 ~(1<<7)이 들어가야 한다. 3번의 경우, 모든 비트를 꺼야 하는데, 모든 비트를 끄는 방법은 끄고자 하는 값과 같은 값으로 ^연산을 해주면 된다. flag2의 값이 1<<3이기 때문에, 1<<3과 ^연산을 해주면 모든 값이 0이 된다.
1. 1 << 2
2. ~(1 << 7)
3. 1 << 3
#24.7 심사문제: 시프트 연산과 플래그 활용하기
flag |= num1 << 3;
flag &= ~(num2 >> 2);
flag ^= 1 << 7;
1. 비트를 켜는 연산은 |=이고 왼쪽으로 3번 이동한 후 켜야하기 때문에 flag |= num1 << 3이 된다.
2. 비트를 끄는 법은 해당 비트를 뒤집은 후 &연산 하는 것이다.
3. 첫 번째 비트를 토글해야 하는데 unsigned char은 8비트의 자료형이므로, 1<<7 해준 후에 ^연산을 해주면 된다.
#25.5 퀴즈
우선순위가 높은 순서대로 나열한다면 x++, !(++과 동급), +, <<, |이 된다. 답은 c.
우선 순위가 높은 (5 % 2)가 먼저 계산되어 1이 반환된다. 그 후 ++num1이 계산되어 2가 된다. 식은 2 * 2 + 1이 되어 5가 된다.
32 >> 5 는 1이므로 답은 b.
(false || false)가 먼저 계산되어 false를 반환한다. 그 후 !false가 계산되어 true가 되고 식은 true && true || false 가 된다. true && true는 true이고, true || false는 true이므로 답은 true.
+가 우선순위가 제일 높기 때문에 num2 + 1이 계산되어 5가 된다. <<연산이 비교 연산보다 우선순위가 높기 때문에 <<가 먼저 계산된다. 2 << 2 는 8이고, 8 < 5 는 거짓이므로 num2 = 0이 된다. 답은 a.
#25.6 연습문제: 괄호 사용하기
#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;
}
실행 결과
6
1. 시프트 연산자
2. 덧셈 연산자
3. 곱셈 연산자
순서대로 실행 되어 결과가 6이 되도록 해야한다. 해당 조건에 맞게 계산되게끔 괄호를 사용하면 답은 2 * ((1 << num1) + (2 >> num2))이 된다.
#25.7 심사문제: 괄호 사용하기
1. num1과 num2를 더하기
2. 1번 결과에 10 곱하기
3. 2번 결과에서 num3 빼기
먼저 1번을 괄호를 사용해 표현하면 (num1 + num2)이 된다. 그 후에 2번을 위해 1번 식의 뒤에 *10을 써주면 (num1 + num2) * 10이 된다. 마지막으로 3번을 위해 뒤에 -num3을 써주면 식은 (num1 + num2) * 10 - num3이 된다. 이 때, *는 -보다 우선순위가 높으므로, 굳이 괄호를 써주지 않아도 된다.
'Programming > C' 카테고리의 다른 글
[ProjectH4C] 코딩도장 Unit 27~31 Write-Up (0) | 2020.07.16 |
---|---|
[ProjectH4C] 코딩도장 Unit 26 Write-Up (0) | 2020.07.15 |
[ProjectH4C] 코딩도장 Unit 20 ~ 22 Write-Up (0) | 2020.07.14 |
[ProjectH4C] 코딩도장 Unit 17 ~ 19 Write-Up (0) | 2020.07.13 |
[ProjectH4C] 코드업(CodeUp) 기초 100제 1081~1100 Write-up (0) | 2020.07.09 |