오늘은 JS의 비트연산자(Bitwise Operator)에 대해 알아보려고 합니다.
비트연산자는 두개의 정수를 비트로 변환하여 각 자릿수에 맞춰 연산자로 비교하거나
혹은 한개의 정수를 비트로 변환 후 일정 수만큼 자릿수를 옮긴 후의 결과값을 다시 10진수로 반환하는 연산자입니다.
그럼 비트연산자에 대해 알아보기전에 먼저 비트가 무엇인지 알아보겠습니다.
사람은 0~9의 수를 사용하는 10진수를 통해 숫자를 표현합니다.
하지만 컴퓨터는 0과 1로만 이루어진 2진수라고도 부르는 비트를 사용합니다.
이는 전구와 스위치를 on/off 할 때 처럼 경우의 수가 2개인 상태를 표현하기 편리하며
꺼졌을 때는 0, 켜졌을 때는 1로 표현합니다.
하지만 하나의 비트로는 0과 1, 두 가지 경우밖에 표현하지 못하기 때문에 더 많은 경우의 수를 표현하기 위해 왼쪽에 비트를 추가해서 사용합니다.
우리가 10진수를 사용하면서 10이상의 수를 표현하기 위해 왼쪽에 숫자를 붙이는것과 같습니다.
비트의 개수가 늘어나면 표현할 수 있는 경우의 수는 기존의 경우의 수에서 2의 제곱만큼 늘어나게 됩니다.
위 사진을 보면 알 수 있듯이 n비트의 경우의 수는 2의 n제곱이 됩니다.
비트는 정수로도 변환할 수 있는데 이 때 비트의 자릿수에 따라 표현하는 숫자가 다릅니다.
가장 오른쪽 비트는 2의 0승으로 정수 1로 표현되며
두번째에 위치한 비트는 2의 1승으로 정수 2로 표현됩니다.
만약 8비트라면 왼쪽부터 각 비트의 값은 128, 64, 32, 16, 8, 4, 2, 1의 값을 가지며
전체 비트 중 해당 비트의 값이 1인 비트들을 모두 더하면 10진수 정수값을 얻을 수 있습니다.
예를 들어, "01110001"이라는 8비트를 10진수로 바꾸기 위해서는 아래 표처럼 계산해야 합니다.
JavaScript는 비트를 표현할 때 최대 32비트까지 표현할 수 있으며 32개의 비트로 표현할 수 있는 정수의 최대값은 아래와 같습니다.
하지만 이 최대값은 양수만 표현한다고 할 때 가능한 값으로
위처럼 양수만 표현하는 비트를 무부호 비트(부호가 없는 비트)라고 합니다.
즉, 32개의 모든 비트가 숫자를 표현하는데 사용됩니다.
그렇다면 비트로 음수를 표현하기 위해서는 어떻게 해야할까요?
비트를 음수로 표현할 때는 최상위 비트의 값으로 양수와 음수를 구분하는데
최상위 비트란 가장 왼쪽에 위치한 비트로 MSB(Most Significant Bit, 부호비트)라고 합니다.
MSB가 0일 경우 양수, 1일 경우 음수로 표현합니다.
음수 비트를 읽을 때는 양수 비트와는 반대로 1이 아닌 0을 가진 비트의 값을 계산합니다.
또한 컴퓨터는 음수 비트를 표현하기 위해 2의 보수 방식을 사용합니다.
2의 보수 방식이란 1의 보수 방식에서 1을 더한 값으로
1의 보수는 아래의 사진과 같이 정수 비트를 반대로 뒤집은 형태를 말합니다.
이렇게 비트를 뒤집고 나서 위에서 말한것처럼 0이 들어있는 비트의 값을 모두 더해 정수를 구하고
1을 더한 뒤 음수로 변경하면 음수 비트의 정수값을 구할 수 있습니다.
음수 비트 "10001110"의 정수는 아래와 같이 계산합니다.
여기서 중요한건 최상위 비트가 정수를 표현하는데 사용되지 않고 음수와 양수를 구분하는 용도로
사용되기 때문에 무부호비트에 비해 표현할 수 있는 최대값이 절반으로 줄어들게 됩니다.
만약 32비트라면 최대값은 아래와 같이 줄어듭니다.
하지만 그만큼 음수를 표현할 수 있기 때문에 실제 표현할 수 있는 범위는 거의 같다고 할 수 있습니다.
그래서 음수를 표현하는 n비트의 표현범위, 최대값, 최솟값은
아래와 같이 계산할 수 있습니다.
4비트로 음수와 양수를 표현 할 경우 표현할 수 있는 10진수와 비트의 경우의 수는 아래와 같으며
10진수 | 2진수 | 10진수 | 2진수(JS표현) |
---|---|---|---|
0 | 0000 | -1 | 1111 |
1 | 0001 | -2 | 1110 ( -0001 ) |
2 | 0010 | -3 | 1101 ( -0010 ) |
3 | 0011 | -4 | 1100 ( -0011 ) |
4 | 0100 | -5 | 1011 ( -0100 ) |
5 | 0101 | -6 | 1010 ( -0101 ) |
6 | 0110 | -7 | 1001 ( -0110) |
7 | 0111 | -8 | 1000 ( -0111 ) |
JS의 경우 연산할 때는 2의 보수 방식으로 처리되지만 출력시에는 양수 비트에 -기호를 사용하여 표현하기 때문에 주의해야 합니다.
주의!
논리연산자의 AND, OR과 헷갈릴 수 있으나 논리연산자는 기호를 두 개씩,
비트연산자는 한 개씩 사용합니다.
2진수로 변환했을 때 같은 자릿수가 양쪽 다 1인 경우에만 1을 반환합니다.
만약 하나라도 0이라면 0을 반환합니다.
const a = 5; // 0101
const b = 3; // 0011
console.log(a & b); // 0001, 1
양쪽 다 1이거나 둘 중 하나라도 1이 있다면 1을 반환하며 양 쪽 모두 1이 없을 경우에만 0을 반환합니다.
const a = 5; // 0101
const b = 3; // 0011
console.log(a | b); // 0111, 7
피연산자의 해당 비트의 값이 서로 다르다면 1을, 같다면 0을 반환합니다.
0과 0, 1과 1은 0을 반환하고 0과 1, 1과 0은 1을 반환합니다.
const a = 5; // 0101
const b = 3; // 0011
console.log(a ^ b); // 0110, 6
NOT연산자의 경우 대상 정수의 비트를 뒤집은 결과값을 반환합니다.
반환되는 정수는 위에서 설명했던것처럼 2의 보수 방식으로 음수를 취급하므로 -(n+1)의 결과값을 반환합니다.
그래서 해당 정수가 양수라면 음수가 나오게 되고, 음수라면 양수로 나오게 됩니다.
const a = 5; // 0101
console.log(~a); // 1010, -6
이제부터 나올 시프트 연산자들은 비트의 자릿수를 옮기는 역할을 합니다.
이 연산자는 특정 수의 비트에서 원하는만큼 왼쪽으로 자릿수를 옮기는 연산자로
아래의 예제에서 a는 비트로 변환될 정수이며 기호 뒤에 위치한 정수는 이동할 자릿수를 뜻합니다.
const a = 13; // 1101
console.log(a << 2); // 110100 (52)
왼쪽으로 두 자리씩 자릿수를 옮기며 새롭게 추가 되어야 하는 오른쪽 비트는 0으로 채워집니다.
지정 수만큼 비트를 오른쪽으로 이동시키는 연산자로 위 예제와 마찬가지로
a
는 변환될 수, 뒤의 정수는 이동할 자릿수를 뜻합니다.
다만, 이 기호의 경우 왼쪽을 채울 때 가장 앞쪽에 위치한 부호비트를 그대로 복사해서 채웁니다.
즉, 양수 비트에 >>
연산자를 적용하면 부호비트 0을 그대로 복사해서 적용하므로 양수로 반환되고
반대로 음수 비트에 >>
연산자를 적용 할 경우 부호비트 1이 복사되어 음수로 표현되게 됩니다.
// 양수
const a = 13; // 1101
console.log(a >> 2); // 0000...0011 (3)
// 음수
const b = -14; // 11110010 -> 11111100
console.log(b >> 2); // -11 (-4) = 1111..1100
이 연산자는 새롭게 추가되는 비트에 대해서 무조건 0으로 채우기 때문에
대상 정수가 양수 일 경우 위 >>
연산자와 똑같은 결과값을 반환합니다.
하지만 음수의 경우도 0으로 채우기 때문에 양수 -> 양수, 음수 -> 양수로 반환됩니다.
예를 들어, -1111은 0011로 변환되어 양수로 취급됩니다.
// 양수
let a = "1101";
let b = 2;
console.log(a >>> b); // 0000...0011 (3)
// 음수
let a = "-1101"; // 1111...0010 (-14)
let b = 2;
console.log(a >>> b); // 0011...1100 (1073741820)