본문으로 건너뛰기
Previous
Next
C 언어 시리즈 #08 — 전처리기 8단계·매크로·_Pragma·번역 단위 경계

C 언어 시리즈 #08 — 전처리기 8단계·매크로·_Pragma·번역 단위 경계

C 언어 시리즈 #08 — 전처리기 8단계·매크로·_Pragma·번역 단위 경계

이 글의 핵심

소스가 컴파일러에 도달하기 전에 전처리기가 수행하는 단계를 순서대로 밟고, 매크로가 왜 디버깅을 어렵게 만드는지, include 가드·_Pragma·표준 pragma가 무엇을 보장하는지 설명합니다.

시리즈 안내

#08 | 📋 전체 목차 | 이전: #07 구조체 · 다음: #09 메모리 관리


먼저, 겁부터 주고 시작할게(위험·썰·강한 권고)

매크로 쓰지 마세요, 웬만하면. 연민 섞은 조언이 아니다. C에서 전처리기를 “재미로” 만지다가, 리뷰는 통과하는데 런타임만 이상해지는 케이스는 너무 흔하다. gcc -E로 열기 전까지 “내가 C를 아는지”를 의심하게 만드는 것도 전처리 쪽이 제일이고, 로그/라인/파일이 흐려지는 것도, #define 뒤에 숨는 타입/우선순위 문제도, 전부 텍스트 치환이 먼저라서 그래. 컴파일러가 도와줄 수 있는 범위가 줄어든다.

매크로 디버깅으로 이틀 날려본 적 있으면, 그 감이 온다. “어제 분명히 MAX만 래핑했는데, 왜 a++가 네 번 돌지?” 식이면 이미 함수가 아니라 퍼즐이다. 스택이 안 잡혀, 브레이크포인트가 거짓말을 하고, gdb는 전처리 에 찍힌 코드를 읽는데 사고는 include 순서·-D 조합·헤더 한 줄에서 난다. 이틀 만에 원인이 “매크로가 인자를 두 번 평가했다”였을 때, 사실상 정신 승리는 “표준 C가 뭐라 하든 팀에선 MAX 매크로는 금지”로 끝난다(그게 맞다).

그럼에도 C 현실에선 include 가드, 플랫폼 ifdef, 로그/어설션 래퍼, 오래된 라이브러리 매크로를 완전히 피하긴 어렵다. 그렇다고 해서 비즈니스 로직까지 매크로로 올리는 건, 읽기/리팩터/온콜 모두에 불리다. static inline이나 enum으로 지금 끊을 수 있으면 끊고, “대단히 반복적인 테이블 생성”만 X-매크로나 생성기로 어라(아래 10절). 나는 맛보기로만 배우고, 프로덕션에선 최소가 낫다고 본다.

요즘 C에서 먼저 터지는 민감한 것들을 한 번에 훑자면, 이런 쪽이다. redefinition·conflicting types 류는 include 체인이랑 가드부터 의심하고, expected identifier 같이 “문법이 썩은” 느낌이면 매크로 끝의 세미콜론이랑 괄호다. macro redefined-D랑 헤더가 같은 이름을 둘 다 만질 때 흔하고, MAX/min 쓰고 값이 미친듯이 나오면 다중 평가/부작용이다. ##·#로 트릭을 건 코드는 컴파일러마다 다르게 굴 수 있어서(특히 MSVC/GCC 믹스) 생산 코드에선 “단순함이 정의”다. 링크/심볼 지옥은 static/inline/extern을 헤더에 대충 섞을 때, Windows는 include 순서(windows.h vs winsock2.h 같은)에서 터진다. assert엉뚱한 파일/라인을 찍으면 래퍼/매크로 위치를 통일해라. -D 넣었는데 #if가 안 먹으면 정수/공백/문자열 꼬임(5.4)이다. 대형 include만으로 빌드가 느리면, 트리를 줄이거나 전방 선언, 혹은 툴이 주는 PCH 쪽을 본다. 진단 루틴은: 전처리 덤프(gcc -E 등)로 “한 장짜리 실화”를 먼저 뽑고(12절), -D/-I/-std재현한 다음, 직전에 define을 바꾼 헤더를 좁힌다(6절). 그리고 13절 쪽에서 다시 함수/정책으로 되돌리는 쪽이 장기적으로 싸다.


1. 전처리기 개요: 역할과 처리 단계

C 소스(.c)는 컴파일러가 바로 “문법 분석”에 들어가지 않는다. 전처리기(preprocessor)는 먼저 지시문(directive)에 따라 텍스트를 변형하고, 여러 소스/헤더를 합쳐 단일 “전처리된 번역 단위”의 흐름을 만든다. 그리고 매크로 치환, 조건부 컴파일 같은 규칙이 적용되며, 그 결과가 이후의 컴파일 단계로 넘어간다.

전처리기가 담당하는 대표적 일은 다음과 같다.

  • #include로 헤더를 합성한다(논리적으로는 “다른 텍스트를 이 위치에 삽입”에 가깝다).
  • #define으로 치환 규칙(매크로)을 정의하고, 함수형 매크로의 인자 치환·#/## 연산을 수행한다.
  • #if 계열로 “어떤 텍스트를 남길지”를 결정한다(조건부 컴파일).
  • (구현/표준) 일부 구현·플랫폼·표준 관련 미리 정의된 매크로(__FILE__ 등)를 제공하거나, 컴파일러는 관례적 매크로(__GNUC__ 등)를 제공한다.

1.1. 논리적 8단계(ISO C의 “이해를 위한 모델”)

표준은 구현이 동일한 관찰 가능한 결과를 내도록, 전처리·번역의 과정을 논리적 단계로 설명할 수 있게 해둔다. 실제로는 컴파일러가 단계를 합치거나 병렬화할 수 있으나, 문제의 원인을 쪼개는 데 이 모델이 매우 유용하다. 전형적으로 다음과 같이 압축해 설명한다(세부는 표준/구현에 따라 다를 수 있음).

  1. 문자 매핑: 구현이 정의한 인코딩(예: UTF-8)의 소스를 기본 소스 문자 집합 관점의 표현으로 가져온다.
  2. 줄 이어 붙이기: \로 끝난 행(줄끝 결합)을 한 줄로 합친다.
  3. 전처리 토큰화: 주석은 공백으로 취급되며, 전처리 토큰으로 나뉜다(컴파일 토큰과 동일하다는 뜻이 아님).
  4. #include 처리·재귀: #include지정된 파열을(또는 구현이 찾는 경로를 통해) 가져와 재귀적으로 전처리한다(중복/순서가 설계에 큰 영향).
  5. 매크로·전처리 연산: 객체형/함수형 매크로, 조건부 컴파일(#if 계열), #(문자열화), ##(토큰 붙이기) 등이 수행된다.
  6. (구현/표준) 문자 상수/문자열 관련의 일부 전처리·구현 정의적 처리(세부 배치는 이식에 따라 달라질 수 있음).
  7. 남은 전처리 토큰컴파일 토큰으로(다음 컴파일 단계).

독자가 기억해야 할 핵심은 “전처리기는 C 문법이 아닌, 따로 먹는 언어에 가깝다”는 점이다. 특히 함수형 매크로는 C 함수처럼 보이나 스택 프레임·인자 전달·최적화 모델이 전혀 다르다.

1.2. “번역 단위(translation unit)”의 경계

C 프로그램은 보통 main.c 하나로 끝나지 않는다. 각 .c 파일은(헤더 포함을 거친 뒤) 하나의 번역 단위로 컴파일되어 오브젝트 파일이 된다. 링커가 이들을 합쳐 실행 파일을 만든다.

  • #include는 “모듈 import”가 아니다. 수많은 튜토리얼이 import에 비유하지만, 실제로는 전처리 시점에 텍스트를 붙이는 동작이며, 같은 선언/정의의 중복이 생기기 쉽다(그래서 헤더 가드, static, extern, ODR(일반적 의미)과의 충돌이 문제가 된다).
  • 같은 매크로 정의번역 단위마다 달라질 수 있다. 어떤 .cFEATURE_X를 켜고, 어떤 .c는 끄는 식. 링크 시점에 “놀랍게도” 런타임이 한쪽의 기대를 깨는 사례가 나온다(빌드/헤더 순서 이슈).

1.3. 이 글의 독해 순서(흐름)

맨 앞에 감(위험·썰·솔직한 권고)를 박아 뒀다. 그다음 전처리기가 뭔지(이 절), 8단계·번역 단위, 그다음 실무 궤적로 includedefine → (다시) 위험(부작용/괄호) → 조건부 → _Pragma/#pragma → 미리 정의된 매크로 → #/##·X-매크로 → 가드·gcc -E웬만하면 매크로 말고 쪽(13절) → 실전 → 오류/트러블슈팅(15절)까지 간다. “8단계” 모델은 1.1에 있다.

1.4. (참고) 다이그래프·트라이그래프·C23 변화

과거 C에는 소스/실행문자집합 불일치를 완화하려 ??= 같은 트라이그래프(trigraph)가 있었으나, C23에서 제거되었다(현대 툴체인/UTF-8 환경에 맞는 변화). 다이그래프(digraph)는 일부 [] {} 대체로 남아 있을 수 있으나(표준/구현에 따름) 널리 쓰는 이유는 적다. 전처리기 확장/매크로 트릭이 더 중요하므로, 이 시리즈에서는 “환경/표준” 수준의 언급에 그친다.


2. #include: 헤더 포함, <> vs "", include guard(개요)

#include는 전처리 지시문으로, “여기에 다른 파일의 내용을 끼워 넣는다”로 이해하는 것이 가장 안전하다.

2.1. "" vs <>의 전형적 의미

  • #include "foo.h": 먼저 현재 파일 기준(또는 구현이 정의한) “사용자 경로/프로젝트 경로”에서 foo.h를 찾는다(구현/컴파일러 옵션에 따름). 팀 내부·프로젝트 로컬 헤더에 쓰는 경우가 많다.
  • #include <foo.h>: 시스템/표준 라이브러리 루트에서 찾는 관례가 강하다(또는 -isystem 등으로 잡힌 경로).

“표준 C 라이브러리는 <>” 정도는 일반론이지만, 빌드 시스템(Meson/CMake)에 따라 -I로 프로젝트 include가 앞에 오면 탐색 순서는 복잡해진다. 중요한 실무 가이드는 동일한 헤더는 동일한 방식으로, 그리고 빌드에 의존하는 상대/절대 경로 난립을 막는 것이다.

2.2. include가 만드는 “의존성 그래프”

대형 프로젝트에서 빌드가 느리거나, 헤더를 바꾸면 전 세계가 재빌드되는 이유는, #include다른 헤더를 끌어다가, 그것이 또 거대한 트리를 불러올 수 있기 때문이다.

  • 최소 포함: 선언/정의에 꼭 필요한 헤더만. #include는 “빌이 무너지는 한만큼” 늘어난다.
  • 앞/뒤 포함 순서매크로/조건부 컴파일에 영향을 줄 수 있다(특히 Windows.h + winsock2.h 류의 순서 제약은 유명하다). 팀에선 “안전한 순서”를 문서·코드로 박는다(예: 공통 platform.h에서 순서를 고정).

2.3. include와 “include guard(헤더 가드)”

같은 헤더를 여러 .c에서 include하는 것은 정상이다. 하지만 한 번의 번역 단위 안에서, 동일한 내용이 여러 경로/중첩으로 두 번 이상 들어오면 중복 선언/정의로 터질 수 있다. 이를 막는 전통 기법이 include guard이며, 이 글 11절에서 패턴을 다시 정리한다(여기서는 개념만: “첫 include에서만 본문을 열고, 이후엔 잠그는”).

예시: mylib.h의 최소 스케치(개념)

#ifndef MYLIB_H
#define MYLIB_H

/* 선언들 */

#endif /* MYLIB_H */

#define·#ifndef·#endif는 곧 5절·11절에서 본다.

2.4. (맛보기) 모듈(C23 import 등)과의 대비(선택)

C23는 구현/툴체인에 따라 모듈이 등장하며(환경에 따라) #include 중심 모델과 병행이 가능하다. 이 글의 범위는 가장 흔한 include+헤더 가드+매크로 쪽에 맞춘다(레거시/임베디드/대다수 C 코드베이스가 이 모델).


3. #define 매크로: 상수 매크로와 함수형 매크로

#define이름(식별자)을 “토큰 시퀀스”에 매핑한다. 단순 치환이라고 생각하면 처음엔 쉬우나, 가장 잘 틀리는 지점이기도 하다.

3.1. 객체형(object-like) 매크로: PI·DEBUG·버전

#define PI 3.14159265358979323846
#define APP_VERSION_MAJOR 1
#define APP_VERSION_STR "1.0.0"
  • 끝에 세미콜론을 넣는 실수가 잦다. #define PI 3.14;PI3.14;로 치환되어 엉뚱한 곳;가 끼어 문장이 쪼개질 수 있다(특히 매크로를 식/문맥이 섬세한 곳에 끼울 때).
  • 문자열/숫자를 조합하는 매크로(APP_VERSION 빌딩)는 문자열화(#)로 깔끔해질 수 있다(9절).

3.2. 함수형(function-like) 매크로: MAX/MIN·래핑

#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))
  • 인자 a, b, x필요 이상을 여러 번 평가될 수 있다(4절). 이는 “C 함수”와 결정적으로 다름.
  • do { ... } while(0) 패턴(문맥에 따라)은 “문(statement)에 안전히 넣는 매크로”에 쓰기도 하지만(구식 코드베이스), 신규는 인라인 함수/제네릭을 우선한다(13절).

3.3. 가변 인자 매크로(C99 __VA_ARGS__)

#define LOG_ERR(fmt, ...) \
    fprintf(stderr, "[ERR] " fmt "\n", __VA_ARGS__)

...가 비는 호출(구현/표준)과의 궁합이 까다로울 수 있어(구버전/MSVC), 대안으로 빈 인자##__VA_ARGS__ 스타일로 꾸미는(구현 확장) 코드도 많다(이식성이 중요한 프로젝트는 문서/매크로 래퍼로 통일).

3.4. (실무) “매크로는 네이밍”으로 눈에 띄게

전역 상수를 매크로로 둔다면, 팀에선 MYLIB_VALUE_MAX처럼 접두어/대문자 규칙을 두는 경우가 많다(매크로가 퍼지면 “선언/정의를 찾는” 사냥이 괴로워짐). 반대로, 함수/변수처럼 보이게 이름 짓는 매크로(max)는 “함수가 아닌 것처럼” 오해를 키운다.


4. 매크로의 위험: 부작용·괄호 규칙·MAX(a++, b++)

서두에서 한 번 말했지만, 4절은 그걸 “기술적으로” 다시 캔다. 함수형 매크로는 C 함수가 아니다. 인자는 “값으로 먼저 꺼내는” 식이 아니라(함수는 그렇게 보이지만), 토큰 치환으로 여러 군데에 붙는다. 그래서 부작용(side effect)이 폭주한다. 여기 터지면, 솔직히 디버거보다 gcc -E 먼저다.

4.1. 다중 평가(여러 번 계산) 문제

#define MAX(a, b) ((a) > (b) ? (a) : (b))

int x = 1, y = 2;
int m = MAX(x++, y++); /* 둘 다 증가? 한쪽만? 표준 C에서 기대 X */
  • ? :는 동일 을 두 번/세 번 쓰면 같은 부작용이 반복될 수 있어 미정(UB에 가깝다고 보는 팀이 많음), 최소한 놀랍고 나쁜 결과를 낳는다.
  • 프로덕션에서는 static inline + 필요 시 _Generic으로 타입을 나누는 편이 안전하다(13절).

4.2. “괄호로 감싼다”는 룰의 이유

#define MUL(x, y) x * y
int z = MUL(1 + 2, 3); /* 1 + 2 * 3? */

#define MUL2(x, y) ((x) * (y))
int w = MUL2(1 + 2, 3); /* (1+2)*3 */
  • 매크로 본문은 괄호로 “우선순위/결합”을 밖이 아니라 내부에서 먼저 끊어야 안전하다(특히 +, * 혼재).

4.3. if와 매크로(세미콜론 함정) 맛보기

#define MAYBE_ERR(msg) if (err) printf("%s", msg)

호출 측이 if (cond) MAYBE_ERR("x"); else ... 를 쓰면 dangling-else/문 끼워넣기 지옥이 열릴 수 있다(레거시에서 자주). 이런 스타일은 do-while(0)로 감싸거나(구식) 인라인 함수로 대체한다(13절).

4.4. (주의) “가독성이 좋아 보이는” 매크로가 만드는 숨 음수

  • 에러 메시지에 매크로를 얹으면 실제 “파일/행”이 흐려 디버깅이 늦어질 수 있다(반대로, 표준 __FILE__/__LINE__는 도움이 된다; 8절).
  • 로그/어설션은 가능하면 한 줄짜리 함수/매크로 래퍼로 통일한다.

5. 조건부 컴파일: #if·#ifdef·#ifndef·#elif·#else·#endif

조건부 컴파일은 “버전/플랫폼/기능 플래그/디버그 모드”를 빌드 옵션에 맞춰 소스에 반영하는 핵심 수단이다.

5.1. defined#if defined(...)

#if defined(DEBUG) / #if !defined(NDEBUG)는 흔하다(구현/프로젝트에서 DEBUG·NDEBUG를 켬).

#if defined(_WIN32)
/* ... */
#elif defined(__linux__)
/* ... */
#else
/* ... */
#endif
  • #ifdefdefined의 축약으로 보면 편하다(단, 복잡한 조합은 #if가 낫다).
  • #elif로 분기(정수 상수/매크로 비교)를 쌓는다.

5.2. “숫자/버전” 비교로 표준/컴파일러 기능 가리기(개념)

C 표준/확장 기능은 보통 __STDC__, __STDC_VERSION__ 등으로 “어떤 문법/헤더를 켤지”를 판단한다(팀/코드베이스에선 -std=c11 권장, 구현/플래그는 문서화).

#if __STDC_VERSION__ >= 201112L
/* C11+ */
#endif

5.3. 실무 냄새: extern "C"(C++ 혼용)

C++로 링크할 때, C API를 C 링크 이름으로 노출하려 extern "C" { ... }가 필요하다(전처리로 감쌀 때가 흔함). 이 또한 __cplusplus로 감싼다(프로젝트 헤더에서 반복).

#ifdef __cplusplus
extern "C" {
#endif
/* C 선언 */
#ifdef __cplusplus
}
#endif

5.4. (경고) 0 false·#if의 정수 상수: 표현식이 끼어드는 함정

#if정수/매크로 전개가 끼어드는 “작은 언어”이므로, 문자열/실수를 다루는 방식은 cpp 류와 다르다. 복잡한 조건은 빌드 시 -D로 단순한 플래그를 넣는 편이 낫다(가독성/재현).


6. #undef: 매크로 정의의 해제

#undef NAME이름 NAME의 매크로 정의를(존재한다면) 제거한다(있다면 이후 defined(NAME)이 거짓).

6.1. 쓰는 이유(실무)

  • 이름 충돌을 팀 단위로 막을 때(간단한 예: 어떤 헤더가 min을 매크로로 걸고 있어서 함수/지역과 충돌).
  • 가드 해제/임시로, 매크로를 한 스코프에서만 쓰는 패턴(드물지만)에서 주의 깊이 사용(대체로 “헤더에선 안 넣는” 쪽).
  • (주의) 시스템/표준/관례 매크로는 함부로 #undef하지 말고, “문서화된” 충돌 해법(순서/검은 목록)을 쓰는 편이 낫다.
#define TEMP 1
/* ... TEMP 사용 ... */
#undef TEMP

7. #pragma_Pragma: 컴파일러 지시

#pragma구현이 정의한 지시를 보내는 수단이며(그래서 이식이 깨지기 쉬움), 표준에도 일부를 “이름이 있는 pragma”로 관리한다.

7.1. C 표준 STDC pragma(예: FP)

표준에 이름이 잡힌 일부(예: 부동소수점 관련)는 #pragma STDC ...로 다루는 케이스가 있다(세부/가용성은 구현/환경). 중요한 실무는 “특정 소스 3줄에 끼워 넣는 것”보다, 빌드/플랫폼에서 동일한 최적/정확도 정책일관되게 켜는 쪽(팀 정책).

7.2. “컴파일러/플랫폼” pragma의 실무(경고/최적화/정렬/팩 등)

  • MSVC, GCC, Clang, ICC 등은 각기 #pragma를 제공한다(경고 on/off, pack, 섹션 배치, 인트린식 등). 빌드가 바뀌면 동작/ABI가 흔들릴 수 있으니, “문서 + 최소 격리”를 원칙으로 한다(헤더 1곳).
  • (임베디드) “특정 주소/섹션/벡터”는 #pragma일 수도, 링커 스크립트/속성이 일수도(팀/타깃이 다름).

7.3. _Pragma("...")는 왜 있나(매크로 안에서)

#pragma전처리 지시라서(편의상) 매크로로 감싸기 어렵다. C는 _Pragma 표현식(매크로 확장이 가능하도록)을 제공해, “매크로로 pragma를 뿌리는” 래퍼를 만들 수 있다(전처리기가 처리).

#define DO_PRAGMA(x) _Pragma(#x)
/* 예: DO_PRAGMA(GCC optimize("O3"))  -- 구현/환경에 따라 */

(위 예는 팀 정책/구현에 강하게 의존하니, 남발하지 말고 “문서/리뷰/단일 래퍼”로 쓰는 것이 낫다.)

7.4. (비교) #pragma once (비표준이나 널리 지원)

#pragma once는 “이 파일 include를 한 번만” 같은 의미(구현마다)로, 전통 ifndef 가드와 비교된다(11절/FAQ). 이식/도구(일부 캐시) 관점이 있어 둘 중 하나+정책을 택한 팀이 많다(혼용은 오히려 혼란).


8. 미리 정의된 매크로(표준·관례)

디버깅/로그/빌드 식별에 자주 쓰인다(구현/표준이 추가하는 것이 많다).

  • __FILE__: 현재(전처리 시점) 파일 이름 문자열(구현/형식).
  • __LINE__: 정수(행 번호).
  • __DATE__ / __TIME__: 전처리 시점(문자열) — 재현 가능 빌드에선 주의(결정론/해시). 릴리스 바이너리에 “빌드 시각”이 박힌다(보안/감사에선 선호/비선호가 갈림).
  • __func__(C99): 함수 이름(문자열; 표준, 단 매크로가 아님에 유의(그래도 printf에 자주).
  • (구현) __GNUC__, __clang__, _MSC_VER … — #if 분기(경고/확장/필드 정렬)에 쓰되, “확장 남용”을 경계.

8.1. 예: 간단 assert 스케치(개념)

#include <stdio.h>
#include <stdlib.h>

#define ASSERT(cond)                                        \
    do {                                                     \
        if (!(cond)) {                                       \
            fprintf(stderr, "Assert failed: %s @ %s:%d\n",   \
                    #cond, __FILE__, (int)__LINE__);         \
            abort();                                         \
        }                                                    \
    } while (0)

(운영/보안/멀티스레드 환경에선 assert 매크로/정책이 제각각. 여기는 확장 아이디어만.)

8.2. (주의) “매크로는 어디에 덮이나”

__FILE__#include로 끼워 넣는 파일/매크로 위치에 따라 “사람 눈에 보이는” 디버그가 달라질 수 있어(특히 “래핑” 매크로), 팀 루틴은 같은 스타일로 고정한다.


9. 고급 기법: 문자열화(#)와 토큰 붙이기(##)

9.1. # (stringize): 토큰을 문자열로

함수형 매크로 인자에 ##없이 #을 붙이면, 인자의 토큰을 문자열 리터럴로 만든다(세부/경계/공백은 구현/표준에 따름; 팀에선 “간단 케이스만”).

#define STR(x) #x
const char* s1 = STR(hello); /* "hello" */

9.2. ## (token pasting / token concatenation)

두 토큰을 이어붙여 한 토큰으로(가능한 경우) 만든다. 빈 토큰/경계 케이스는 컴파일러/확장 이슈가 있어(특히 MSVC/GCC의 역사) 단순/명시적 쪽이 안전하다.

#define PASTE(a, b) a##b
int PASTE(var, 123) = 0; /* var123 */

9.3. (함정) ###의 조합, 평가 순서

  • 서로 다른 “전처리 단계(개념)”에서, #는 인자 문자열화 전에(세부), ##붙이기가 먼저/나중(환경) 문제로 놀랍게 컴파일되는 케이스가 있다(특수 목적/레거시에서만, 신규는 피하는 편).
  • (실무) 타입-안전/리팩터링에 불리(IDE “찾기/바꾸기” 품질 떨어짐)하므로, X-매크로(10절)처럼 “반복”에만, 혹은 빌드 시 코드 생성으로 밀기도 한다(대형 프로젝트).

10. X-매크로(X-macro) 패턴: “목록 1곳, 생성 여러 곳”

X-매크로는 LIST(X)X를 바꿔 열/함수/스위치/문자열 테이블을 동시에 만드는 기법으로, C에서 자주 쓰인다(“DRY, 단 매크로로”).

10.1. 최소 예: 열거 + 문자열

/* colors.def */
X(RED,   "red")
X(GREEN, "green")
X(BLUE,  "blue")

/* colors.h */
#define X(a, s) a,
enum Color {
  #include "colors.def"
  COLOR_COUNT
};
#undef X

#define X(a, s) s,
static const char* const COLOR_NAME[] = {
  #include "colors.def"
};
#undef X

장점: 열거 값과 문자열이 어긋날 확률이 줄고, 스위치/테이블/검사기를 같은 소스에서 만든다.
단점: 디버그/툴 지원(리팩터링)이 “일반 C 코드”만큼 좋지 않다. “매크로의 언어”에 올라탄 셈.

10.2. (실무) 용도/한계

  • 프로토콜/오류코드/명령어 테이블에 유용(동일 ID를 switch, 로그, REST 표현으로 공유).
  • 너무 커지면 python으로 코드 생성(빌드 스텝)이 더 읽힐 수도 있다(팀 취향).

11. 헤더 가드(Include Guard)와 중복 포함 방지(패턴 정리)

11.1. 전통 가드(표준, 이식)

#ifndef FOO_H
#define FOO_H
/* ... */
#endif
  • 고유한 매크로 이름(프로젝트 접두)을 써 충돌을 피한다.
  • (팁) 일부 팀은 PROJECT_PATH_FOO_H 같이 “경로성”을 넣는다(유니크, 단 길어짐).

11.2. #pragma once(비표준이라도 실무에서 흔함)

#pragma once
/* ... */
  • 널리 지원(웬만한 툴체인). “동일 inode/경로” 류의 엣지가(구구체적으로) 프로젝트/도구에 따라 논쟁이 있을 수 있으니, “정책 1가지+검사”로 가는 팀이 많다.

11.3. (잘못된 믿음) “헤더에 static inline 다 넣는다”

C에서 헤더에 static inline 함수를 둔다고 해서 “항상 중복 ODR”이 안전하다는 식의 논의는(특히 비-inline/static 데이터·typedef+동일 등) 케이스에 따라 링크 이슈로 이어질 수 있다(대형/혼용 코드). 기본은 하나의 정의(ODR-analog) 직관을 갖고, 팀 가이드를 따른다(이 글은 “전처리+헤더”이므로 세부는 링크/빌드 시리즈로).

11.4. (참고) C++20 modules vs C(현황)

C++ 쪽은 모듈이 성숙해가며 #include 중심을 점진적으로 줄이는 흐름이 있으나, C 세계(특히 임베디드/기존 API)는 아직 include가 주류다(1.4, 2.4 참고).


12. 전처리 디버깅: gcc -E로 “확장 결과” 확인하기

무엇이 매크로로 터지나 궁금할 때, 가장 강한 무기는 전처리 결과를 그대로 덤프하는 것이다.

  • GNU C: gcc -E (또는 clang -E도 유사)로 전처리만 수행해 stdout에(또는 -o로) 남긴다.
  • MSVC/E(또는 /P로 파일)를 제공한다(문서/버전).

12.1. gcc -E 최소(개념)

gcc -E -std=c11 -I./include src/main.c
  • -I는 include 경로(프로젝트 구조). -D로 매크로를 찍고, -E빠르게 “실제 C 전에 무엇이 남는지” 본다.
  • 줄/파일 마커(#로 시작하는 라인)가 #line 류로 남는 경우가 있어(구현) 읽는 법을 익히면(특히 #include 체인) 원인이 보인다.

12.2. (팁) “작은 .c로 재현”

문제는 거대하다. 먼저 최소 파일 20줄 + 동일 include 체인으로 gcc -E를 돌리면, 누가 어떤 것을 define했는지 빨라진다.

12.3. (읽기) 전처리 산출물이 너무 클 때

  • -C(주석 유지) 등 옵션이 있다(툴/버전/컴파일러). 목적(원인)에 맞는 플래그를 고른다.
  • -save-temps=obj(GCC) 류로 중간을 남기기도(조사용).

12.4. (대안) cpp / IDE 전처리 보기(환경)

IDE가 “preprocess” 뷰를 주기도(Clangd/VS/…). “정확한 재현”은 CI/로그에 동일한 컴파일러 커맨드로 남는 것이 좋다.


13. Best Practices: 매크로 vs static inline·const·enum·inline 정책

13.1. “상수”는 뭘 쓰나(요약)

  • 읽는 코드 품질: enum, static const/static const 관례(구현/표준), constexpr이 없는 C에선(…) 최적화/디버그 측면이 비슷해지는 경우가 많다(정확·환경·-O에 따름; FAQ 참고).
  • 매크로는 스코프/네임스페이스가 약하고, undef/순서/중복 define에 취약(대형 include).

13.2. MAX·min·비교 유틸

  • (신규) static inline + 필요 시 static inline 중복(헤더) 정책(팀).
  • (타입) long long 류/부동(부작용)은 절대 MAX 매크로로 하지 말고, 정확한 비교/비교 유틸을 따로(혹은 언어/라이브리).
static inline int imax(int a, int b) { return a > b ? a : b; }

(위는 예; C99 inline 세부/ODR-유사 룰은 “심볼/외부 inline” 케이스에 주의한다.)

13.3. (제네릭) C11 _Genericmax·출력(개념)

팀/표준이 C11+면 _Generic으로 타입을 나누는(반복) 패턴이 있다(여기는 개념만; 실수/부호/프로모션을 아직 먹이면 안전하지 않다).

13.4. (정리) “매크로는 남는 이유”

  • include guard/조건부/플랫폼/ABI 분기/간단 stringize 등에 여전히 강하다.
  • 비즈니스 로직함수로, 빌드/포팅/반복매크로(또는 생성)로 — 이런 분리를 추천한다.

14. 실전 예제: 설정 헤더·플랫폼 분기(템플릿)

14.1. config.h 스케치(빌드가 -D로 주입)

/* config.h */
#ifndef CONFIG_H
#define CONFIG_H

#if !defined(ENABLE_LOG)
#define ENABLE_LOG 0
#endif

#if !defined(PLATFORM_WIN)
#  if defined(_WIN32) || defined(_WIN64)
#    define PLATFORM_WIN 1
#  else
#    define PLATFORM_WIN 0
#  endif
#endif

#if ENABLE_LOG
#  include <stdio.h>
#  define LOG_INFO(fmt, ...) \
    do { printf("[I] " fmt "\n", __VA_ARGS__); } while (0)
#else
#  define LOG_INFO(fmt, ...) ((void)0)
#endif

#endif /* CONFIG_H */
  • “로그/디버그”는 0 비용/부작용 제거를 염두에 두고(특히 임베디드), printf 포맷/보안(포맷 스트)도 별 이슈(팀 루틴).

14.2. (간단) POSIX/Win32 sleep 래핑(개념)

#if PLATFORM_WIN
#  include <windows.h>
#  define sleep_ms(ms) Sleep((DWORD)(ms))
#else
#  include <unistd.h> /* ... */
#  define sleep_ms(ms) /* usleep/nanosleep 팀 래퍼 */
#endif

(실서비스는 timespec·오류/인터럽트/정확도 정책이 필요. 여기는 전처리 분기 예시로만.)

14.3. (팁) “하나의 platform_*.h”로 순서/매크로 고정

win_def 같은 것들의 순서(2.2) 류는 platform.h 한 군데에 모아서 “모든 include는 이 래퍼를 먼저” 같은 규율을 쓰기도 한다.


15. 일반적 오류와 문제해결(서술)

표로 정리해도, 결국 케이스는 비슷하게 돌아온다. redefinition이나 conflicting typesinclude 체인에서 같은 typedef/struct를 두 번 끼운 경우가 흔하다. 11절 가드가 살아 있는지, “한 번의 번역 단위에서 동일 경로로 두 번 열리나”를 gcc -E한 장으로 뽑아서 보면 금방 좁혀진다. expected identifier처럼 “문장이 쪼개진 느낌”이면, 매크로가 끼워 넣은 ;) 땜에 그런 경우가 많다(3.1의 세미콜론 실수, 4.2 괄호). macro redefined-D와 헤더가 똑같은 이름을 다시 쓰는 팀 스모크에서 터지고, MAX/min 뒤에 값이 랜덤에 가깝게 가면 4.1 다중 평가를 의심해라. #·##는 9.3처럼 “컴파일러마다 살짝” 달라질 수 있으니(특히 MSVC/GCC 혼합), 프로덕션에선 단순화하거나 생성기로 밀었다고 생각하는 편이 낫다.

링크 쪽(정의/심볼)은 static·inline·extern이 헤더/소스에 원칙 없이 섞이면 11.3 류 토론으로 간다(“한 .c에 뭘 둘지” 팀 루틴). Windows는 2.2 include 순서 + 매크로 충돌이 경고/빌드를 썩이는 유명 루트다—공통 platform.h순서를 못 박는 팀이 많다. assert나 로그에 엉뚱한 파일/라인이 찍히면, 래퍼/매크로 체인이 #line을 흐리게 만든 8.2 케이스를 보면서 스타일을 한 군데로 모은다. -D는 넣었는데 #if가 안 먹으면 5.4처럼 정수/공백/문자열 꼬임이 의심되고, 대형 include만으로 느려지면 2.2 트리를 줄이거나 전방 선언, PCH(툴)까지 생각한다.

권장 진단 순서는 늘 이거다. (1) gcc -E전처리된 단일 파일에서 의심 구간(12절) → (2) CI/로그에 같은 -D·-I·-std재현 → (3) define이 바뀌는 직전 헤더(순서) → (4) 6절 undef·이름 충돌 → (5) 13절 쪽 “비매크로화”로 다시 안 터지게 만든다. 첫 장의 써먹을 한 줄: “웬만하면 매크로 말고”—여기서 끊지 못하면, 최소한 팀 룰로 한 번만 더 걸러라.


16. 내부 동작을 한 장으로(요약 mermaid; 개념)

flowchart TD
  A[소스+헤더 읽기] --> B[전처리 토큰화/주석 제거(개념)]
  B --> C[#include로 텍스트 합성(재귀)]
  C --> D[조건부 컴파일로 분기/제거]
  D --> E[객체/함수형 매크로 확장 + # / ##]
  E --> F[전처리 산출 → 컴파일러 토큰]

17. (원문 보강) ISO C 8단계·_Pragma·번역 단위(압축)

앞 1.1~1.2에서 이미 “논리 8단계”와 include/번역 단위의 경계를 강조했다. 여기서는 한 가지만 박는다: #include는 붙이기이고, “모듈”이 아니다(2절, 11절). 그래서 동일한 선언이 중복되지 않게 설계(가드, 최소 노출)하는 것이 C 팀의 기본 훈련이다.

#pragma STDC 등의 표준 pragma는 컴파일/부동/일부 측면을 “문장으로” 제어하는 케이스가 있으나(7.1), 팀/제품이 요구하는 정책이 있다면 “소스 3곳”보다 “빌드 한곳(플래그) + 공통 build_config” 쪽이 유지에 유리하다(7, 14절). _Pragma는(7.3) “매크로 래퍼”를 위해 존재 이유가 납득될 수 있다(확장/구현 믹스는 문서/리뷰 필수).


18. 요약

  • 맨 앞서 말했듯, 웬만하면 매크로 쓰지 말고—그다음 1절에서야 “전처리기”가 C 문법 밖에 있는 따로 먹는 언어라는 점을 차분히 잡는다(그리고 3~4절에서 “함수형 매크로=함수”는 절대가 아님을 다시 박는다).
  • #include텍스트 합성이므로 의존성/순서/중복이 실무의 대부분(2, 11절).
  • 조건부, #undef, pragma빌드/플랫폼/기능을 꿰는 축(5~7절). __FILE__/__LINE__은 로그/어설션(8절).
  • #/##·X-매크로는 강력하지만 도구/리팩터/이식에 취약(9~10절) → “생성/함수/정책”로 분리(13절).
  • 원인이 안 보이면 gcc -E로 “전처리 후 텍스트”를 본다(12절).
    다음: #09 동적 메모리·단편화·도구

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. 헤더/플랫폼/기능 스위치를 동일한 빌드/재현 가능한 형태로 유지하려는 순간(빌드 시스템+-D+#if, include 가드)과, MAX류로 퍼진 부작용함수/정책으로 치우는 순간(리뷰/리팩터)에 바로 쓰인다(15절 본문).

Q. gcc -E만 쓰면 끝인가요?

A. 단서로 훌륭하지만, “-I/-D/다른 컴파일러(Clang/MSVC) 동일”을 맞춰야 “재현”이 된다(12절). CI 로그에 정확한 커맨드를 남기는 습관이 중요하다.

Q. pragma once vs include guard, 어떤 쪽이 맞나요?

A. “표준이냐/도구/이식/정책”의 문제(11, FAQ). 한 가지로 통일+검사가 핵심(혼용은 팀이 컨트롤 가능할 때만).

Q. __DATE__/__TIME__는 릴리스에 넣어도 되나요?

A. “재현 가능한 빌드(Deterministic build)”/보안/감사에 따라 다르다. 최소한 빌드 ID는 어디에 박혔는지를 문서화(8.2).

Q. 선행으로 읽으면 좋은 글은?

A. C 언어 시리즈 목차에서 함수(ABI), 구조체(패딩), 타입이 연결되어 있다(Frontmatter relatedPosts 참고). 전처리기는 “헤더/링크/ABI”의 접착이다.

Q. 더 깊이 공부하려면?

A. C 표준(전처리/번역), 컴파일러 매뉴얼(전처리 옵션), cppreference는 매크로/연산/순서를 빠르게 훑는 데 유용하다(구현/버전/표준/확장은 항상 팀 케이스로 확인).


같이 보면 좋은 글 (내부 링크)


이 글에서 다루는 키워드 (관련 검색어)

C, 전처리기, 매크로, pragma, 컴파일 파이프라인 등으로 검색하시면 이 글이 도움이 됩니다.