본문 바로가기

비트 연산자

정수를 이루는 비트를 자리마다 계산하는 AND, OR, XOR, NOT과 자리를 옮기는 시프트 연산자를 정수 승격과 함께 익히고, 시프트 횟수의 조건과 음수의 오른쪽 시프트, 마스크로 플래그를 켜고 끄고 검사하는 방법, 비트 연산의 우선순위 함정을 다룹니다.

정수는 메모리에서 0과 1의 비트가 늘어선 모양으로 저장됩니다. 비트 연산자(bitwise operator)는 이 비트들을 자리마다 직접 계산하거나 자리를 옮기는 연산자로, 여러 개의 켜짐과 꺼짐을 정수 하나에 담는 플래그나, 통신 형식의 필드, 장치의 레지스터를 다룰 때 씁니다.

비트 연산자는 앞 글의 논리 연산자와 모양이 비슷하지만 하는 일이 다릅니다. 논리 연산자 &&는 피연산자 전체를 참과 거짓 하나로 보지만, 비트 연산자 &는 같은 자리의 비트끼리 계산하며 단락 평가도 하지 않습니다. 이 글은 네 가지 비트 논리 연산과 시프트, 마스크로 플래그를 다루는 방법을 봅니다.

자리마다 계산하는 비트 연산

비트 연산자는 정수 타입이나, 이름이 밖으로 드러나는 보통의 열거형에만 쓸 수 있고, float나 double에는 쓸 수 없습니다. 네 비트만 적어 보면 5는 0101이고 3은 0011이며, 각 연산자는 같은 자리의 비트를 다음처럼 계산합니다.

연산자자리마다의 규칙0101과 0011
&둘 다 1인 자리만 10001
|하나라도 1이면 10111
^서로 다른 자리만 10110
~모든 자리를 뒤집음피연산자의 전체 폭에서 뒤집음

비트 OR는 세로선 하나 |로, XOR(배타적 논리합)는 ^로 적습니다. 산술 연산처럼 비트 연산도 계산하기 전에 피연산자를 바꿉니다. 두 피연산자를 받는 &, |, ^는 일반 산술 변환으로 공통 타입을 맞추고, ~와 시프트는 int보다 작은 정수를 먼저 int로 승격합니다. C++11의 enum class는 정수로 저절로 바뀌지 않으므로 비트 연산에 바로 쓸 수 없습니다.

bitwise.cpp
#include <cstdint>
#include <iostream>

int main() {
    std::uint32_t a = 0b0101;
    std::uint32_t b = 0b0011;
    std::cout << (a & b) << ' ' << (a | b) << ' ' << (a ^ b) << '\n';
    std::cout << static_cast<std::uint32_t>(~a) << '\n';

    std::uint8_t small = 5;
    auto inverted = ~small;
    std::cout << inverted << ' ' << +static_cast<std::uint8_t>(inverted) << '\n';
}
1 7 6
4294967290
-6 250

첫 줄은 표의 결과를 십진수로 보인 것입니다. ~a는 32비트 전체를 뒤집으므로, 낮은 네 비트만 보면 1010이지만 위의 28비트가 모두 1이 되어 4294967290이 됩니다. 비트 패턴을 다룰 때는 이렇게 폭이 정해진 <cstdint>의 부호 없는 타입 std::uint32_t를 쓰면, 몇 비트를 다루는지와 부호의 영향이 없다는 것이 분명해집니다.

셋째 줄은 승격의 영향입니다. auto는 변수의 타입을 초기화하는 식의 타입으로 정하게 하는 키워드이므로, inverted의 타입은 ~small의 타입과 같습니다. auto의 자세한 규칙은 auto 키워드와 타입 추론에서 다룹니다. std::uint8_t 5는 ~를 적용하기 전에 int로 승격되므로, ~small의 타입은 int이고 값은 32비트 전체를 뒤집은 -6입니다. 모든 비트를 뒤집은 값이 -6으로 읽히는 것은 부호 있는 정수가 2의 보수(two's complement)로 표현되기 때문이며, 2의 보수에서 ~x의 값은 -x - 1입니다. 2의 보수로 음수를 나타내는 방법은 음수·보수·넘침에서 자세히 다룹니다.

원래의 8비트로 보려면 std::uint8_t로 다시 바꿔야 하며, 그 값은 11111010인 250입니다. std::uint8_t는 보통 unsigned char이어서 그냥 출력하면 글자로 보이므로, 앞에 단항 +를 붙여 정수로 출력했습니다.

시프트 연산자

E1 << n은 E1의 비트를 왼쪽으로 n자리 옮기고 빈 낮은 자리를 0으로 채우며, E1 >> n은 오른쪽으로 옮기고 낮은 자리의 비트를 버립니다. 그래서 왼쪽 시프트는 2ⁿ을 곱하는 것과, 오른쪽 시프트는 2ⁿ으로 나누는 것과 비슷하지만, 정해진 폭 안에서 비트를 옮기는 연산이라는 점이 다릅니다.

현재 C++ 기본 제공 왼쪽과 오른쪽 시프트의 공통 사전 조건, 결과 타입, 비트와 값 규칙 비교

BUILT-IN SHIFT CONTRACT

시프트는 승격된 타입과 유효한 횟수부터 확인한다

연산 기호보다 먼저 정수 승격 뒤의 왼쪽 타입과 시프트 횟수 범위를 확인합니다.

두 연산의 공통 사전 점검

1 · 승격
두 피연산자는 정수형 또는 범위 없는 열거형이며 정수 승격을 거칩니다.
2 · 횟수
0 ≤ n < N. 음수이거나 승격된 왼쪽 타입의 너비 N 이상이면 정의되지 않습니다.
3 · 결과
결과 타입은 승격된 왼쪽 피연산자 E1의 타입입니다.

E1 << n · 왼쪽 시프트

빈 낮은 자리는 0으로 채웁니다.

값 계약
E1 × 2ⁿ과 2ᴺ을 법으로 합동인 결과를 만듭니다.
unsigned 예
10u << 2 → 40u

E1 >> n · 오른쪽 시프트

낮은 자리 비트를 버리고 현재 표준의 나눗셈 규칙을 적용합니다.

값 계약
E1 / 2ⁿ을 음의 무한대 방향으로 반올림합니다.
signed 예
-5 >> 1 → -3 · 부호 확장

E1은 n보다 먼저 평가됩니다. 시프트를 성능 요령으로 선택하지 말고, 고정 너비의 비트 이동이 의도일 때 사용합니다.

그림처럼 시프트는 먼저 세 가지를 확인합니다. 두 피연산자는 정수 승격을 거치고, 결과의 타입은 승격된 왼쪽 피연산자의 타입이며, 옮기는 횟수 n은 0 이상이고 그 결과 타입의 비트 수보다 작아야 합니다. 32비트 int를 32자리 이상이나 음수 자리만큼 옮기면 정의되지 않은 동작입니다.

C++20부터 왼쪽 시프트의 결과는 E1 × 2ⁿ을 그 타입의 비트 수로 순환시킨 값으로 정해졌고, 오른쪽 시프트는 E1 / 2ⁿ을 음의 무한대 쪽으로 내린 값입니다. 부호 있는 음수를 오른쪽으로 옮기면 빈 높은 자리를 부호 비트로 채우는 부호 확장이 일어납니다. 예전 표준에서는 음수의 오른쪽 시프트가 구현이 정하는 동작이었으므로, 오래된 자료를 볼 때는 어느 표준을 기준으로 하는지 확인합니다.

shift.cpp
#include <cstdint>
#include <iostream>

int main() {
    std::uint32_t value = 10;
    std::cout << (value << 2) << ' ' << (value >> 1) << ' ' << (-5 >> 1) << '\n';
}
40 5 -3

10을 왼쪽으로 두 자리 옮기면 40, 오른쪽으로 한 자리 옮기면 5입니다. -5를 오른쪽으로 한 자리 옮긴 결과가 -2가 아니라 -3인 것은, -5 / 2인 -2.5를 0 쪽이 아니라 음의 무한대 쪽으로 내리기 때문이며, 이 점에서 정수 나눗셈 -5 / 2의 -2와 다릅니다. 시프트를 곱셈과 나눗셈의 대신으로 쓰지 말고, 비트의 자리를 옮기려는 곳에만 씁니다.

마스크와 플래그

마스크(mask)는 관심 있는 비트의 자리만 1로 표시한 값입니다. 서로 겹치지 않는 플래그를 비트 하나씩으로 정해 두면, 여러 개의 켜짐과 꺼짐을 정수 하나에 담고 마스크로 켜고 끄고 검사할 수 있습니다.

flags.cpp
#include <cstdint>
#include <iostream>

int main() {
    using Flags = std::uint32_t;
    constexpr Flags READ = Flags{1} << 0;
    constexpr Flags WRITE = Flags{1} << 1;
    constexpr Flags DEBUG = Flags{1} << 2;

    Flags flags = READ | DEBUG;
    flags |= WRITE;
    std::cout << flags << '\n';
    flags &= static_cast<Flags>(~READ);
    std::cout << flags << '\n';
    flags ^= DEBUG;
    std::cout << flags << ' ' << std::boolalpha
              << ((flags & (READ | WRITE)) != 0) << ' '
              << ((flags & (READ | WRITE)) == (READ | WRITE)) << '\n';
}
7
6
2 true false

using Flags = std::uint32_t;는 std::uint32_t에 Flags라는 다른 이름을 붙이는 타입 별칭(type alias)이어서, 코드의 Flags는 모두 std::uint32_t와 같은 타입입니다. READ, WRITE, DEBUG는 각각 2⁰, 2¹, 2²의 자리 하나만 켠 값이고, 처음 flags는 0101인 5입니다. flags |= WRITE는 그 자리를 켜는 설정으로 0111인 7을, flags &= ~READ는 그 자리만 끄는 해제로 0110인 6을, flags ^= DEBUG는 그 자리를 뒤집는 토글로 0010인 2를 만듭니다. ~READ를 Flags로 다시 바꾼 것은, 더 좁은 타입이라면 승격된 int의 폭에서 뒤집힌 값이 섞이지 않게 하기 위해서입니다.

마스크 연산은 바꾸는 자리와 검사 조건을 분리한다

원 도표의 독립 0101 실험표와 별도로 본문 실제 코드의 누적 상태 0101→0111→0110→0010을 보여 줄 수 있다. 표의 독립 결과를 연속 상태로 오인하지 않도록 구분한다.

C++ 비트 플래그를 설정, 해제, 토글하는 식과 여러 비트를 검사할 때 any와 all 조건이 달라지는 이유를 정리한 표

FLAG MASK LEDGER

비트 변경과 any·all 판정은 별개 연산이다

마스크가 가리키는 비트를 어떻게 바꿀지와, 검사 결과를 어떤 조건으로 판정할지를 따로 읽습니다. 각 예시는 flags=0101에서 독립적으로 시작합니다.

핵심 관계설정·해제·토글은 각각 flags=0101에서 독립적으로 시작한다. 검사에서는 0101 & 0011 = 0001을 any와 all의 서로 다른 질문으로 나눈다.0101 | 0010설정: 01110101 & ~0001해제: 01000101 ^ 0100토글: 0001검사: 00010101 & 0011any: != 0trueall: == 0011false
핵심 관계설정·해제·토글은 각각 flags=0101에서 독립적으로 시작한다. 검사에서는 0101 & 0011 = 0001을 any와 all의 서로 다른 질문으로 나눈다.0101| 0010설정01110101& ~0001해제01000101^ 0100토글0001검사: 00010101 & 0011any!= 0: trueall== 0011false

설정·해제·토글은 각각 flags=0101에서 독립적으로 시작한다. 검사에서는 0101 & 0011 = 0001을 any와 all의 서로 다른 질문으로 나눈다.

관계도 해설과 예제 원자료

타입과 비트 배치 계약

flags와 mask는 필요한 너비를 가진 같은 unsigned 타입으로 둡니다.

좁은 unsigned도 ~ 전에 승격될 수 있으므로 static_cast<Flags>(~mask)로 선택한 너비에 되돌립니다.

READ=0001 · WRITE=0010 · DEBUG=0100 · 현재 flags=0101

초기값 flags=0101에서 마스크 연산을 적용한 결과
목적 식 · 마스크 의미 · flags=0101 예
설정 flags |= mask — 1인 자리를 켭니다.

예: 0101 | 0010 → 0111

해제 flags &= static_cast<Flags>(~mask) — 승격 후 반전한 값을 Flags 너비로 되돌려 해당 자리만 0으로 만듭니다.

예: 0101 & ~0001 → 0100

토글 flags ^= mask — 1인 자리의 상태를 반전합니다.

예: 0101 ^ 0100 → 0001

하나라도 확인 (flags & mask) != 0 — 겹치는 비트가 하나 이상인지 봅니다.

예: 0101 & 0011 = 0001 · 0001 != 0 → true

모두 확인 (flags & mask) == mask — 요구한 비트가 전부 켜졌는지 봅니다.

예: 0101 & 0011 = 0001 · 0001 == 0011 → false

상태 변경

|= mask
설정 · 0101 | 0010 → 0111
&= static_cast<Flags>(~mask)
해제 · 선택한 너비에서 0101 & ~0001 → 0100
^= mask
토글 · 0101 ^ 0100 → 0001

조건 검사

(flags & mask) != 0
하나라도 켜졌는지 확인 · 0101 & 0011 = 0001 · 0001 != 0 → true
(flags & mask) == mask
모두 켜졌는지 확인 · 0101 & 0011 = 0001 · 0001 == 0011 → false

단일 비트 마스크에서는 any와 all이 같아 보이지만, 결합 마스크에서는 결과가 달라집니다. 여러 실행 흐름이 같은 값을 바꾸면 별도의 동기화나 원자 연산이 필요합니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
본문·주석초깃값
예의 값과 변화
관찰·상황값·보충 정보변화·이유
비트 초깃값

본문의 누적 변경 예는 READ와 DEBUG가 켜진 0101에서 시작한다.

  • 초기 상태: 0101
  • 켜진 플래그: READ=0001, DEBUG=0100
  • READ와 DEBUG의 비트가 1이고 WRITE=0010의 비트는 0이다. 독립 실험표와 구분되는 누적 코드이다.
WRITE 설정·READ 해제

WRITE를 켠 뒤 READ를 끈다.

  • WRITE 설정: 0101 | 0010 = 0111
  • READ 해제: 0111 & ~0001 = 0110
  • OR는 WRITE 비트를 1로 만들고, READ 마스크의 보수와 AND하면 READ 비트만 0이 된다.
DEBUG 토글

DEBUG 비트를 XOR로 뒤집어 최종 0010을 얻는다.

  • 이전: 0110
  • 연산: ^= DEBUG(0100)
  • 최종: 0010
  • DEBUG=0100과 XOR하므로 켜져 있던 DEBUG 비트가 1에서 0으로 바뀐다. WRITE 비트는 1을 유지한다.

마스크 연산은 바꾸는 자리와 검사 조건을 분리한다

원 도표의 독립 0101 실험표와 별도로 본문 실제 코드의 누적 상태 0101→0111→0110→0010을 보여 줄 수 있다. 표의 독립 결과를 연속 상태로 오인하지 않도록 구분한다.

C++ 비트 플래그를 설정, 해제, 토글하는 식과 여러 비트를 검사할 때 any와 all 조건이 달라지는 이유를 정리한 표

FLAG MASK LEDGER

비트 변경과 any·all 판정은 별개 연산이다

마스크가 가리키는 비트를 어떻게 바꿀지와, 검사 결과를 어떤 조건으로 판정할지를 따로 읽습니다. 각 예시는 flags=0101에서 독립적으로 시작합니다.

핵심 관계설정·해제·토글은 각각 flags=0101에서 독립적으로 시작한다. 검사에서는 0101 & 0011 = 0001을 any와 all의 서로 다른 질문으로 나눈다.0101 | 0010설정: 01110101 & ~0001해제: 01000101 ^ 0100토글: 0001검사: 00010101 & 0011any: != 0trueall: == 0011false
핵심 관계설정·해제·토글은 각각 flags=0101에서 독립적으로 시작한다. 검사에서는 0101 & 0011 = 0001을 any와 all의 서로 다른 질문으로 나눈다.0101| 0010설정01110101& ~0001해제01000101^ 0100토글0001검사: 00010101 & 0011any!= 0: trueall== 0011false

설정·해제·토글은 각각 flags=0101에서 독립적으로 시작한다. 검사에서는 0101 & 0011 = 0001을 any와 all의 서로 다른 질문으로 나눈다.

관계도 해설과 예제 원자료

타입과 비트 배치 계약

flags와 mask는 필요한 너비를 가진 같은 unsigned 타입으로 둡니다.

좁은 unsigned도 ~ 전에 승격될 수 있으므로 static_cast<Flags>(~mask)로 선택한 너비에 되돌립니다.

READ=0001 · WRITE=0010 · DEBUG=0100 · 현재 flags=0101

초기값 flags=0101에서 마스크 연산을 적용한 결과
목적 식 · 마스크 의미 · flags=0101 예
설정 flags |= mask — 1인 자리를 켭니다.

예: 0101 | 0010 → 0111

해제 flags &= static_cast<Flags>(~mask) — 승격 후 반전한 값을 Flags 너비로 되돌려 해당 자리만 0으로 만듭니다.

예: 0101 & ~0001 → 0100

토글 flags ^= mask — 1인 자리의 상태를 반전합니다.

예: 0101 ^ 0100 → 0001

하나라도 확인 (flags & mask) != 0 — 겹치는 비트가 하나 이상인지 봅니다.

예: 0101 & 0011 = 0001 · 0001 != 0 → true

모두 확인 (flags & mask) == mask — 요구한 비트가 전부 켜졌는지 봅니다.

예: 0101 & 0011 = 0001 · 0001 == 0011 → false

상태 변경

|= mask
설정 · 0101 | 0010 → 0111
&= static_cast<Flags>(~mask)
해제 · 선택한 너비에서 0101 & ~0001 → 0100
^= mask
토글 · 0101 ^ 0100 → 0001

조건 검사

(flags & mask) != 0
하나라도 켜졌는지 확인 · 0101 & 0011 = 0001 · 0001 != 0 → true
(flags & mask) == mask
모두 켜졌는지 확인 · 0101 & 0011 = 0001 · 0001 == 0011 → false

단일 비트 마스크에서는 any와 all이 같아 보이지만, 결합 마스크에서는 결과가 달라집니다. 여러 실행 흐름이 같은 값을 바꾸면 별도의 동기화나 원자 연산이 필요합니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
본문·주석초깃값
예의 값과 변화
관찰·상황값·보충 정보변화·이유
비트 초깃값

본문의 누적 변경 예는 READ와 DEBUG가 켜진 0101에서 시작한다.

  • 초기 상태: 0101
  • 켜진 플래그: READ=0001, DEBUG=0100
  • READ와 DEBUG의 비트가 1이고 WRITE=0010의 비트는 0이다. 독립 실험표와 구분되는 누적 코드이다.
WRITE 설정·READ 해제

WRITE를 켠 뒤 READ를 끈다.

  • WRITE 설정: 0101 | 0010 = 0111
  • READ 해제: 0111 & ~0001 = 0110
  • OR는 WRITE 비트를 1로 만들고, READ 마스크의 보수와 AND하면 READ 비트만 0이 된다.
DEBUG 토글

DEBUG 비트를 XOR로 뒤집어 최종 0010을 얻는다.

  • 이전: 0110
  • 연산: ^= DEBUG(0100)
  • 최종: 0010
  • DEBUG=0100과 XOR하므로 켜져 있던 DEBUG 비트가 1에서 0으로 바뀐다. WRITE 비트는 1을 유지한다.

위 코드는 같은 flags를 차례로 바꾸었지만, 그림의 설정과 해제, 토글은 각각 처음의 0101에서 따로 시작한 결과입니다. 그림의 검사처럼, 여러 비트를 합친 마스크로 검사할 때는 무엇을 묻는지를 정해야 합니다. (flags & mask) != 0은 마스크의 비트 가운데 하나라도 켜졌는지를, (flags & mask) == mask는 모두 켜졌는지를 묻습니다. 마지막 flags는 WRITE만 켜져 있으므로, READ와 WRITE 가운데 하나는 있지만 둘 다 있지는 않아 true와 false가 됩니다.

이 플래그가 있다는 것은 정수의 비트가 켜져 있다는 뜻일 뿐, 그 사용자에게 정말 권한이 있는지를 검증했다는 뜻은 아닙니다. 또 여러 스레드가 같은 플래그를 동시에 바꾼다면, |= 같은 비트 연산은 읽고 계산하고 쓰는 여러 단계여서 원자적이지 않으므로, 뒤의 장에서 배울 원자 타입이나 잠금으로 보호해야 합니다.

XOR로 되돌리기

XOR에는 같은 값으로 두 번 계산하면 원래 값으로 돌아오는 성질이 있습니다. x ^ key ^ key는 언제나 x이므로, 0x58을 0x0f로 XOR하면 0x57이 되고, 다시 0x0f로 XOR하면 0x58로 돌아옵니다.

이 성질 때문에 XOR를 간단한 암호처럼 쓰는 예를 볼 수 있지만, 고정된 키로 XOR한 데이터는 패턴이 드러나고 키를 쉽게 알아낼 수 있어서 보안 기능이 될 수 없습니다. 데이터를 보호해야 한다면 검증된 암호 라이브러리를 씁니다.

우선순위와 괄호

비트 연산자는 우선순위가 낮은 편이어서, 다른 연산자와 섞으면 생각과 다르게 묶이기 쉽습니다. 높은 쪽부터 단항 ~와 !, 산술 연산자, 시프트, 크기와 같음 비교, 비트 AND &, 비트 XOR ^, 비트 OR, 논리 AND와 논리 OR, 대입의 순서입니다.

특히 같음 비교가 비트 AND보다 먼저 묶인다는 점이 함정입니다. flags & WRITE != 0은 flags & (WRITE != 0)으로 묶이고, WRITE != 0은 true, 즉 1이 되므로 결국 flags & 1이 되어 WRITE가 아니라 READ의 비트를 검사하게 됩니다. 마스크 검사는 (flags & WRITE) != 0처럼 비트 연산을 괄호로 먼저 묶어 씁니다.

더 깊이: 비트를 세고 돌리는 표준 함수

C++20의 <bit> 헤더는 시프트와 마스크로 직접 만들던 비트 계산 가운데 자주 쓰는 것을 함수로 제공합니다. std::popcount(x)는 켜진 비트의 개수를 세고, std::countl_zero(x)와 std::countr_zero(x)는 가장 높은 자리와 가장 낮은 자리에서부터 이어지는 0 비트의 개수를 셉니다. std::rotl(x, n)과 std::rotr(x, n)은 밀려난 비트를 반대쪽으로 다시 넣는 회전(rotation)을 합니다. 회전을 (x << n) | (x >> (32 - n))처럼 시프트로 직접 쓰면, n이 0일 때 32자리 시프트가 되어 앞에서 본 시프트 횟수의 조건을 어기게 됩니다.

2의 거듭제곱을 다루는 함수도 있습니다. std::has_single_bit(x)는 x가 2의 거듭제곱인지 알려 주고, std::bit_width(x)는 x를 나타내는 데 필요한 최소 비트 수를 돌려줍니다. std::bit_ceil(x)와 std::bit_floor(x)는 x 이상인 가장 작은 2의 거듭제곱과 x 이하인 가장 큰 2의 거듭제곱입니다. 예를 들어 5는 101이므로 std::bit_width(5u)는 3이고, std::bit_ceil(5u)는 8입니다.

이 함수들은 unsigned char부터 unsigned long long까지와 구현이 더한 확장 부호 없는 정수 타입처럼 부호 없는 정수 타입만 받으며, int를 넘기면 오버로드 후보에서 빠져 컴파일되지 않습니다. 이 글에서 비트 패턴을 부호 없는 타입으로 다룬 것과 같은 방향입니다. 바이트의 순서를 뒤집는 std::byteswap은 C++23에 더해졌으며, 함수 목록은 cppreference의 <bit> 헤더에서 볼 수 있습니다.

정리

  • 비트 연산자는 정수의 같은 자리 비트끼리 계산하며, &는 AND, |는 OR, ^는 XOR, ~는 모든 비트를 뒤집습니다.
  • ~와 시프트는 작은 정수를 int로 승격한 뒤 계산하므로, 원래의 폭으로 보려면 다시 그 타입으로 바꿉니다.
  • 비트 패턴은 std::uint32_t 같은 폭이 정해진 부호 없는 타입으로 다룹니다.
  • 시프트의 횟수는 0 이상이고 결과 타입의 비트 수보다 작아야 하며, C++20에서 음수의 오른쪽 시프트는 음의 무한대 쪽으로 내립니다.
  • 마스크로 |=는 비트를 켜고, &= ~는 끄고, ^=는 뒤집으며, 여러 비트의 검사는 하나라도 켜졌는지와 모두 켜졌는지를 구별합니다.
  • 같음 비교가 비트 AND보다 먼저 묶이므로, 마스크 검사는 (flags & mask) != 0처럼 괄호로 감쌉니다.

연습 문제

1. 한 번 더 토글하면

flags.cpp의 마지막 토글 뒤에 flags ^= DEBUG;를 한 번 더 실행하면 마지막 줄은 어떻게 출력되나요?

해설 보기

DEBUG의 자리를 다시 뒤집어 0010이 0110인 6이 됩니다. READ는 여전히 꺼져 있으므로 하나라도 켜졌는지는 true, 모두 켜졌는지는 false여서 6 true false가 출력됩니다.

2. 비트 하나의 검사

flags가 WRITE이고 마스크도 WRITE 하나일 때, 하나라도 켜졌는지와 모두 켜졌는지의 검사 결과는 각각 무엇인가요?

해설 보기

마스크가 비트 하나이면 두 검사가 같은 자리를 묻게 되므로 둘 다 true입니다. 두 검사가 갈리는 것은 마스크에 비트가 둘 이상 있을 때뿐이므로, 비트 하나로 확인한 결과를 여러 비트의 마스크에 그대로 적용하지 않습니다.

3. 괄호를 빠뜨린 검사

flags가 READ, 즉 1일 때 flags & WRITE != 0과 (flags & WRITE) != 0은 각각 무엇인가요?

해설 보기

flags & WRITE != 0은 flags & (WRITE != 0), 즉 1 & 1로 묶여 1이 되므로 참으로 판단됩니다. (flags & WRITE) != 0은 1 & 2가 0이어서 거짓입니다. WRITE가 꺼져 있는데도 앞의 식은 켜져 있다고 잘못 판단하므로, 괄호로 비트 연산을 먼저 묶어야 합니다.

다음 글 기타 연산자에서는 대입과 복합 대입, 조건 연산자, 쉼표 연산자, sizeof처럼 앞에서 다루지 않은 연산자를 살펴봅니다.