Quick Reference
전처리기는 컴파일 전에 텍스트를 포함·치환·제외합니다. 타입 검사와 인자 한 번 평가가 필요한 계산은 매크로 대신 함수로 둡니다.
#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
static inline int max_int(int a, int b) {
return a > b ? a : b; /* 타입 검사와 인자 한 번 평가 */
}#include는 선언 텍스트를 가져오고, #define은 타입 없는 토큰 치환을 합니다. 빌드·플랫폼 분기는 작은 경계에만 두고, 계산을 재사용할 때는 static inline 함수를 우선합니다.
문법
전처리기는 C 문법을 해석하는 단계가 아니라, 컴파일 전에 텍스트를 바꾸는 단계입니다.
- 타입 검사를 하지 않는다
- 식의 부작용을 이해하지 않는다
- 글자 그대로 치환하고 포함한다
역할은 크게 셋입니다.
#include "config.h" // 파일 삽입
#define MAX 128 // 텍스트 치환
#ifdef DEBUG // 조건부 포함#ifdef DEBUG는 DEBUG가 정의됐는지만 검사하고 값이 0인지 1인지는 보지 않습니다. 값 자체로 분기하려면 #if DEBUG 또는 #if defined(DEBUG) && DEBUG처럼 의도를 적습니다. 구현 예약 이름(대문자와 밑줄 조합 등)은 프로젝트 macro 이름으로 쓰지 않습니다.
구성 요소
#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
- 특정 기능의 빌드 포함 여부
나쁜 용도는 같은 기능 로직을 너무 많이 갈라서 코드 흐름을 읽기 어렵게 만드는 것입니다.
플랫폼 경계와 build option은 작은 adapter에 모으고, 일반 기능 로직을 전처리 분기마다 복제하지 않습니다. 조건이 많아지면 테스트 조합도 함께 늘어납니다.
빠른 점검
- 전처리기는 텍스트 치환
#include는 삽입#define은 타입을 모른다- 조건부 컴파일은 플랫폼/빌드 차이에 쓴다
- 계산 로직은 가능하면 함수로 뺀다
주의할 점
매크로는 부작용이 있는 인자를 안전하게 다뤄주지 않습니다. MIN(i++, j++)처럼 쓰는 순간 값 자체보다 평가 횟수가 문제가 됩니다. 조금이라도 계산 로직처럼 보이면 static inline 함수로 바꾸는 쪽이 낫습니다.
참고 링크
2 sources