
비트 연산이란?
비트연산은 그렇게 자주 사용되지는 않지만 특정상황에서는 매우 유용하다.
대표적으로 게임서버에서 ID를 생성할 때 정보를 압축해서 저장하거나,
특정 숫자의 개별 비트를 조작할때 사용된다.
기본 비트 연산자
1. Bitwise Not(~)
모든 비트를 뒤집는 연산이다. 0은 1로, 1은 0으로 변환된다.
int a = 5; // 0101
int result = ~a; // 1010 (음수로 표현됨)
2. Bitwise And(&)
두 비트가 모두 1일 때만 1을 반환한다.
int a = 5; // 0101
int b = 3; // 0011
int result = a & b; // 0001 = 1
3. Bitwise or(|)
두 비트 중 하나라도 1이면 1을 반환한다.
int a = 5; // 0101
int b = 3; // 0011
int result = a | b; // 0111 = 7
4. Bitwise xor(^)
두 비트가 다르면 1, 같으면 0을 반환한다.(청개구리 같은...)
int a = 5; // 0101
int b = 3; // 0011
int result = a ^ b; // 0110 = 6
// XOR의 특성: 같은 값으로 두 번 XOR하면 원래 값이 됨
int original = 123;
int key = 456;
int encrypted = original ^ key;
int decrypted = encrypted ^ key; // original과 같음
비트 시프트 연산
좌측 시프트 연산(<<)
비트를 왼쪽으로 이동시킨다.
왼쪽으로 넘어가는 비트는 버려지고, 오른쪽에는 0이 채워진다.
int a = 5; // 0101
int result = a << 2; // 010100 = 20
// 일반적으로 2의 거듭제곱 곱셈과 같은 효과
// a << n은 a * (2^n)과 같음
좌측 시프트 연산(>>)
비트를 오른쪽으로 이동시킨다.
부호 있는 정수의 경우 부호 비트가 유지된다.
int a = 20; // 010100
int result = a >> 2; // 0101 = 5
// 일반적으로 2의 거듭제곱 나눗셈과 같은 효과
// a >> n은 a / (2^n)과 같음 (양수의 경우)
주의사항이 있다면 부호있는 정수를 우측 시프트 연산으로 이동시킬 경우
부호비트가 영향을 줄 수 있으므로, 비트플래그를 사용할 때는 unsigned 타입을 사용하는게 안전하다.
(솔직히 시프트 연산을 할 변수들은 unsigned 로 하는게 그냥 정신건강에 이로움.)
Bitflag(비트플래그)에 대해서
비트 플래그는 개별 비트를 플래그(깃발)로 사용해 여러개의 불린 상태를 하나의 정수형 변수에 저장하는 것.
각 비트 위치가 특정 의미를 가지고 있고, 1이면 해당 상태가 활성화 된 상태, 0이라면 비활성화 상태이다.
근데 왜 Bitflag를 사용함?
- 메모리 효율성이 미쳤다.
// 기존 방식 (32바이트 사용)
bool isInvincible; // 1바이트
bool isStunned; // 1바이트
bool isPolymorphed; // 1바이트
bool isFlying; // 1바이트
// ... 더 많은 상태들
// 비트플래그 방식 (1바이트로 8개 상태 저장 가능)
unsigned char states; // 1바이트로 8개 상태 관리
성능상으로 메모리 접근 횟수를 감소 시키고, 캐시 효율성이 향상 된다.
상태 복사시에는 하나의 변수만 복사해서 해당 변수의 비트단위 활성화(1)여부만 확인 해서
boolan 타입의 역할을 대신 하기 때문.
상세 구현 방법
// 상태 정의 (비트 위치)
enum CharacterState {
STATE_FLYING = 0, // 0번 비트
STATE_STUNNED = 1, // 1번 비트
STATE_POLYMORPHED = 2, // 2번 비트
STATE_INVINCIBLE = 3, // 3번 비트
STATE_BURNING = 4, // 4번 비트
STATE_FROZEN = 5, // 5번 비트
STATE_POISONED = 6, // 6번 비트
STATE_BLESSED = 7 // 7번 비트
};
unsigned char characterStates = 0; // 00000000
초기화는 이렇게 진행한다.
// 개별 상태 설정
characterStates |= (1 << STATE_INVINCIBLE);
// 결과: 00001000 (3번 비트가 1)
// 여러 상태 동시 설정
characterStates |= (1 << STATE_FLYING) | (1 << STATE_BLESSED);
// 결과: 10001001 (0번, 3번, 7번 비트가 1)
// 실제 비트 변화 과정
// 초기값: 00000000
// 무적 설정: 00001000
// 비행+축복: 10001001
이후 이렇게 상태설정을 하기 위해 해당 unsigend char 변수의 비트들을 켜주고,
// 개별 상태 제거
characterStates &= ~(1 << STATE_INVINCIBLE);
// 과정 설명:
// 1. (1 << STATE_INVINCIBLE) = 00001000
// 2. ~(1 << STATE_INVINCIBLE) = 11110111
// 3. characterStates & 11110111 = 3번 비트만 0으로 만듦
// 여러 상태 동시 제거
unsigned char removeStates = (1 << STATE_FLYING) | (1 << STATE_STUNNED);
characterStates &= ~removeStates;
위에처럼 상태를 제거, 즉 비트를 끄고 모두 관리가 가능하다.
근데 약간 궁금한 점이 생긴다면 여기서 비트 연산자들은 어떻게 사용되고 있는 걸까?
복합 대입 연산자 (Compound Assignment Operators)
비트 연산에서 자주 사용되는 복합 대입 연산자들을 살펴보면,
|= (OR 후 대입)
// 기본형
characterStates = characterStates | (1 << STATE_INVINCIBLE);
// 축약형 (동일한 결과)
characterStates |= (1 << STATE_INVINCIBLE);
// 의미: 기존 값에 새로운 비트를 추가 (비트 켜기)
// 과정:
// characterStates: 00000000
// (1 << 3): 00001000
// OR 연산 결과: 00001000
// characterStates에 대입: 00001000
&= (AND 후 대입)
// 기본형
characterStates = characterStates & ~(1 << STATE_INVINCIBLE);
// 축약형 (동일한 결과)
characterStates &= ~(1 << STATE_INVINCIBLE);
// 의미: 특정 비트를 제거 (비트 끄기)
// 과정:
// characterStates: 10001001
// (1 << 3): 00001000
// ~(1 << 3): 11110111
// AND 연산 결과: 10000001
// characterStates에 대입: 10000001
^= (XOR 후 대입)
// 기본형
characterStates = characterStates ^ (1 << STATE_INVINCIBLE);
// 축약형 (동일한 결과)
characterStates ^= (1 << STATE_INVINCIBLE);
// 의미: 비트 토글 (켜져있으면 끄고, 꺼져있으면 켜기)
// 과정:
// characterStates: 00001000 (무적 상태)
// (1 << 3): 00001000
// XOR 연산 결과: 00000000 (무적 해제)
// characterStates에 대입: 00000000
상태 설정하기 (비트 켜기)
// 개별 상태 설정
characterStates |= (1 << STATE_INVINCIBLE);
// 결과: 00001000 (3번 비트가 1)
// 여러 상태 동시 설정
characterStates |= (1 << STATE_FLYING) | (1 << STATE_BLESSED);
// 결과: 10001001 (0번, 3번, 7번 비트가 1)
// 실제 비트 변화 과정
// 초기값: 00000000
// 무적 설정: 00001000
// 비행+축복: 10001001
왜 복합 대입 연산자를 사용하는가?
1. 코드 간결성
// 길고 반복적인 코드
characterStates = characterStates | (1 << STATE_INVINCIBLE);
// 간결하고 읽기 쉬운 코드
characterStates |= (1 << STATE_INVINCIBLE);
2. 성능상 이점
// 변수를 두 번 참조 (메모리 접근 2회)
arr[complexIndex] = arr[complexIndex] | newFlag;
// 변수를 한 번만 참조 (메모리 접근 1회)
arr[complexIndex] |= newFlag;
3. 실수 방지
// 변수명이 길 때 오타 가능성
someVeryLongVariableName = someVeryLongVariableName | flag; // 오타 위험
// 오타 가능성 감소
someVeryLongVariableName |= flag;
각 연산자의 사용 시나리오
|= 사용 시나리오 (비트 추가)
// 버프 추가
playerBuffs |= BUFF_STRENGTH;
playerBuffs |= BUFF_SPEED | BUFF_MAGIC_SHIELD;
// 권한 부여
userPermissions |= PERM_READ | PERM_WRITE;
// 플래그 활성화
systemFlags |= FLAG_DEBUG_MODE;
&= 사용 시나리오 (비트 제거)
// 디버프 제거
playerDebuffs &= ~DEBUFF_POISON;
playerDebuffs &= ~(DEBUFF_SLOW | DEBUFF_WEAKNESS);
// 권한 제거
userPermissions &= ~PERM_DELETE;
// 특정 비트만 유지 (마스킹)
value &= 0x0F; // 하위 4비트만 유지
^= 사용 시나리오 (비트 토글)
// 상태 토글
gameSettings ^= SETTING_SOUND_ENABLED;
gameSettings ^= SETTING_FULLSCREEN;
// 암호화/복호화
data ^= encryptionKey;
// 스위치 동작
lightStates ^= (1 << lightNumber);
즉 정리를 하자면,
비트 추가시 |= / 비트 제거시 &= / 비트 토글(상태 확인)시 ^=를 사용하여 구현가능하다.
실무 예시: 게임 설정 관리
enum GameSetting {
SETTING_SOUND = 0,
SETTING_MUSIC = 1,
SETTING_FULLSCREEN = 2,
SETTING_VSYNC = 3,
SETTING_SUBTITLES = 4
};
class GameConfig {
private:
unsigned int settings;
public:
// 설정 활성화
void enableSetting(GameSetting setting) {
settings |= (1 << setting);
}
// 설정 비활성화
void disableSetting(GameSetting setting) {
settings &= ~(1 << setting);
}
// 설정 토글
void toggleSetting(GameSetting setting) {
settings ^= (1 << setting);
}
// 여러 설정 한번에 적용
void applyProfile(const std::string& profile) {
if (profile == "performance") {
// 성능 중심: 사운드만 켜고 나머지 끄기
settings = (1 << SETTING_SOUND);
} else if (profile == "quality") {
// 품질 중심: 모든 설정 켜기
settings |= (1 << SETTING_SOUND) | (1 << SETTING_MUSIC) |
(1 << SETTING_FULLSCREEN) | (1 << SETTING_VSYNC) |
(1 << SETTING_SUBTITLES);
}
}
// 설정 상태 확인
bool isEnabled(GameSetting setting) const {
return (settings & (1 << setting)) != 0;
}
};
// 사용 예시
GameConfig config;
// 개별 설정
config.enableSetting(SETTING_SOUND); // |= 사용
config.disableSetting(SETTING_MUSIC); // &= 사용
config.toggleSetting(SETTING_FULLSCREEN); // ^= 사용
// 복합 설정
config.applyProfile("quality");
상태 제거하기 (비트 끄기)
// 개별 상태 제거
characterStates &= ~(1 << STATE_INVINCIBLE);
// 과정 설명:
// 1. (1 << STATE_INVINCIBLE) = 00001000
// 2. ~(1 << STATE_INVINCIBLE) = 11110111
// 3. characterStates & 11110111 = 3번 비트만 0으로 만듦
// 여러 상태 동시 제거
unsigned char removeStates = (1 << STATE_FLYING) | (1 << STATE_STUNNED);
characterStates &= ~removeStates;
상태 토글하기
// 상태 반전 (있으면 제거, 없으면 추가)
characterStates ^= (1 << STATE_BURNING);
// XOR 특성을 이용:
// 0 ^ 1 = 1 (비트 켜짐)
// 1 ^ 1 = 0 (비트 꺼짐)
Bitmask(비트 마스크) 이해
비트 마스크는 특정 비트들을 선택적으로 조작하거나 확인하기 위해서 사용하는 비트 패턴이다.
위에서는 상태를 확인하거나 조작하려면 복합 대입 비트연산자를 사용했지만
비트 마스크를 통해서 좀 더 간편하게 관리가 가능하다.
일단 종류가 여러개 있다.
1. 단일 비트 마스크
// 특정 위치의 비트만 확인
unsigned char INVINCIBLE_MASK = (1 << STATE_INVINCIBLE); // 00001000
bool isInvincible = (characterStates & INVINCIBLE_MASK) != 0;
2. 다중 비트 마스크
// 여러 상태를 동시에 확인하는 마스크
unsigned char DEBUFF_MASK = (1 << STATE_STUNNED) |
(1 << STATE_FROZEN) |
(1 << STATE_POISONED);
// 결과: 01100010
// 디버프 상태가 하나라도 있는지 확인
bool hasDebuff = (characterStates & DEBUFF_MASK) != 0;
// 모든 디버프가 있는지 확인
bool hasAllDebuffs = (characterStates & DEBUFF_MASK) == DEBUFF_MASK;
3. 반전 마스크 (제거용)
// 특정 비트들을 0으로 만들기 위한 마스크
unsigned char CLEAR_DEBUFFS_MASK = ~DEBUFF_MASK;
// DEBUFF_MASK가 01100010이면
// CLEAR_DEBUFFS_MASK는 10011101
characterStates &= CLEAR_DEBUFFS_MASK; // 디버프 전부 제거
고급 비트마스크 패턴
1. 범위 마스크
// 특정 범위의 비트들을 조작
// 예: 하위 4비트만 추출
unsigned char LOWER_4_BITS = 0x0F; // 00001111
unsigned char lower = characterStates & LOWER_4_BITS;
// 상위 4비트만 추출
unsigned char UPPER_4_BITS = 0xF0; // 11110000
unsigned char upper = characterStates & UPPER_4_BITS;
2. 조건부 마스크
// 조건에 따라 다른 마스크 적용
unsigned char getMask(bool isWarrior) {
if (isWarrior) {
// 전사는 마법 상태 제외
return ~((1 << STATE_POLYMORPHED) | (1 << STATE_BLESSED));
} else {
// 마법사는 물리 상태 제외
return ~((1 << STATE_BURNING) | (1 << STATE_FROZEN));
}
}
실무에서의 활용 예시
1. 권한 관리 시스템
enum Permission {
PERM_READ = 0, // 읽기 권한
PERM_WRITE = 1, // 쓰기 권한
PERM_DELETE = 2, // 삭제 권한
PERM_ADMIN = 3 // 관리자 권한
};
class User {
private:
unsigned int permissions;
public:
// 권한 부여
void grantPermission(Permission perm) {
permissions |= (1 << perm);
}
// 권한 제거
void revokePermission(Permission perm) {
permissions &= ~(1 << perm);
}
// 권한 확인
bool hasPermission(Permission perm) const {
return (permissions & (1 << perm)) != 0;
}
// 여러 권한 동시 확인
bool hasAllPermissions(unsigned int requiredPerms) const {
return (permissions & requiredPerms) == requiredPerms;
}
// 사용자 타입별 기본 권한 설정
void setUserType(const std::string& type) {
if (type == "admin") {
permissions = 0xFF; // 모든 권한
} else if (type == "editor") {
permissions = (1 << PERM_READ) | (1 << PERM_WRITE);
} else {
permissions = (1 << PERM_READ); // 읽기만
}
}
};
2. 게임 AI 상태 머신
enum AIState {
AI_PATROL = 0,
AI_CHASE = 1,
AI_ATTACK = 2,
AI_FLEE = 3,
AI_DEAD = 4
};
enum AICondition {
COND_PLAYER_VISIBLE = 0,
COND_LOW_HEALTH = 1,
COND_HAS_WEAPON = 2,
COND_NEAR_ALLY = 3
};
class GameAI {
private:
unsigned char currentState;
unsigned char conditions;
public:
void updateConditions() {
// 조건들을 실시간으로 업데이트
if (playerInSight()) {
conditions |= (1 << COND_PLAYER_VISIBLE);
} else {
conditions &= ~(1 << COND_PLAYER_VISIBLE);
}
if (health < 30) {
conditions |= (1 << COND_LOW_HEALTH);
} else {
conditions &= ~(1 << COND_LOW_HEALTH);
}
}
void updateState() {
// 조건에 따른 상태 전환
unsigned char fleeConditions = (1 << COND_PLAYER_VISIBLE) | (1 << COND_LOW_HEALTH);
unsigned char attackConditions = (1 << COND_PLAYER_VISIBLE) | (1 << COND_HAS_WEAPON);
if ((conditions & fleeConditions) == fleeConditions) {
currentState = (1 << AI_FLEE);
} else if ((conditions & attackConditions) == attackConditions) {
currentState = (1 << AI_ATTACK);
} else if (conditions & (1 << COND_PLAYER_VISIBLE)) {
currentState = (1 << AI_CHASE);
} else {
currentState = (1 << AI_PATROL);
}
}
};
비트마스크 디버깅 팁
// 비트 상태를 문자열로 출력하는 유틸리티 함수
std::string toBinaryString(unsigned char value) {
std::string result;
for (int i = 7; i >= 0; i--) {
result += (value & (1 << i)) ? '1' : '0';
}
return result;
}
// 현재 활성화된 상태들을 출력
void printActiveStates(unsigned char states) {
std::cout << "Current states (" << toBinaryString(states) << "): ";
const char* stateNames[] = {
"Flying", "Stunned", "Polymorphed", "Invincible",
"Burning", "Frozen", "Poisoned", "Blessed"
};
for (int i = 0; i < 8; i++) {
if (states & (1 << i)) {
std::cout << stateNames[i] << " ";
}
}
std::cout << std::endl;
}
성능 최적화 팁
// 자주 사용되는 마스크는 미리 정의
const unsigned char MOVEMENT_IMPAIRING = (1 << STATE_STUNNED) | (1 << STATE_FROZEN);
const unsigned char MAGIC_IMMUNITY = (1 << STATE_INVINCIBLE) | (1 << STATE_BLESSED);
const unsigned char DAMAGE_OVER_TIME = (1 << STATE_BURNING) | (1 << STATE_POISONED);
// 빠른 상태 확인 함수들
inline bool canMove(unsigned char states) {
return !(states & MOVEMENT_IMPAIRING);
}
inline bool isImmuneToMagic(unsigned char states) {
return states & MAGIC_IMMUNITY;
}
inline bool hasDOT(unsigned char states) {
return states & DAMAGE_OVER_TIME;
}
enum을 활용한 개선된 비트플래그
enum CharacterState {
STATE_AIR = 0,
STATE_STUN = 1,
STATE_POLYMORPH = 2,
STATE_INVINCIBLE = 3,
STATE_COUNT
};
class Character {
private:
unsigned int stateFlags;
public:
void setState(CharacterState state) {
stateFlags |= (1 << state);
}
void clearState(CharacterState state) {
stateFlags &= ~(1 << state);
}
bool hasState(CharacterState state) const {
return (stateFlags & (1 << state)) != 0;
}
void toggleState(CharacterState state) {
stateFlags ^= (1 << state);
}
};
마무리
비트 연산은 메모리 효율성과 성능이 중요한 상황에서 매우 유용하다.
특히 게임 개발 외에도 시스템 프로그래밍이나 임베디드 프로그래밍 등 메모리 자원이 상당히 빡빡하게 있는 경우
최적화를 목적으로 사용하기에 적합한 도구이다.
솔직히 처음 나도 봤을때는 많이 복잡하긴 했는데 해당 부분을 익히면 적어도
컴퓨터의 기본 메모리 활용법 정도는 익힐 수 있기 때문에 꼭 배우길 추천한다.
'Game DevTip > C++' 카테고리의 다른 글
| 11. C++ 참조자에 대하여 (0) | 2025.07.15 |
|---|---|
| 10. C++ 포인터 기초 : 메모리 주소의 이해 (1) | 2025.07.15 |
| 8. C++로 GameObjectPool을 구현 해보자. (1) | 2025.05.21 |
| 7. C++ friend, 동적메모리 관리, 클래스 상속에 대해서 알아보자. (0) | 2025.05.21 |
| 6. C++ string 타입에 대해서 (0) | 2024.12.05 |
댓글