icon
3장 : 연산자와 표현식

비트 연산자

이제는 컴퓨터의 가장 기본적인 단위인 비트(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 & 00110001 (십진수 5 & 3 → 1)
|비트 OR (Bitwise OR)두 비트 중 하나라도 1이면 1, 모두 0일 때만 0.0101 | 00110111 (십진수 5 | 3 → 7)
^비트 XOR (Bitwise XOR)두 비트가 서로 다를 때만 1, 같으면 0.0101 ^ 00110110 (십진수 5 ^ 3 → 6)
~비트 NOT (Bitwise NOT)비트를 반전시킵니다. (1은 0으로, 0은 1로).~01011010 (십진수 ~5 → -6)

비트 AND (&): 각 비트 위치에서 두 피연산자 비트가 모두 1일 때만 결과 비트가 1이 됩니다. (나머지는 0)

비트 AND 예시
  0101  (십진수 5)
& 0011  (십진수 3)
------
  0001  (십진수 1)

비트 OR (|): 각 비트 위치에서 두 피연산자 비트 중 하나라도 1이면 결과 비트가 1이 됩니다. (모두 0일 때만 0)

비트 OR 예시
  0101  (십진수 5)
| 0011  (십진수 3)
------
  0111  (십진수 7)
  0101  (십진수 5)
| 0011  (십진수 3)
------
  0111  (십진수 7)

비트 XOR (^): 각 비트 위치에서 두 피연산자 비트가 서로 다르면 결과 비트가 1이 됩니다. (같으면 0)

비트 XOR 예시
  0101  (십진수 5)
^ 0011  (십진수 3)
------
  0110  (십진수 6)

비트 NOT (~): 단항 연산자로, 피연산자의 모든 비트를 반전시킵니다. (1은 0으로, 0은 1로). 주의할 점은 정수형은 일반적으로 2의 보수(Two's Complement) 형태로 음수를 표현하기 때문에, 비트 NOT 연산의 결과는 예상과 다를 수 있습니다. 예를 들어, 4비트 정수 0101 (십진수 5)의 ~01011010 (십진수 -6)이 됩니다.

비트 NOT 예시
  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 << 11010 (십진수 5 << 1 → 10)
>>오른쪽 시프트 (Right Shift)비트를 오른쪽으로 이동시킵니다. 왼쪽 빈자리는 부호 비트(signed) 또는 0(unsigned)으로 채웁니다.0101 >> 10010 (십진수 5 >> 1 → 2)

왼쪽 시프트 (<<)

  • 비트를 왼쪽으로 n만큼 이동시키는 것은 해당 숫자에 $2^n$을 곱하는 것과 동일한 효과를 가집니다.
  • 가장 왼쪽에 있는 비트들은 버려지고, 오른쪽에 새로 생기는 빈 비트들은 0으로 채워집니다.
왼쪽 시프트 예시
  0000 0101  (십진수 5)
<< 1
----------
  0000 1010  (십진수 10)

오른쪽 시프트 (>>)

  • 비트를 오른쪽으로 n만큼 이동시키는 것은 해당 숫자를 $2^n$으로 나누고 소수점 이하를 버리는 것과 동일한 효과를 가집니다.
  • 가장 오른쪽에 있는 비트들은 버려집니다.
  • 중요
    • 부호 없는 정수 (unsigned int 등): 왼쪽에 새로 생기는 빈 비트들은 0으로 채워집니다.
    • 부호 있는 정수 (int 등): 왼쪽에 새로 생기는 빈 비트들은 구현에 따라 0으로 채워지거나(논리 시프트), 최상위 부호 비트의 값으로 채워질 수 있습니다(산술 시프트). 대부분의 시스템에서는 산술 시프트를 사용하여 음수의 부호를 유지하려 합니다. 따라서 부호 있는 정수에서는 오른쪽 시프트가 항상 나눗셈과 같은 결과를 보장하지 않을 수 있습니다. 혼동을 피하려면 unsigned 타입을 사용하는 것이 좋습니다.
오른쪽 시프트 예시
  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;
}

비트 연산자의 응용 예시

비트 연산자는 다양한 상황에서 활용됩니다.

  1. 플래그 (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)
  2. 데이터 암호화/복호화: 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;
  3. 빠른 곱셈/나눗셈: 정수를 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

혼동을 피하고 코드의 가독성을 높이기 위해 비트 연산자를 사용할 때도 괄호를 적절히 사용하는 것이 좋습니다.