int보다 작은 정수형이 연산에 참여할 때 자동으로 int로 변환되는 규칙
(integer promotion). C의 가장 함정이 많은 부분이다.
규칙
char, short, _Bool, 비트 필드 등 int 보다 작은 타입은 연산 전에 int 로 승격된다 (int 가 모든 값을 표현할 수 있으면 int, 아니면 unsigned int)
uint8_t a = 200, b = 100;
uint8_t c = a + b; // a, b가 int로 승격 → 300 → uint8_t로 잘려 44
int d = a + b; // 300 (승격된 int 그대로)
함정 ① — 비트 반전
uint8_t x = 0x0F;
uint8_t y = ~x; // ~0x0F 는 int 연산 → 0xFFFFFFF0
// uint8_t 대입 시 0xF0 — 결과는 맞다
if (~x == 0xF0) { … } // ✗ 거짓! 0xFFFFFFF0 != 0x000000F0
비교할 때 승격된 값이 그대로 쓰이므로 예상과 다르다.
if ((uint8_t)~x == 0xF0) { … } // ✓ 명시적으로 다시 좁힌다
함정 ② — 시프트
uint8_t reg = 0x80;
uint16_t v = reg << 8; // reg가 int로 승격 → 0x8000. 의도대로
uint32_t big = 1 << 31; // ✗ 1은 int. 부호 비트로 넘어가 미정의 동작
uint32_t ok = 1UL << 31; // ✓
부호 있는 정수의 오버플로는 미정의 동작이다. 컴파일러가 "오버플로는 일어나지 않는다"고 가정하고 코드를 지워 버리는 일도 실제로 있다.
함정 ③ — 부호 있는 값과 없는 값의 비교
int i = -1;
unsigned u = 1;
if (i < u) { … } // ✗ 실행되지 않는다!
둘 다 int 크기이므로 승격이 아니라 "일반 산술 변환"이 적용되어
int가 unsigned로 변환된다. -1은 0xFFFFFFFF = 4294967295가 되어
1보다 크다.
for (unsigned i = n - 1; i >= 0; i--) { … } // ✗ 무한 루프
// unsigned는 항상 >= 0
부호 없는 타입으로 감소 루프를 만들면 안 된다는 것이 여기서 나온다.
함정 ④ — 8비트 MCU에서 특히 위험
// int 가 16비트인 MCU (AVR, 일부 8051)
uint16_t a = 50000, b = 50000;
uint32_t c = a + b; // ✗ int(16비트) 연산 → 오버플로 → 잘린 값
uint32_t d = (uint32_t)a + b; // ✓ 미리 넓힌다
int의 크기가 플랫폼마다 다르다는 점이 문제를 키운다.
32비트 MCU에서 잘 돌던 코드가 8비트로 포팅하면 깨진다.
방어 방법
① 고정 폭 타입을 쓴다 — uint8_t, int32_t (<stdint.h>)
② 연산 전에 명시적으로 캐스팅한다
③ 컴파일러 경고를 켠다
-Wall -Wextra -Wconversion -Wsign-compare
④ MISRA C 규칙을 따른다 (필수 타입 모델)
-Wconversion이 특히 유용하다 — 암묵적 축소 변환을 전부 경고해 준다.
처음 켜면 경고가 수백 개 나오지만, 그중 진짜 버그가 섞여 있다.
왜 이런 규칙이 있나
옛 CPU에서 int가 "가장 자연스러운 연산 단위" 였기 때문이다.
char 두 개를 더할 때도 어차피 레지스터로 올려 계산하므로,
언어 차원에서 그것을 규정한 것이다.
지금은 8비트 연산이 효율적인 MCU도 많아 이 규칙이 오히려 방해가 되지만, 호환성 때문에 바뀌지 않는다.
면접 답변 골격
"C는 int보다 작은 타입을 연산 전에 int로 승격합니다. 그래서
uint8_t 끼리의 ~나 비교가 예상과 다르게 동작하고, 부호 있는 값과
없는 값을 비교하면 부호 없는 쪽으로 변환되어 -1 < 1u가 거짓이 됩니다.
고정 폭 타입과 -Wconversion으로 방어합니다."