C언어 전처리기와 매크로, 실기 예제 포함
C 프로그램이 실행 파일로 만들어지기까지는 일반적으로 알려진 컴파일 외에도 그보다 앞서 진행되는 단계가 있다. 바로 전처리(Preprocessing)이다. 우리가 익숙하게 사용하는 #include 한 줄 역시 사실은 컴파일러가 아닌 전처리기가 처리하는 지시문이다. 전처리기는 소스 코드에 적힌 #으로 시작하는 모든 지시어를 해석하고, 그 결과로 만들어진 새로운 소스를 비로소 컴파일러에 넘긴다. 정보처리기사 실기에서도 #define으로 정의된 매크로의 치환 결과와 괄호 누락에 따른 부작용을 묻는 문제가 자주 출제된다. 본 글은 전처리 단계의 의미와 핵심 지시문, 그리고 시험 빈출 패턴을 코드 예시와 함께 정리한다.

전처리 단계와 #include, #define
C 컴파일 과정은 크게 네 단계로 나뉜다. 전처리기(Preprocessor) → 컴파일러(Compiler) → 어셈블러(Assembler) → 링커(Linker) 순이며, 가장 먼저 동작하는 전처리기는 소스 코드를 단순한 텍스트로 본다. 즉 전처리기는 C언어의 문법을 이해하지 않으며, 단지 #으로 시작하는 지시문을 만나면 그 지시에 따라 코드를 치환하거나 끼워 넣는 일만 한다. 이러한 단순성 덕분에 전처리기는 빠르고 강력하지만, 동시에 사용자가 의도하지 않은 결과를 만들기도 쉬워 사용에 주의가 필요하다.
가장 먼저 익히는 지시문은 #include이다. 이는 지정된 파일의 내용을 그 위치에 그대로 복사해 끼워 넣으라는 명령이다. 헤더 파일을 가져오는 표준 패턴으로, 표기 방식에 따라 검색 위치가 달라진다.
#include <stdio.h> // 시스템 표준 헤더 디렉터리에서 검색
#include "myheader.h" // 현재 디렉터리부터 검색
꺾쇠괄호는 컴파일러에 설정된 표준 헤더 경로에서 파일을 찾고, 큰따옴표는 현재 소스 파일과 같은 디렉터리부터 검색한 뒤 표준 경로로 확장한다. 따라서 사용자가 직접 만든 헤더는 큰따옴표를 사용하는 것이 관례이다. 또한 같은 헤더가 여러 번 포함되는 다중 포함 문제를 막기 위해 모든 헤더 파일은 다음과 같은 헤더 가드를 두는 것이 표준이다.
// myheader.h
#ifndef MYHEADER_H
#define MYHEADER_H
void greet(void);
#endif
헤더 가드의 핵심 도구가 바로 두 번째 지시문 #define이다. #define은 식별자에 어떤 텍스트를 연결해두는 매크로 정의이며, 이후 코드에서 그 식별자를 만나면 전처리기가 정의된 텍스트로 단순 치환한다. 가장 단순한 형태는 매크로 상수이며, 의미 없는 숫자에 이름을 붙여 가독성을 크게 높인다.
#define PI 3.14159
#define MAX_SIZE 100
double area = PI * r * r;
int arr[MAX_SIZE];
전처리 단계가 끝나면 코드 안의 PI는 모두 3.14159로, MAX_SIZE는 100으로 치환된다. 따라서 변수와 달리 메모리를 차지하지 않고 컴파일 결과 코드에 직접 박힌다는 특성이 있다. 이러한 단순 치환 방식은 매우 빠르고 가볍지만, 자료형 검사가 없어 잘못 사용하면 디버깅이 어려운 오류로 이어질 수 있어, 최근에는 가능하면 const 변수나 enum을 우선 사용하는 흐름이 자리잡았다.
함수 매크로와 조건부 컴파일
#define은 단순 상수뿐만 아니라 인자를 받는 함수 매크로도 정의할 수 있다. 함수 매크로는 호출 부분이 그대로 코드에 펼쳐지므로 함수 호출의 오버헤드가 없고, 자료형에 구애받지 않아 다양한 자료형에 동일한 로직을 적용할 수 있다는 장점이 있다. 가장 대표적인 예가 두 값을 비교하는 MAX 매크로이다.
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = MAX(3, 7); // ((3) > (7) ? (3) : (7)) → 7
double y = MAX(1.5, 2.7); // 자료형 무관
여기서 가장 중요한 원칙이 모든 인자와 전체 식을 괄호로 감싸는 것이다. 괄호를 빠뜨리면 매크로가 펼쳐지는 위치에서 의도와 전혀 다른 연산 순서로 해석될 수 있다. 다음의 SQUARE 매크로가 대표적인 함정이다.
#define SQUARE(x) x * x // 잘못된 정의 (괄호 없음)
int a = SQUARE(2 + 3);
// 치환 결과: 2 + 3 * 2 + 3 = 11 (의도: 25)
#define SQUARE2(x) ((x) * (x)) // 안전한 정의
int b = SQUARE2(2 + 3); // ((2+3) * (2+3)) = 25
이러한 매크로 부작용은 정보처리기사 실기에서 단골 출제 포인트이며, 괄호 한 쌍의 유무로 결과가 달라지는 코드가 자주 등장한다. 또한 함수 매크로는 인자에 부작용이 있는 식(예: i++)을 넘기면 인자가 여러 번 평가되어 기대하지 않은 동작을 일으키므로, 이런 경우에는 매크로 대신 인라인 함수를 사용하는 것이 안전하다.
조건부 컴파일은 특정 조건이 참일 때만 코드의 일부를 컴파일에 포함시키는 강력한 기능이다. #ifdef, #ifndef, #if, #else, #elif, #endif 같은 지시문이 사용되며, 디버그 빌드와 릴리즈 빌드를 분리하거나 운영체제별로 다른 코드를 작성할 때 표준적으로 활용된다.
#define DEBUG
#ifdef DEBUG
printf("디버그 모드: x = %d\n", x);
#endif
#if defined(__linux__)
printf("리눅스 환경\n");
#elif defined(_WIN32)
printf("윈도우 환경\n");
#else
printf("기타 환경\n");
#endif
DEBUG가 정의된 경우에만 디버그 출력 코드가 컴파일되므로, 릴리즈 빌드에서는 단순히 #define DEBUG 한 줄만 지우면 디버그 코드 전부가 자동으로 제거된다. 이 방식은 코드 본체를 손대지 않고 빌드 옵션만으로 동작을 바꿀 수 있어, 대규모 프로젝트의 유지 관리에서 매우 중요한 역할을 한다.
정보처리기사 실기 빈출 매크로 예제
정보처리기사 실기에서 전처리기와 매크로는 주로 단순 치환의 결과를 손으로 추적하는 형태로 출제된다. 다음 세 가지 패턴이 핵심이다.
유형 1: 매크로 상수 치환.
#define N 5
#define M (N + 2)
int main() {
int arr[M];
for (int i = 0; i < M; i++) arr[i] = i;
printf("%d\n", arr[6]); // 6
printf("%d\n", M * 2); // 14
return 0;
}
M은 (N + 2)로 치환되므로 M * 2는 (5 + 2) * 2가 되어 14가 된다. 만약 매크로 정의에서 괄호를 빼면 N + 2 * 2가 되어 9가 출력되는 함정이 있으므로, 정의 시 괄호의 위치를 정확히 따라가야 한다.
유형 2: 함수 매크로의 부작용.
#define MUL(x, y) x * y
int main() {
int a = MUL(2 + 3, 4); // 2 + 3 * 4 = 14 (의도: 20)
int b = MUL(2, 3 + 4); // 2 * 3 + 4 = 10 (의도: 14)
printf("%d %d\n", a, b); // 14 10
return 0;
}
괄호가 없는 함수 매크로의 위험성을 정확히 묻는 패턴이다. 치환을 그대로 손으로 펼친 뒤 연산 우선순위를 적용하는 절차를 정확히 따라가야 한다.
유형 3: 조건부 컴파일과 #ifdef.
#define VERSION 2
int main() {
#if VERSION == 1
printf("v1\n");
#elif VERSION == 2
printf("v2\n");
#else
printf("etc\n");
#endif
return 0;
}
// 출력: v2
VERSION 매크로의 값에 따라 어느 분기가 컴파일에 포함되는지 추적하는 기본 패턴이다. 조건이 거짓인 분기는 컴파일러에 전달되지 않으므로 출력에도 나타나지 않는다.
이 세 가지 패턴을 정확히 손으로 추적하면 매크로 영역의 시험 대비는 충분하다. 결국 전처리기는 컴파일 전에 텍스트를 단순 치환하는 도구이며, 그 단순함이 곧 강력함과 위험성의 양면이 된다. 매크로 상수에는 const 변수를, 함수 매크로에는 인라인 함수를 우선 검토하고, 꼭 매크로를 써야 한다면 모든 인자와 전체 식을 괄호로 감싸는 원칙을 지키면 된다. 이 토대 위에서 다음 글의 비트 연산과 종합 정리, 그리고 자료구조의 컴파일 단위 구성으로 자연스럽게 이어진다.
메타 디스크립션: C언어의 전처리 단계와 #include·#define·#ifdef 지시문의 동작 방식, 함수 매크로의 괄호 사용 원칙과 부작용, 조건부 컴파일의 활용, 그리고 정보처리기사 실기 빈출 3패턴을 코드 예시와 함께 정리합니다.