함수 포인터로 동작을 전달하는 감각을 익혔다면, 이번에는 데이터를 가장 작은 단위인 비트 수준에서 조작하는 연습을 해 봅니다. 비트 연산을 이해하면 메모리와 하드웨어 레지스터를 효율적으로 다루고, 여러 상태를 하나의 정수에 압축해 표현할 수 있습니다.
이번 글에서 새로 나오는 용어
- 비트 (Bit): 0 또는 1 중 하나의 값을 표현하는 최소 단위
- 비트 마스크 (Bit Mask): 특정 비트를 골라내거나 조작하는 데 사용하는 값
- 플래그 (Flag): 상태를 나타내기 위해 켜고 끄는 비트
- 시프트 연산 (Shift): 비트를 왼쪽 또는 오른쪽으로 이동시키는 연산
핵심 개념
학습 메모
- 소요 시간: 60분 내외
- 준비물: 정수 자료형 크기, 논리 연산자 이해, 2진수 표기법 감각
- 학습 목표: 비트 연산 종류별 의미 이해, 플래그·마스크 설계, 시프트로 빠른 곱셈·나눗셈 구현
C의 비트 연산자는 &, |, ^, ~, <<, >> 여섯 가지입니다. 여기서 &는 비트 단위 AND, |는 OR, ^는 XOR, ~는 NOT, <<는 왼쪽 시프트, >>는 오른쪽 시프트입니다. 정수형에서만 사용하며, 각 비트 자리에 대해 논리를 수행합니다. 입문 단계에서는 부호 있는 정수보다 unsigned 값을 기준으로 예제를 보는 편이 더 안전합니다.
이번 글에서는 다음 순서로 살펴봅니다.
- 비트 연산자별 동작과 2진수 표기를 직접 써 가며 확인하기
- 플래그 값을 정의하고 켜기·끄기·토글하기
- 센서 상태나 권한을 한 정수에 묶어 관리하기
- 시프트로 빠른 곱셈/나눗셈과 데이터 패킹 구현하기
코드로 따라하기
기본 비트 연산
#include <stdio.h>
void print_bits(unsigned int value) {
int i;
for (i = 7; i >= 0; i--) {
unsigned int mask = 1u << i;
printf("%u", (value & mask) ? 1u : 0u);
}
printf("\n");
}
int main(void) {
unsigned int a = 0xAA; // 10101010
unsigned int b = 0xF0; // 11110000
printf("a & b = ");
print_bits(a & b);
printf("a | b = ");
print_bits(a | b);
printf("a ^ b = ");
print_bits(a ^ b);
printf("~a = ");
print_bits((unsigned char)(~a));
return 0;
}
위 예제는 C11/C17 환경에서도 바로 따라 할 수 있도록 16진수 0xAA, 0xF0를 사용했습니다. print_bits 함수는 상위 8비트만 출력하도록 만들어 비트 패턴을 눈으로 확인할 수 있게 도와줍니다. ~ 연산은 정수 승격 뒤에 수행되므로, 보고 싶은 비트 폭만 남기려면 예제처럼 캐스팅이나 마스크를 함께 사용하는 습관이 도움이 됩니다.
플래그 정의와 조작
#include <stdio.h>
enum {
FLAG_READ = 1 << 0, // 0001
FLAG_WRITE = 1 << 1, // 0010
FLAG_EXEC = 1 << 2, // 0100
FLAG_SYNC = 1 << 3 // 1000
};
void print_status(unsigned int flags) {
printf("권한: R=%d W=%d X=%d S=%d\n",
(flags & FLAG_READ) != 0,
(flags & FLAG_WRITE) != 0,
(flags & FLAG_EXEC) != 0,
(flags & FLAG_SYNC) != 0);
}
int main(void) {
unsigned int flags = 0;
flags |= FLAG_READ;
flags |= FLAG_WRITE;
print_status(flags);
flags &= ~FLAG_WRITE;
flags |= FLAG_SYNC;
print_status(flags);
flags ^= FLAG_EXEC;
print_status(flags);
return 0;
}
|=는 플래그를 켜고, &= ~는 특정 플래그를 끕니다. ^=는 토글(toggle) 동작입니다. 이런 패턴은 파일 권한, 이벤트 상태 등 다양한 곳에서 사용됩니다.
센서 상태를 비트로 묶기
#include <stdio.h>
#define SENSOR_TEMP (1u << 0)
#define SENSOR_HUMID (1u << 1)
#define SENSOR_LIGHT (1u << 2)
#define SENSOR_MOTION (1u << 3)
unsigned int read_sensors(int temperature, int humidity, int light, int motion) {
unsigned int status = 0;
if (temperature > 30) {
status |= SENSOR_TEMP;
}
if (humidity < 30) {
status |= SENSOR_HUMID;
}
if (light < 100) {
status |= SENSOR_LIGHT;
}
if (motion) {
status |= SENSOR_MOTION;
}
return status;
}
void handle_status(unsigned int status) {
if (status & SENSOR_TEMP) {
printf("온도 경고\n");
}
if (status & SENSOR_HUMID) {
printf("습도 경고\n");
}
if (status & SENSOR_LIGHT) {
printf("조도 경고\n");
}
if (status & SENSOR_MOTION) {
printf("움직임 감지\n");
}
}
int main(void) {
unsigned int status = read_sensors(32, 25, 90, 1);
handle_status(status);
return 0;
}
여러 상태를 정수 하나에 담으면 함수 인자도 간단해지고, 조건을 겹쳐 검사하기 쉬워집니다.
시프트 연산으로 빠른 계산
#include <stdio.h>
int main(void) {
unsigned int value = 5;
printf("value << 1 = %u\n", value << 1); // 5 * 2
printf("value << 3 = %u\n", value << 3); // 5 * 8
unsigned int packed = (0x0A << 4) | 0x03; // 상위 4비트 + 하위 4비트
printf("packed = 0x%X\n", packed);
unsigned int high = (packed >> 4) & 0x0F;
unsigned int low = packed & 0x0F;
printf("high=%u, low=%u\n", high, low);
return 0;
}
시프트는 단순 곱셈뿐 아니라 데이터를 여러 조각으로 나누거나 합치는 데도 쓰입니다. 임베디드 장비에서는 레지스터의 특정 비트만 수정해야 하는 경우가 많으므로 시프트와 마스크 조합이 필수입니다. 다만 부호 있는 정수를 오른쪽 시프트할 때의 결과는 환경에 따라 다르게 보일 수 있으므로, 입문 단계에서는 unsigned 값으로 먼저 연습하는 편이 안전합니다.
왜 중요한가
- 비트 연산을 이해하면 메모리 사용량을 줄이고, 상태를 압축해 전달할 수 있습니다.
- 하드웨어 레지스터, 파일 포맷, 네트워크 프로토콜을 읽을 때 각 비트 위치의 의미를 파악할 수 있습니다.
- 시프트는 특정 플랫폼에서 빠른 곱셈/나눗셈 대체 연산으로 사용됩니다.
- 플래그와 마스크 설계 능력은 시스템 프로그래밍과 임베디드에서 기본 역량입니다.
CodeSandbox로 이어서 실습하기
아래 샌드박스는 CodeSandbox의 Universal starter입니다. C는 터미널에서 직접 컴파일하고 다시 실행하는 흐름이 중요하니, 이번 글의 코드를 파일로 만들고 빌드-실행 사이클을 다시 따라가 보세요.
💬 댓글
이 글에 대한 의견을 남겨주세요