LearnCPP - O

Justin·2026년 2월 14일

LearnCPP.com

목록 보기
6/22

std::bitset을 활용한 비트 조작(Bit manipulation)

  • 불리언 타입은 오직 이나 거짓, 이 두 가지 상태만 가지잖아요? 이 상태를 저장하는 데는 단 1비트면 충분하거든요.
  • 그런데 변수가 최소 1바이트(8비트) 크기를 가져야 한다면, 불리언 값 하나를 저장하기 위해 1비트만 쓰고 나머지 7비트는 사용하지 않고 버려둔다는 뜻이 됩니다.
  • 대부분의 상황에서는 크게 신경 쓰지 않아도 괜찮아요.
  • 버려지는 7비트를 아끼는 것보다 코드를 이해하고 유지 보수하기 쉽게 만드는 게 더 중요하니까요.
  • 하지만 저장 공간이 매우 중요한 일부 상황에서는, 8개의 개별 불리언 값을 하나의 바이트 안에 꽉꽉 눌러 담아 저장 효율을 높이는 것이 아주 유용할 수 있어요.
  • 이렇게 하려면 객체를 비트 수준에서 다룰 수 있어야 하는데요.
  • 다행히 C++는 우리에게 딱 맞는 도구들을 제공해 준답니다!
  • 이렇게 객체 내의 개별 비트를 수정하는 것을 비트 조작(Bit manipulation)이라고 불러요.
  • 비트 조작그래픽 암호화 데이터 압축 최적화 같은 특정 프로그래밍 분야에서 아주 많이 쓰이지만, 일반적인 프로그래밍에서는 자주 쓰이지는 않아요.
  • 그래서 이 챕터 전체는 선택 사항이랍니다.
  • 가볍게 훑어보시거나 건너뛴 다음, 나중에 필요할 때 다시 돌아와서 읽으셔도 전혀 문제없어요!

비트 플래그(Bit flags)

  • 지금까지 우리는 변수 하나에 하나의 값만 저장해 왔어요.
int foo { 5 };
  • 하지만 객체가 하나의 값만 가진다고 생각하는 대신, 객체 안의 각 비트 하나하나를 독립적인 불리언 값으로 취급할 수도 있어요.
  • 이렇게 개별 비트들이 불리언 값처럼 사용될 때, 이 비트들을 비트 플래그(Bit flags)라고 부릅니다.
  • 비트 플래그들의 집합을 정의하기 위해, 우리는 보통 필요한 플래그 개수에 맞는 적절한 크기의 부호 없는 정수std::bitset 을 사용한답니다.

비트 번호 매기기와 비트 위치(Bit positions)

  • 일련의 비트들이 있을 때, 오른쪽에서 왼쪽으로 번호를 매깁니다.
  • 이때 1이 아니라 0부터 시작합니다.
  • 각 숫자는 비트 위치를 나타내요.
76543210 비트 위치 (Bit position)
00000101 비트 시퀀스 (Bit sequence)
  • 위의 0000 0101이라는 비트 시퀀스를 보면, 위치 0위치 2에 있는 비트가 1의 값을 가지고 있고, 나머지 비트들은 모두 0의 값을 가지고 있음을 알 수 있어요.

std::bitset으로 비트 조작하기

  • std::bitset은 비트 조작에 매우 유용한 4가지 핵심 멤버 함수를 제공해요.
  • test() 특정 비트가 0인지 1인지 알아볼 때 사용해요.
  • set() 특정 비트를 켜고 싶을(1로 만들고 싶을) 때 사용해요. (이미 켜져 있다면 아무 일도 일어나지 않아요.)
  • reset() 특정 비트를 끄고 싶을(0으로 만들고 싶을) 때 사용해요. (이미 꺼져 있다면 아무 일도 일어나지 않아요.)
  • flip() 비트 값을 0에서 1로, 또는 1에서 0으로 반대로 뒤집고 싶을 때 사용해요.
  • 이 함수들은 모두 우리가 조작하고 싶은 비트의 위치 하나를 유일한 인수로 받습니다.
#include <bitset>
#include <iostream>

int main() {
    std::bitset<8> bits{ 0b0000'0101 }; // 8비트가 필요하고, 초기 비트 패턴은 0000 0101로 시작해요.
    bits.set(3);   // 위치 3의 비트를 1로 설정해요 (이제 0000 1101이 됩니다)
    bits.flip(4);  // 위치 4의 비트를 뒤집어요 (이제 0001 1101이 됩니다)
    bits.reset(4); // 위치 4의 비트를 다시 0으로 꺼요 (이제 0000 1101이 됩니다)

    std::cout << "모든 비트: " << bits << '\n';
    std::cout << "비트 3의 값: " << bits.test(3) << '\n';
    std::cout << "비트 4의 값: " << bits.test(4) << '\n';

    return 0;
}
  • 이 코드를 실행하면 이렇게 출력됩니다.
모든 비트: 00001101
비트 3의 값: 1
비트 4의 값: 0
  • 비트들에게 의미 있는 이름을 붙여주면 코드를 훨씬 더 읽기 쉽게 만들 수 있어요.
#include <bitset>
#include <iostream>

int main(){
    [[maybe_unused]] constexpr int  isHungry   { 0 };
    [[maybe_unused]] constexpr int  isSad      { 1 };
    [[maybe_unused]] constexpr int  isMad      { 2 };
    [[maybe_unused]] constexpr int  isHappy    { 3 };
    [[maybe_unused]] constexpr int  isLaughing { 4 };
    [[maybe_unused]] constexpr int  isAsleep   { 5 };
    [[maybe_unused]] constexpr int  isDead     { 6 };
    [[maybe_unused]] constexpr int  isCrying   { 7 };

    std::bitset<8> me{ 0b0000'0101 }; // 8비트가 필요하고, 초기 비트 패턴은 0000 0101이에요.
    me.set(isHappy);      // 위치 3(isHappy)의 비트를 1로 설정해요 (이제 0000 1101이 됩니다)
    me.flip(isLaughing);  // 위치 4(isLaughing)의 비트를 뒤집어요 (이제 0001 1101이 됩니다)
    me.reset(isLaughing); // 위치 4(isLaughing)의 비트를 다시 0으로 꺼요 (이제 0000 1101이 됩니다)

    std::cout << "모든 비트: " << me << '\n';
    std::cout << "나는 행복한가?: " << me.test(isHappy) << '\n';
    std::cout << "나는 웃고 있는가?: " << me.test(isLaughing) << '\n';
    return 0;
}

한 번에 여러 비트를 가져오거나 설정하고 싶다면요?

  • 안타깝게도 std::bitset으로는 한 번에 여러 비트를 조작하는 게 조금 번거로워요.
  • 이런 작업을 하고 싶거나 std::bitset 대신 부호 없는 정수를 직접 비트 플래그로 사용하고 싶다면, 조금 더 전통적인 방식을 사용해야 합니다.

std::bitset의 크기에 대한 비밀

  • std::bitset은 메모리를 아끼기보다는 속도를 최적화하는데 초점이 맞춰져 있답니다.
  • std::bitset의 크기는 비트들을 담는 데 필요한 바이트 수를 sizeof(size_t) 단위로 올림하여 결정돼요.
  • size_t는 32비트 컴퓨터에서는 4바이트, 64비트 컴퓨터에서는 8바이트예요.
  • 따라서 std::bitset<8>은 기술적으로는 8개의 비트를 저장하기 위해 단 1바이트만 있으면 되지만, 실제로는 시스템에 따라 4바이트나 8바이트의 메모리를 차지하게 됩니다.
  • 즉, std::bitset은 메모리 절약이 목적일 때보다는 편리함이 필요할 때 사용하는 것이 가장 좋습니다.

std::bitset에 질문 던지기

  • 앞서 배운 4가지 외에도 자주 쓰이는 유용한 멤버 함수들이 몇 가지 더 있어요.
  • size() 비트셋 안에 총 몇 개의 비트가 있는지 개수를 알려줘요.
  • count() 1으로 설정된 비트가 몇 개인지 세어줘요.
  • all() 모든 비트가 참인지 확인해서 불리언 값으로 알려줘요.
  • any() 참인 비트가 하나라도 있는지 확인해서 불리언 값으로 알려줘요.
  • none() 참인 비트가 단 하나도 없는지 확인해서 불리언 값으로 알려줘요.

비트 단위 연산자란?

  • C++은 6가지의 비트 조작 연산자를 제공하는데, 이를 흔히 비트 단위 연산자(bitwise operators)라고 불러요.
  • 비트 연산자는 부호 없는 정수 또는 std::bitset과 함께 사용하세요.
연산자기호형식연산 결과 설명
왼쪽 시프트 (Left shift)<<x << nx의 비트들을 왼쪽으로 n칸 이동. 새로 생기는 빈칸은 0으로 채움.
오른쪽 시프트 (Right shift)>>x >> nx의 비트들을 오른쪽으로 n칸 이동. 새로 생기는 빈칸은 0으로 채움.
비트 부정 (Bitwise NOT)~~xx의 모든 비트를 뒤집음 (0→1, 1→0).
비트 AND (Bitwise AND)&x & yx와 y의 대응 비트가 둘 다 1일 때만 1.
비트 OR (Bitwise OR)|x | yx와 y의 대응 비트 중 하나라도 1이면 1.
비트 XOR (Bitwise XOR)^x ^ yx와 y의 대응 비트가 서로 다를 때 1.

비트 왼쪽 시프트(<<)와 오른쪽 시프트(>>) 연산자

  • 비트 왼쪽 시프트 << 연산자는 비트들을 왼쪽으로 이동시켜요.
  • 예를 들어 x << 2라고 쓰면, "x의 비트들을 왼쪽으로 2칸 이동시킨 값을 만들어라"라는 뜻이죠.
  • 이때 원래 변수의 값은 건드리지 않고, 비트들을 왼쪽으로 밀어낼 때 오른쪽에 생기는 빈자리에는 무조건 0을 채워 넣습니다.
  • 0011이라는 비트를 왼쪽으로 이동시키는 예시를 볼까요?
0011 << 10110
0011 << 21100
0011 << 31000
  • 세 번째 경우를 잘 보세요.
  • 끝에 있던 1이 밖으로 밀려났죠? 비트 배열의 끝을 넘어간 비트들은 영원히 사라집니다.
  • 비트 오른쪽 시프트 >> 연산자도 비슷하게 작동합니다. 방향이 오른쪽이라는 점만 달라요.

비트 부정 (Bitwise NOT)

  • 비트 부정 연산자는 개념적으로 아주 간단해요.
  • 그냥 모든 비트를 0에서 1로, 혹은 그 반대로 뒤집는 거예요.
~00111100
~0000 01001111 1011

비트 OR (Bitwise OR)

  • 비트 OR은 논리 OR 연산자와 비슷하게 작동해요.
  • 논리 OR는 둘 중 하나라도 참(true)이면 결과가 참이 되었죠?
  • 비트 OR는 전체가 아니라 각 비트 자리마다 OR 연산을 수행해요.
  • 0b0101 | 0b0110 이라는 식을 예로 들어볼게요.
0 1 0 1 OR
0 1 1 0
-------
0 1 1 1
  • 결과는 이진수 0111이 됩니다.
std::cout << (std::bitset<4>{ 0b0101 } | std::bitset<4>{ 0b0110 }) << '\n';

출력 결과: 0111
  • 여러 개를 한꺼번에 연산 할 때도 마찬가지예요. (0b0111 | 0b0011 | 0b0001)
  • 각 열에 1이 하나라도 있으면 그 열의 결과는 1이 됩니다.
0 1 1 1 OR
0 0 1 1 OR
0 0 0 1
--------
0 1 1 1

비트 AND (Bitwise AND)

  • 비트 AND도 비슷하지만, OR 대신 AND 논리를 사용해요.
  • 즉, 두 비트가 모두 1일 때만 결과가 1이 되고, 아니면 0이 됩니다.
  • 0b0101 & 0b0110을 계산해 볼까요?
0 1 0 1 AND
0 1 1 0
--------
0 1 0 0
  • 두 번째 비트만 둘 다 1이라서 결과가 1이 되었네요.
  • 여러 개를 연산할 때는, 해당 열의 비트가 모두 1이어야만 결과가 1이 됩니다. 0b0001 & 0b0011 & 0b0111
0 0 0 1 AND
0 0 1 1 AND
0 1 1 1
--------
0 0 0 1

비트 XOR (Bitwise XOR)

  • 마지막은 비트 XOR또는 배타적 논리합이라고 부르는 연산자예요.
  • 이 친구는 두 비트가 서로 다를 때 1이 되고, 같으면 0이 됩니다.
  • 쉽게 말해 "너랑 나랑 다르면 참!"이라는 거죠.
  • 0b0110 ^ 0b0011을 계산해 봅시다.
0 1 1 0 XOR
0 0 1 1
-------
0 1 0 1
  • 여러 개를 XOR 연산할 때는 어떨까요?
  • 각 열에서 1의 개수가 홀수 개이면 결과가 1이 되고, 짝수 개이면 0이 됩니다.
0 0 0 1 XOR
0 0 1 1 XOR
0 1 1 1
--------
0 1 0 1

비트 대입 연산자 (Bitwise assignment operators)

  • 산술 연산자 += -= 처럼 비트 연산자도 대입과 동시에 연산을 수행하는 단축형이 있어요.
  • 이 연산자들은 왼쪽 변수의 값을 변경합니다.
  • 예를 들어, x = x >> 1; 대신 x >>= 1; 이라고 간단히 쓸 수 있죠.
연산자기호형식연산 및 대입 설명
왼쪽 시프트 대입<<=x <<= nx를 n만큼 왼쪽 시프트하고 그 결과를 x에 저장해요.
오른쪽 시프트 대입>>=x >>= nx를 n만큼 오른쪽 시프트하고 그 결과를 x에 저장해요.
비트 AND 대입&=x &= yx와 y를 비트 AND 연산하고 결과를 x에 저장해요.
비트 OR 대입|=x |= yx와 y를 비트 OR 연산하고 결과를 x에 저장해요.
비트 XOR 대입^=x ^= yx와 y를 비트 XOR 연산하고 결과를 x에 저장해요.

비트 연산자는 작은 정수 타입을 승격(promote)시킵니다

  • int보다 작은 정수 타입(예: char short)을 비트 연산자에 사용하면, 이 값들은 자동으로 intunsigned int승격되어 계산됩니다.
  • 그리고 결과값intunsigned int로 반환되죠.
  • 가능하다면 int보다 작은 정수 타입에는 비트 시프트 연산을 피하는 것이 좋습니다.
  • 만약 꼭 해야 한다면, static_cast를 통해 결과를 원하는 타입으로 다시 변환해 주세요.

비트 마스크 (Bit masks)

  • 우리가 개별 비트를 조작하려면, 수많은 비트 중에서 '내가 건드리고 싶은 특정 비트'가 무엇인지 컴퓨터에게 알려줘야 해요.
  • 하지만 아쉽게도 비트 단위 연산자들은 "3번째 비트를 바꿔줘" 같은 위치 기반 명령을 직접 알아듣지 못합니다.
  • 대신 그들은 비트 마스크라는 도구를 사용해요.
  • 비트 마스크란, 뒤이어 올 연산에 의해 수정될 특정 비트들을 선택하기 위해 미리 정의해 둔 비트들의 집합을 말합니다.
  • 이해를 돕기 위해 페인트칠을 예로 들어볼게요.
  • 창문 틀에 페인트를 칠하려고 하는데, 유리에 페인트가 묻으면 안 되겠죠?
  • 그래서 우리는 유리에 마스킹 테이프를 붙입니다.
  • 그러면 페인트를 칠해도 테이프가 붙은 곳(유리)은 보호되고, 테이프가 없는 곳(창문 틀)만 색이 칠해지죠.
  • 비트 마스크도 똑같은 역할을 해요.
  • 우리가 원하지 않는 비트는 건드리지 않도록 막아주고, 우리가 원하는 비트에만 접근할 수 있게 해 준답니다.

C++14에서 비트 마스크 정의하기

  • 가장 기본적인 비트 마스크는 각 비트 위치(0번, 1번...)마다 하나씩 마스크를 만들어 두는 거예요.
  • 상관없는 비트는 0으로, 조작하고 싶은 비트는 1로 표시합니다.
  • 보통은 나중에 다시 쓰기 편하도록 의미 있는 이름을 붙여서 상수로 정의해 둡니다.
  • C++14부터는 이진수 리터럴을 지원하기 때문에 아주 직관적으로 만들 수 있어요.
#include <cstdint>

constexpr std::uint8_t mask0{ 0b0000'0001 }; // 0번 비트를 나타냅니다
constexpr std::uint8_t mask1{ 0b0000'0010 }; // 1번 비트를 나타냅니다
constexpr std::uint8_t mask2{ 0b0000'0100 }; // 2번 비트를 나타냅니다
constexpr std::uint8_t mask3{ 0b0000'1000 }; // 3번 비트를 나타냅니다
constexpr std::uint8_t mask4{ 0b0001'0000 }; // 4번 비트를 나타냅니다
constexpr std::uint8_t mask5{ 0b0010'0000 }; // 5번 비트를 나타냅니다
constexpr std::uint8_t mask6{ 0b0100'0000 }; // 6번 비트를 나타냅니다
constexpr std::uint8_t mask7{ 0b1000'0000 }; // 7번 비트를 나타냅니다

비트 상태 확인하기 (Testing a bit)

  • 특정 비트가 켜져 있는지(1인지) 꺼져 있는지(0인지) 알고 싶을 땐, 비트 단위 AND 연산자(&)를 사용합니다.
#include <cstdint>
#include <iostream>

int main() {
    // ... 마스크 정의 생략 (위와 동일) ...
    [[maybe_unused]] constexpr std::uint8_t mask0{ 0b0000'0001 }; 
    [[maybe_unused]] constexpr std::uint8_t mask1{ 0b0000'0010 }; 

    std::uint8_t flags{ 0b0000'0101 }; // 8개의 플래그 중 0번과 2번이 켜져 있네요.

    // 0번 비트 확인
    std::cout << "bit 0 is " << (static_cast<bool>(flags & mask0) ? "on\n" : "off\n");

    // 1번 비트 확인
    std::cout << "bit 1 is " << (static_cast<bool>(flags & mask1) ? "on\n" : "off\n");
    return 0;
}

출력 결과
bit 0 is on
bit 1 is off
  • 원리 설명: flags & mask0를 하면 두 비트가 모두 1인 자리만 1이 남습니다.
0000 0101 (flags)
0000 0001 (mask0)
--------- (AND 연산)
0000 0001 (결과: 0이 아님 -> True)
  • 반면 flags & mask1은:
0000 0101 (flags)
0000 0010 (mask1)
--------- (AND 연산)
0000 0000 (결과: 0-> False)

비트 켜기 (Setting a bit)

  • 특정 비트를 켜고 싶을 때는 비트 단위 OR 대입 연산자(|=)를 사용합니다.
// ... 마스크 정의 생략 ...

std::uint8_t flags{ 0b0000'0101 };
std::cout << "bit 1 is " << (static_cast<bool>(flags & mask1) ? "on\n" : "off\n");

flags |= mask1; // 1번 비트를 켭니다!

std::cout << "bit 1 is " << (static_cast<bool>(flags & mask1) ? "on\n" : "off\n");
  • OR 연산(|)을 사용하면 여러 비트를 한 번에 켤 수도 있습니다.
flags |= (mask4 | mask5); // 4번과 5번 비트를 동시에 켭니다.

비트 끄기 / 초기화 (Resetting a bit)

  • 특정 비트를 끄고 싶을 때는 비트 단위 AND(&)와 비트 단위 NOT(~)을 함께 사용해야 해요.
  • 조금 복잡해 보일 수 있지만 원리를 알면 간단합니다.
// ... 마스크 정의 생략 ...

std::uint8_t flags{ 0b0000'0101 };

std::cout << "bit 2 is " << (static_cast<bool>(flags & mask2) ? "on\n" : "off\n");

flags &= ~mask2; // 2번 비트를 끕니다!

std::cout << "bit 2 is " << (static_cast<bool>(flags & mask2) ? "on\n" : "off\n");
    1. mask2 0000 0100입니다.
    1. ~mask2를 하면 1111 1011이 됩니다.
    1. 이제 flags1111 1011을 AND 연산합니다.
    1. 0과 만나는 번 비트는 무조건 0`이 되고, 1과 만나는 나머지 비트는 원래 값을 유지합니다.

주의사항

  • ~mask2를 할 때 컴파일러가 경고를 낼 수 있습니다.
  • mask2는 작은 자료형인데 ~ 연산을 하면서 int로 승격되기 때문이에요.
  • 이럴 때는 다시 원래 타입으로 캐스팅해주면 됩니다.
flags &= static_cast<std::uint8_t>(~mask2);

비트 뒤집기 (Flipping a bit)

  • 비트의 상태를 반대로(0은 1로, 1은 0으로) 바꾸고 싶다면 비트 단위 XOR 연산자 ^ 를 사용합니다.
// ... 마스크 정의 생략 ...

std::uint8_t flags{ 0b0000'0101 };

std::cout << "bit 2 is " << (static_cast<bool>(flags & mask2) ? "on\n" : "off\n");

flags ^= mask2; // 2번 비트를 뒤집습니다.

std::cout << "bit 2 is " << (static_cast<bool>(flags & mask2) ? "on\n" : "off\n");

flags ^= mask2; // 다시 뒤집습니다.

std::cout << "bit 2 is " << (static_cast<bool>(flags & mask2) ? "on\n" : "off\n");

비트 마스크와 std::bitset

  • C++ 표준 라이브러리의 std::bitset을 사용해도 비트 조작이 가능합니다.
  • test() set() reset() flip() 같은 함수를 제공해서 편리하죠.
  • 하지만 여러 비트를 한꺼번에 조작하려면 여전히 비트 연산자를 사용하는 것이 좋습니다.
profile
안녕하세요.

0개의 댓글