전처리기는 C 문법을 이해하지 않고 텍스트를 바꾸므로 함수처럼 믿으면 바로 위험해집니다.
- 매크로가 함수처럼 보이는데 왜 이상하게 깨지는지 확인하고 싶다
#include,#ifdef,#define의 역할을 빠르게 다시 구분하고 싶다- 디버그 빌드와 릴리스 빌드를 전처리로 어떻게 나누는지 정리하고 싶다
숏컷 코드
#include <stdio.h>
#define MAX_BUFFER 256
#ifdef DEBUG
#define LOG(msg) fprintf(stderr, "[DBG] %s\n", (msg))
#else
#define LOG(msg) ((void)0)
#endif문법
전처리기는 C 문법을 해석하는 단계가 아니라, 컴파일 전에 텍스트를 바꾸는 단계입니다.
- 타입 검사를 하지 않는다
- 식의 부작용을 이해하지 않는다
- 글자 그대로 치환하고 포함한다
역할은 크게 셋입니다.
#include "config.h" // 파일 삽입
#define MAX 128 // 텍스트 치환
#ifdef DEBUG // 조건부 포함구성 요소
#include는 개념적으로 다른 파일 내용을 현재 위치에 붙여 넣는 것과 같습니다. 그래서 헤더 중복 포함 문제도 생기고 include guard도 필요합니다.
#define SQUARE(x) ((x) * (x))괄호를 치는 이유는 연산자 우선순위 때문입니다. 하지만 괄호만으로 끝나지 않습니다.
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int i = 1;
int j = 2;
int m = MAX(i++, j++);여기서 문제는 i++, j++가 여러 번 평가될 수 있다는 점입니다. 전처리기는 함수 호출이 아니라 텍스트를 펼칩니다.
그래서 판단 기준은 단순합니다.
- 상수 이름: 매크로 가능
- 플랫폼 분기: 매크로 가능
- 계산 로직: 가능하면
static inline함수
조건부 컴파일
#ifdef _WIN32
#define PATH_SEP '\\'
#else
#define PATH_SEP '/'
#endif좋은 용도는 아래입니다.
- 운영체제 차이
- 디버그 로그 on/off
- 특정 기능의 빌드 포함 여부
나쁜 용도는 같은 기능 로직을 너무 많이 갈라서 코드 흐름을 읽기 어렵게 만드는 것입니다.
체크포인트
- 전처리기는 텍스트 치환
#include는 삽입#define은 타입을 모른다- 조건부 컴파일은 플랫폼/빌드 차이에 쓴다
- 계산 로직은 가능하면 함수로 뺀다
주의할 점
매크로는 부작용이 있는 인자를 안전하게 다뤄주지 않습니다. MIN(i++, j++)처럼 쓰는 순간 값 자체보다 평가 횟수가 문제가 됩니다. 조금이라도 계산 로직처럼 보이면 static inline 함수로 바꾸는 쪽이 낫습니다.
참고 링크
1 sources