비트 연산자
이제는 컴퓨터의 가장 기본적인 단위인 비트(Bit) 수준에서 데이터를 직접 조작하는 연산자에 대해 알아볼 차례입니다.
비트 연산자(Bitwise Operators) 는 정수형 데이터의 개별 비트(0 또는 1)를 대상으로 연산을 수행합니다.
비트 연산자는 일반적인 애플리케이션 개발에서는 자주 사용되지 않을 수 있지만, 특정 상황(예: 임베디드 시스템 프로그래밍, 하드웨어 제어, 네트워크 통신, 암호화, 데이터 압축, 성능 최적화)에서는 매우 강력하고 효율적인 도구가 됩니다.
이 장에서는 비트 연산자의 종류와 사용법, 그리고 기본적인 응용 예시를 살펴보겠습니다.
비트 연산자의 기본 개념
컴퓨터는 모든 데이터를 이진수(Binary Number) 형태로 저장합니다.
예를 들어, 십진수 5
는 이진수로 0101
로, 십진수 3
은 이진수로 0011
로 표현될 수 있습니다 (예시는 4비트 기준).
비트 연산자는 이러한 이진수의 개별 비트(0 또는 1)에 대해 논리 연산을 수행합니다.
비트 연산자는 정수형 데이터 타입(예: char
, short
, int
, long
, long long
, unsigned int
등)에만 적용할 수 있습니다.
실수형 데이터(float
, double
)에는 사용할 수 없습니다.
비트 논리 연산자
기본적인 논리 연산자(&&
, ||
, !
)와 유사하지만, 비트 단위로 연산을 수행합니다.
연산자 | 이름 | 설명 | 예시 (이진수) |
---|---|---|---|
& | 비트 AND (Bitwise AND) | 두 비트가 모두 1일 때만 1, 아니면 0. | 0101 & 0011 → 0001 (십진수 5 & 3 → 1) |
| | 비트 OR (Bitwise OR) | 두 비트 중 하나라도 1이면 1, 모두 0일 때만 0. | 0101 | 0011 → 0111 (십진수 5 | 3 → 7) |
^ | 비트 XOR (Bitwise XOR) | 두 비트가 서로 다를 때만 1, 같으면 0. | 0101 ^ 0011 → 0110 (십진수 5 ^ 3 → 6) |
~ | 비트 NOT (Bitwise NOT) | 비트를 반전시킵니다. (1은 0으로, 0은 1로). | ~0101 → 1010 (십진수 ~5 → -6) |
비트 AND (&
):
각 비트 위치에서 두 피연산자 비트가 모두 1
일 때만 결과 비트가 1
이 됩니다. (나머지는 0
)
0101 (십진수 5)
& 0011 (십진수 3)
------
0001 (십진수 1)
비트 OR (|
):
각 비트 위치에서 두 피연산자 비트 중 하나라도 1
이면 결과 비트가 1
이 됩니다. (모두 0
일 때만 0
)
0101 (십진수 5)
| 0011 (십진수 3)
------
0111 (십진수 7)
0101 (십진수 5)
| 0011 (십진수 3)
------
0111 (십진수 7)
비트 XOR (^
):
각 비트 위치에서 두 피연산자 비트가 서로 다르면 결과 비트가 1
이 됩니다. (같으면 0
)
0101 (십진수 5)
^ 0011 (십진수 3)
------
0110 (십진수 6)
비트 NOT (~
):
단항 연산자로, 피연산자의 모든 비트를 반전시킵니다. (1은 0으로, 0은 1로).
주의할 점은 정수형은 일반적으로 2의 보수(Two's Complement) 형태로 음수를 표현하기 때문에, 비트 NOT 연산의 결과는 예상과 다를 수 있습니다. 예를 들어, 4비트 정수 0101
(십진수 5)의 ~0101
은 1010
(십진수 -6)이 됩니다.
0101 (십진수 5)
~ 0101 (십진수 5)
------
1010 (십진수 -6, 2의 보수 표현 시)
#include <iostream>
// 숫자의 이진수 표현을 출력하는 헬퍼 함수 (선택 사항, 이해 돕기 위함)
void printBinary(unsigned int n) {
for (int i = 7; i >= 0; --i) { // 8비트 (char) 기준
std::cout << ((n >> i) & 1);
}
std::cout << std::endl;
}
int main() {
unsigned char a = 5; // 이진수: 0000 0101
unsigned char b = 3; // 이진수: 0000 0011
std::cout << "a = " << (int)a << ", b = " << (int)b << std::endl;
std::cout << "a (binary): "; printBinary(a);
std::cout << "b (binary): "; printBinary(b);
// 비트 AND
unsigned char result_and = a & b; // 0000 0001 (십진수 1)
std::cout << "a & b = " << (int)result_and << std::endl;
std::cout << "Result (binary): "; printBinary(result_and);
// 비트 OR
unsigned char result_or = a | b; // 0000 0111 (십진수 7)
std::cout << "a | b = " << (int)result_or << std::endl;
std::cout << "Result (binary): "; printBinary(result_or);
// 비트 XOR
unsigned char result_xor = a ^ b; // 0000 0110 (십진수 6)
std::cout << "a ^ b = " << (int)result_xor << std::endl;
std::cout << "Result (binary): "; printBinary(result_xor);
// 비트 NOT (음수 처리 방식 때문에 unsigned 사용 권장)
unsigned char c = 5; // 0000 0101
unsigned char result_not = ~c; // 1111 1010 (십진수 250, 8비트 unsigned char 기준)
std::cout << "~c = " << (int)result_not << std::endl;
std::cout << "Result (binary): "; printBinary(result_not);
return 0;
}
printBinary
함수는 unsigned int
를 받아 8비트만 출력하도록 임의로 만들었습니다. 실제 비트 수는 sizeof(int)
에 따라 달라질 수 있습니다.
비트 시프트 연산자
비트 시프트 연산자는 정수형 데이터의 비트를 왼쪽 또는 오른쪽으로 지정된 비트 수만큼 이동시킵니다.
연산자 | 이름 | 설명 | 예시 (이진수) |
---|---|---|---|
<< | 왼쪽 시프트 (Left Shift) | 비트를 왼쪽으로 이동시키고, 오른쪽 빈자리는 0으로 채웁니다. | 0101 << 1 → 1010 (십진수 5 << 1 → 10) |
>> | 오른쪽 시프트 (Right Shift) | 비트를 오른쪽으로 이동시킵니다. 왼쪽 빈자리는 부호 비트(signed) 또는 0(unsigned)으로 채웁니다. | 0101 >> 1 → 0010 (십진수 5 >> 1 → 2) |
왼쪽 시프트 (<<
)
- 비트를 왼쪽으로
n
만큼 이동시키는 것은 해당 숫자에 $2^n$을 곱하는 것과 동일한 효과를 가집니다. - 가장 왼쪽에 있는 비트들은 버려지고, 오른쪽에 새로 생기는 빈 비트들은
0
으로 채워집니다.
0000 0101 (십진수 5)
<< 1
----------
0000 1010 (십진수 10)
오른쪽 시프트 (>>
)
- 비트를 오른쪽으로
n
만큼 이동시키는 것은 해당 숫자를 $2^n$으로 나누고 소수점 이하를 버리는 것과 동일한 효과를 가집니다. - 가장 오른쪽에 있는 비트들은 버려집니다.
- 중요
- 부호 없는 정수 (unsigned int 등): 왼쪽에 새로 생기는 빈 비트들은
0
으로 채워집니다. - 부호 있는 정수 (int 등): 왼쪽에 새로 생기는 빈 비트들은 구현에 따라
0
으로 채워지거나(논리 시프트), 최상위 부호 비트의 값으로 채워질 수 있습니다(산술 시프트). 대부분의 시스템에서는 산술 시프트를 사용하여 음수의 부호를 유지하려 합니다. 따라서 부호 있는 정수에서는 오른쪽 시프트가 항상 나눗셈과 같은 결과를 보장하지 않을 수 있습니다. 혼동을 피하려면unsigned
타입을 사용하는 것이 좋습니다.
- 부호 없는 정수 (unsigned int 등): 왼쪽에 새로 생기는 빈 비트들은
0000 0101 (십진수 5)
>> 1
----------
0000 0010 (십진수 2)
1111 1011 (십진수 -5, 2의 보수)
>> 1 (부호 있는 경우)
----------
1111 1101 (십진수 -3, 시스템에 따라 다를 수 있음)
#include <iostream>
int main() {
unsigned int value = 10; // 이진수: 0000 1010 (가정: 8비트)
// 왼쪽 시프트
unsigned int left_shifted = value << 2; // 10 * 2^2 = 10 * 4 = 40
// 0000 1010 << 2 -> 0010 1000
std::cout << "10 << 2 = " << left_shifted << std::endl; // 출력: 40
// 오른쪽 시프트
unsigned int right_shifted = value >> 1; // 10 / 2^1 = 10 / 2 = 5 (정수 나눗셈)
// 0000 1010 >> 1 -> 0000 0101
std::cout << "10 >> 1 = " << right_shifted << std::endl; // 출력: 5
// 음수 (signed int) 시프트 주의
int negative_val = -16; // 2의 보수: ...1111 0000
int right_shifted_neg = negative_val >> 2; // -16 / 4 = -4 (시스템마다 결과가 다를 수 있음)
// ...1111 0000 >> 2 -> ...1111 1100
std::cout << "-16 >> 2 = " << right_shifted_neg << std::endl; // 출력: -4 (대부분의 시스템에서)
return 0;
}
비트 연산자의 응용 예시
비트 연산자는 다양한 상황에서 활용됩니다.
-
플래그 (Flag) 관리: 여러 개의 참/거짓 상태를 하나의 정수 변수에 비트로 저장하여 메모리를 절약하고 빠르게 상태를 확인/변경할 수 있습니다. 각 비트가 특정 상태를 나타내는 플래그 역할을 합니다.
플래그 관리 예시 const int OPTION_A = 0b0001; // 1 const int OPTION_B = 0b0010; // 2 const int OPTION_C = 0b0100; // 4 const int OPTION_D = 0b1000; // 8 int settings = 0; // 현재 설정 값 // 옵션 A 활성화 settings = settings | OPTION_A; // settings |= OPTION_A; (비트 OR로 해당 비트 1로 설정) std::cout << "Settings after enabling A: " << settings << std::endl; // 출력: 1 // 옵션 C 활성화 settings = settings | OPTION_C; // settings |= OPTION_C; std::cout << "Settings after enabling C: " << settings << std::endl; // 출력: 5 (0101) // 옵션 B가 활성화되어 있는지 확인 if ((settings & OPTION_B) == OPTION_B) { // 비트 AND로 해당 비트가 1인지 확인 std::cout << "Option B is enabled." << std::endl; } else { std::cout << "Option B is disabled." << std::endl; // 출력 } // 옵션 A 비활성화 settings = settings & (~OPTION_A); // settings &= (~OPTION_A); (비트 NOT과 AND로 해당 비트 0으로 설정) std::cout << "Settings after disabling A: " << settings << std::endl; // 출력: 4 (0100)
-
데이터 암호화/복호화: XOR 연산은 같은 값을 두 번 XOR하면 원래 값으로 돌아오는 특징이 있어 간단한 암호화/복호화에 사용되기도 합니다.
XOR 암호화/복호화 예시 char original_char = 'X'; // 0101 1000 char key = 0b00001111; // 0000 1111 char encrypted_char = original_char ^ key; // 0101 1000 ^ 0000 1111 = 0101 0111 (십진수 87, 'W') char decrypted_char = encrypted_char ^ key; // 0101 0111 ^ 0000 1111 = 0101 1000 ('X') std::cout << "Original: " << original_char << std::endl; std::cout << "Encrypted: " << encrypted_char << std::endl; std::cout << "Decrypted: " << decrypted_char << std::endl;
-
빠른 곱셈/나눗셈: 정수를 2의 거듭제곱으로 곱하거나 나눌 때, 비트 시프트 연산자는 일반 곱셈/나눗셈보다 훨씬 빠르게 동작합니다.
비트 시프트를 이용한 곱셈/나눗셈 예시 int num = 10; int multiplied = num << 3; // 10 * 2^3 = 10 * 8 = 80 int divided = num >> 1; // 10 / 2^1 = 10 / 2 = 5 std::cout << "10 * 8 (via shift): " << multiplied << std::endl; // 출력: 80 std::cout << "10 / 2 (via shift): " << divided << std::endl; // 출력: 5
연산자 우선순위 (비트 연산자 포함)
비트 연산자도 다른 연산자들과 마찬가지로 우선순위를 가집니다. 일반적으로 산술 연산자보다는 낮고, 논리 연산자보다는 높습니다.
- 높음: 단항 연산자 (
!
,~
,++
,--
) - 산술 연산자:
*
,/
,%
- 산술 연산자:
+
,-
- 시프트 연산자:
<<
,>>
- 관계 연산자:
<
,<=
,>
,>=
- 관계 연산자:
==
,!=
- 비트 AND:
&
- 비트 XOR:
^
- 비트 OR:
|
- 논리 AND:
&&
- 논리 OR:
||
- 낮음: 대입 연산자 (
=
,+=
,&=
등)
int a = 6; // 0110
int b = 3; // 0011
int c = 10;
int result = a & b | c; // (a & b)가 먼저 계산된 후 | c와 연산
// (0110 & 0011) | 1010
// 0010 | 1010
// 1010 (십진수 10)
std::cout << "a & b | c = " << result << std::endl; // 출력: 10
int result2 = (a & b) | c; // 괄호로 명확히
std::cout << "(a & b) | c = " << result2 << std::endl; // 출력: 10
// 비트 AND와 논리 AND 혼동 주의!
bool condition = (a & b) && (c > 5); // (0010은 0이 아니므로 true) && (10 > 5는 true) -> true
std::cout << "(a & b) && (c > 5) = " << std::boolalpha << condition << std::endl; // 출력: true
혼동을 피하고 코드의 가독성을 높이기 위해 비트 연산자를 사용할 때도 괄호를 적절히 사용하는 것이 좋습니다.