C++ bitset | "비트 집합" 가이드
이 글의 핵심
고정 크기 비트 집합 bitset의 연산, vector<bool>과의 선택, 마스킹·부분집합 실전 패턴을 다룹니다.
bitset이란?
std::bitset 은 고정 크기 비트 집합을 표현하는 STL 컨테이너입니다. 비트 단위로 데이터를 효율적으로 저장하고 조작할 수 있으며, 플래그, 상태 관리, 비트 마스크 등에 유용합니다.
#include <bitset>
std::bitset<8> bits; // 8비트
bits.set(0); // 0번 비트 1
bits.set(3); // 3번 비트 1
bits.reset(0); // 0번 비트 0
왜 필요한가?:
- 메모리 효율: 1비트 = 1개 bool (일반 배열은 1바이트)
- 비트 연산: AND, OR, XOR, NOT 등 직관적 연산
- 타입 안전: 컴파일 타임 크기 검증
- 편의성: 비트 조작 함수 제공
// ❌ 일반 배열: 8바이트
bool flags[8] = {false};
// ✅ bitset: 1바이트
std::bitset<8> flags; // 8비트 = 1바이트
비트 인덱싱:
std::bitset<8> bits{"10101010"};
// 76543210 (인덱스)
std::cout << bits[0] << '\n'; // 0 (오른쪽)
std::cout << bits[7] << '\n'; // 1 (왼쪽)
// 주의: 문자열과 인덱스 방향이 반대!
기본 사용
#include <bitset>
// 생성
std::bitset<8> b1; // 00000000
std::bitset<8> b2{0b10101010}; // 10101010
std::bitset<8> b3{"10101010"}; // 10101010
// 접근
bool bit0 = b2[0];
bool bit7 = b2[7];
// 출력
std::cout << b2 << std::endl; // 10101010
실전 예시
예시 1: 플래그
#include <bitset>
enum Permission {
Read = 0,
Write = 1,
Execute = 2,
Delete = 3
};
class FilePermissions {
std::bitset<4> perms;
public:
void grant(Permission p) {
perms.set(p);
}
void revoke(Permission p) {
perms.reset(p);
}
bool has(Permission p) const {
return perms[p];
}
void print() const {
std::cout << "읽기: " << perms[Read] << std::endl;
std::cout << "쓰기: " << perms[Write] << std::endl;
std::cout << "실행: " << perms[Execute] << std::endl;
std::cout << "삭제: " << perms[Delete] << std::endl;
}
};
int main() {
FilePermissions fp;
fp.grant(Read);
fp.grant(Write);
if (fp.has(Read)) {
std::cout << "읽기 가능" << std::endl;
}
fp.print();
}
예시 2: 비트 연산
#include <bitset>
int main() {
std::bitset<8> b1{"11110000"};
std::bitset<8> b2{"10101010"};
// AND
auto and_result = b1 & b2;
std::cout << "AND: " << and_result << std::endl; // 10100000
// OR
auto or_result = b1 | b2;
std::cout << "OR: " << or_result << std::endl; // 11111010
// XOR
auto xor_result = b1 ^ b2;
std::cout << "XOR: " << xor_result << std::endl; // 01011010
// NOT
auto not_result = ~b1;
std::cout << "NOT: " << not_result << std::endl; // 00001111
}
예시 3: 변환
#include <bitset>
std::bitset<8> bits{"10101010"};
// 정수로
unsigned long ul = bits.to_ulong();
std::cout << "정수: " << ul << std::endl; // 170
// 문자열로
std::string str = bits.to_string();
std::cout << "문자열: " << str << std::endl; // 10101010
// 정수에서
std::bitset<8> bits2{170};
예시 4: 카운트
#include <bitset>
std::bitset<8> bits{"10101010"};
// 1의 개수
std::cout << "1의 개수: " << bits.count() << std::endl; // 4
// 크기
std::cout << "크기: " << bits.size() << std::endl; // 8
// 모두 1?
std::cout << "모두 1: " << bits.all() << std::endl; // false
// 하나라도 1?
std::cout << "하나라도 1: " << bits.any() << std::endl; // true
// 모두 0?
std::cout << "모두 0: " << bits.none() << std::endl; // false
비트 연산
std::bitset<8> bits;
// 설정
bits.set(); // 모두 1
bits.set(3); // 3번 비트 1
bits.set(3, 0); // 3번 비트 0
// 리셋
bits.reset(); // 모두 0
bits.reset(3); // 3번 비트 0
// 플립
bits.flip(); // 모두 반전
bits.flip(3); // 3번 비트 반전
// 테스트
bool bit3 = bits.test(3);
비트 연산 기초
정수나 bitset에서 자주 쓰는 연산을 정리하면 다음과 같습니다.
| 연산 | 의미 | 예시 직관 |
|---|---|---|
AND & | 둘 다 1인 위치만 1 | 마스크로 특정 비트만 추출 |
| OR (비트 OR) | 하나라도 1이면 1 | 비트 켜기 |
XOR ^ | 서로 다르면 1 | 토글, 같은지 비교 |
NOT ~ | 전부 반전 | 전체 마스크 뒤집기 |
<< / >> | 왼쪽/오른쪽 시프트 | (2^k) 배 곱·나눗셈에 해당하는 이동 |
std::bitset은 위 연산을 비트 집합 전체에 대해 오버로드되어 있어, 루프 없이 한 번에 AND/OR/XOR/NOT을 할 수 있습니다. 정수와 달리 크기가 다른 bitset 간 연산은 컴파일 타임에 크기가 같아야 합니다.
std::bitset<8> a{"11001100"};
std::bitset<8> b{"10101010"};
std::bitset<8> low4 = a & std::bitset<8>{"00001111"}; // 하위 4비트만
bitset vs vector
둘 다 비트 단위 압축을 목표로 하지만 역할이 다릅니다.
| 항목 | std::bitset<N> | std::vector<bool> |
|---|---|---|
| 크기 | 컴파일 타임 상수 N (템플릿 인자) | 런타임에 resize 가능 |
| 저장 위치 | 보통 스택 또는 객체 안에 고정 크기 | 힙에 동적 할당 |
| 연산 | AND·OR·XOR·NOT, 시프트 등 비트 연산 풍부 | 요소별 접근·일부 알고리즘 위주 |
| 표준 보장 | 완전한 비트 집합 타입 | vector의 특수화로 참조자 등 세부가 까다로움 |
언제 무엇을 쓸까
- N이 코드에서 고정(예: 플래그 64비트, IPv4 마스크 32비트)이면
bitset<N>이 타입 안전하고 연산이 명확합니다. - 사용자 입력이나 파일 크기에 따라 길이가 바뀌면
vector<bool>또는 비트 길이가 매우 크면boost::dynamic_bitset같은 대안을 검토합니다. vector<bool>은 “bool 모음”이지 이론적으로 완전한vector<T>와 동일하지 않다는 점(예:std::vector<bool>::reference)을 문서에 유의합니다.
비트 마스킹 패턴
특정 비트 켜기/끄기/토글
unsigned x = 0b1010;
unsigned mask = 1u << 3; // 3번 비트
x |= mask; // 켜기
x &= ~mask; // 끄기
x ^= mask; // 토글
bitset에서는 set(pos), reset(pos), flip(pos)로 동일한 일을 더 읽기 쉽게 할 수 있습니다.
하위 k비트만 유지: x & ((1u << k) - 1)
특정 구간 비트 추출: 시프트와 AND를 조합합니다.
std::bitset<16> v{"1111000011110000"};
auto low8 = (v & std::bitset<16>{"0000000011111111"}); // 개념적으로 하위 8비트
실전: 플래그 관리·순열·조합
플래그 조합
여러 옵션을 한 정수나 bitset에 넣어 비트 OR로 전달하는 패턴은 API 플래그에서 흔합니다.
enum class Opt : unsigned { A = 1u << 0, B = 1u << 1, C = 1u << 2 };
unsigned f = static_cast<unsigned>(Opt::A) | static_cast<unsigned>(Opt::C);
bool hasB = (f & static_cast<unsigned>(Opt::B)) != 0;
bitset으로 옵션 인덱스를 직접 쓰면 디버깅 시 이진 출력이 명확해집니다.
부분집합 열거(비트 마스크)
원소이 (0..n-1)인 집합의 모든 부분집합은 0부터 2^n - 1까지의 마스크로 순회할 수 있습니다.
#include <bitset>
#include <iostream>
int main() {
const int n = 4;
// n이 크면 (1u << n) 오버플로에 주의 (unsigned 비트 폭 이하로 사용)
for (unsigned mask = 0; mask < (1u << n); ++mask) {
std::bitset<n> bs(mask);
std::cout << bs << '\n';
}
}
(n)이 크면 2^n이 폭발하므로 백트래킹이나 다음 부분집합 최적화(예: submask = (submask - 1) & mask)를 상황에 맞게 씁니다.
조합·순열과의 관계
- 조합: 비트가 1인 위치를 “선택된 원소”로 두는 표현이 자연스럽습니다.
- 순열: 순서가 중요하므로 비트만으로는 부족하고,
next_permutation등과 함께 쓰이는 경우가 많습니다. 다만 “사용 여부”를 비트로 두는 상태 압축 DP에서는bitset/uint32_t마스크가 자주 등장합니다.
성능 최적화
- 고정 크기·핫 루프:
bitset<N>의 단일 연산은 컴파일러가 워드 단위로 최적화하기 좋습니다. N이 작으면 스택에 두고 반복 할당을 피하세요. - 카운트:
count()로 1의 개수를 한 번에 구할 수 있습니다. 특정 비트를 순회할 때는 인덱스 루프와test(i)가 표준적인 방법이며, 매우 빠른 최하위 비트 탐색이 필요하면 정수형 마스크와 컴파일러 내장 함수(예: GCC__builtin_ctzll)를 프로젝트 정책에 맞게 검토합니다. - I/O: 대량 출력 시
to_string()반복은 비용이 클 수 있습니다. 디버깅용이 아니면 정수 비트 연산으로 처리하거나 버퍼를 한 번에 쓰는 방식을 검토합니다. - vector
vs bitset : 동적 크기가 꼭 필요하지 않으면bitset이 레이아웃이 단순하고 최적화하기 쉬운 경우가 많습니다.
자주 발생하는 문제
문제 1: 크기
// 컴파일 타임 크기
std::bitset<8> bits;
// ❌ 런타임 크기
// int n = 8;
// std::bitset<n> bits; // 에러
// ✅ 런타임 크기: vector<bool>
std::vector<bool> bits(n);
문제 2: 인덱스
std::bitset<8> bits{"10101010"};
// 인덱스 0: 오른쪽 비트
std::cout << bits[0] << std::endl; // 0 (오른쪽)
std::cout << bits[7] << std::endl; // 1 (왼쪽)
문제 3: 변환
std::bitset<64> bits{0xFFFFFFFFFFFFFFFF};
// ❌ 오버플로우
// unsigned long ul = bits.to_ulong(); // 32비트 시스템에서 에러
// ✅ unsigned long long
unsigned long long ull = bits.to_ullong();
문제 4: 문자열
// 문자열 생성
std::bitset<8> bits{"10101010"};
// ❌ 잘못된 문자
// std::bitset<8> bits2{"102"}; // 예외
// ✅ 유효성 검사
try {
std::bitset<8> bits3{"10201010"};
} catch (const std::invalid_argument&) {
std::cout << "유효하지 않은 문자" << std::endl;
}
실무 패턴
패턴 1: 상태 머신
enum class State {
Idle = 0,
Running = 1,
Paused = 2,
Error = 3
};
class StateMachine {
std::bitset<4> activeStates_;
public:
void enterState(State s) {
activeStates_.set(static_cast<int>(s));
}
void exitState(State s) {
activeStates_.reset(static_cast<int>(s));
}
bool isInState(State s) const {
return activeStates_[static_cast<int>(s)];
}
bool isIdle() const {
return activeStates_.none(); // 모든 비트 0
}
};
// 사용
StateMachine sm;
sm.enterState(State::Running);
if (sm.isInState(State::Running)) {
std::cout << "실행 중\n";
}
패턴 2: 집합 연산
class CharSet {
std::bitset<256> chars_; // ASCII 문자 집합
public:
void add(char c) {
chars_.set(static_cast<unsigned char>(c));
}
bool contains(char c) const {
return chars_[static_cast<unsigned char>(c)];
}
CharSet operator&(const CharSet& other) const {
CharSet result;
result.chars_ = chars_ & other.chars_;
return result;
}
CharSet operator|(const CharSet& other) const {
CharSet result;
result.chars_ = chars_ | other.chars_;
return result;
}
size_t size() const {
return chars_.count();
}
};
// 사용
CharSet vowels;
vowels.add('a'); vowels.add('e'); vowels.add('i');
CharSet consonants;
consonants.add('b'); consonants.add('c');
auto intersection = vowels & consonants; // 교집합
auto union_set = vowels | consonants; // 합집합
패턴 3: 비트 필드 압축
class CompactData {
std::bitset<32> flags_;
public:
// 0-7: 타입 (8비트)
void setType(uint8_t type) {
for (int i = 0; i < 8; ++i) {
flags_[i] = (type >> i) & 1;
}
}
uint8_t getType() const {
uint8_t type = 0;
for (int i = 0; i < 8; ++i) {
if (flags_[i]) {
type |= (1 << i);
}
}
return type;
}
// 8-15: 상태 플래그
void setFlag(int index, bool value) {
flags_[8 + index] = value;
}
bool getFlag(int index) const {
return flags_[8 + index];
}
};
FAQ
Q1: bitset은 무엇인가요?
A: 고정 크기 비트 집합을 표현하는 STL 컨테이너입니다. 비트 단위로 데이터를 효율적으로 저장하고 조작할 수 있습니다.
Q2: bitset의 크기는 어떻게 결정되나요?
A: 컴파일 타임에 템플릿 인자로 고정됩니다. 런타임에 크기를 변경할 수 없습니다.
std::bitset<8> bits; // 8비트 고정
Q3: 어떤 비트 연산을 지원하나요?
A: AND(&), OR(|), XOR(^), NOT(~), 왼쪽 시프트(<<), 오른쪽 시프트(>>)를 지원합니다.
std::bitset<8> b1{"11110000"};
std::bitset<8> b2{"10101010"};
auto result = b1 & b2; // AND
Q4: 정수나 문자열로 변환할 수 있나요?
A: 가능합니다. to_ulong(), to_ullong(), to_string()을 사용합니다.
std::bitset<8> bits{"10101010"};
unsigned long ul = bits.to_ulong();
std::string str = bits.to_string();
Q5: vector과 차이는?
A:
- bitset: 고정 크기, 컴파일 타임 결정
- vector
: 동적 크기, 런타임 변경 가능
std::bitset<8> bits; // 고정
std::vector<bool> vec(8); // 동적
vec.resize(16); // OK
Q6: bitset의 메모리 크기는?
A: 비트 수를 8로 나눈 값(바이트 단위)입니다. 예를 들어, bitset<8>은 1바이트, bitset<32>는 4바이트입니다.
Q7: 비트 인덱스 방향은?
A: 인덱스 0은 오른쪽 비트입니다.
std::bitset<8> bits{"10101010"};
// 76543210 (인덱스)
bits[0]; // 0 (오른쪽)
bits[7]; // 1 (왼쪽)
Q8: bitset 학습 리소스는?
A:
- “C++ Primer” by Lippman, Lajoie, Moo
- “Effective STL” by Scott Meyers
- cppreference.com - std::bitset
관련 글: Bit Manipulation, Bit Fields.
한 줄 요약: std::bitset은 고정 크기 비트 집합으로, 메모리 효율적이고 비트 연산을 직관적으로 수행할 수 있습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ Algorithm Set | “집합 알고리즘” 가이드
- C++ Custom Allocator | “커스텀 할당자” 가이드
- C++ 함수 객체 | “Functor” 완벽 가이드
관련 글
- C++ Algorithm Copy |
- C++ Algorithm Count |
- C++ Algorithm Generate |
- C++ 알고리즘 |
- C++ Algorithm Heap |