# C언어 (6)

## 1\. 전처리

### 1-1. 전처리란?

C 소스 코드가 실행 파일이 되기까지의 과정을 다시 떠올려보자.

```plaintext
소스파일(.c) → [전처리] → 전처리 파일(.i) → [컴파일] → 목적파일(.o) → [링크] → 실행파일
```

**전처리(Preprocessing)** 는 컴파일이 시작되기 전에 소스 코드를 먼저 가공하는 단계다. `#`으로 시작하는 지시문들을 처리하는 것이 이 단계에서 일어나는 일이다. `#include`로 헤더 파일의 내용을 붙여넣고, `#define`으로 정의된 매크로를 치환하고, `#ifdef` 같은 조건부 컴파일 지시자를 평가한다.

전처리가 끝나면 `#`으로 시작하는 지시문은 전부 사라지고, 순수한 C 코드만 남는다. 이 결과물이 전처리 파일(`.i`)이고, 이것이 컴파일러에 전달된다.

### 1-2. 전처리의 규칙

전처리 지시문에는 몇 가지 규칙이 있다.

전처리 지시문은 항상 `#`으로 시작한다. `#` 앞에 공백은 있어도 되지만, 일반적으로 줄 맨 앞에 쓴다.

전처리 지시문은 **한 줄에 하나씩** 작성한다. 세미콜론으로 끝나지 않는다. C 코드의 문장은 `;`으로 끝나지만, 전처리 지시문은 줄바꿈이 곧 끝이다. 한 줄에 다 쓰기 어려울 때는 줄 끝에 `\`를 붙여서 다음 줄로 이어쓸 수 있다.

```c
#define LONG_MACRO(x, y) \
    ((x) > (y) ? (x) : (y))
```

전처리 지시문은 코드 어디에든 올 수 있지만, 관례적으로 파일 상단에 모아두는 것이 일반적이다.

### 1-3. 전처리기 지시자의 종류

자주 사용하는 전처리기 지시자를 정리하면 이렇다.

`#include` 는 다른 파일의 내용을 현재 위치에 삽입한다. `<stdio.h>`처럼 꺾쇠로 감싸면 시스템 헤더 경로에서 찾고, `"myheader.h"`처럼 따옴표로 감싸면 현재 디렉토리에서 먼저 찾는다.

`#define` 은 매크로를 정의한다. 단순 상수 치환부터 함수처럼 동작하는 매크로까지 만들 수 있다.

`#undef` 는 이미 정의된 매크로를 해제한다.

`#ifdef`, `#ifndef`, `#if`, `#elif`, `#else`, `#endif` 는 조건부 컴파일을 위한 지시자들이다.

`#pragma` 는 컴파일러에게 특별한 지시를 내린다. 컴파일러마다 지원하는 내용이 다르다.

* * *

## 2\. 매크로

### 2-1. 매크로 상수

`#define`으로 이름에 값을 대응시키는 것이 매크로 상수다. 전처리 단계에서 해당 이름이 등장하는 곳을 전부 지정한 값으로 **텍스트 치환** 한다.

```c
#define PI 3.14159
#define MAX_SIZE 100
#define MESSAGE "Hello, World!"

int main(void) {
    double area = PI * 5 * 5;
    int arr[MAX_SIZE];
    printf("%s\n", MESSAGE);
    return 0;
}
```

전처리 후에는 이렇게 바뀐다.

```c
int main(void) {
    double area = 3.14159 * 5 * 5;
    int arr[100];
    printf("%s\n", "Hello, World!");
    return 0;
}
```

`PI`, `MAX_SIZE`, `MESSAGE`라는 이름이 전부 대응하는 값으로 치환된 것이다. 매크로 이름은 관례적으로 **대문자** 로 쓴다. 일반 변수와 구분하기 위해서다.

### 2-2. 매크로 상수를 사용하면 좋은 점

**매직 넘버를 제거한다.** 코드 곳곳에 `100`이라는 숫자가 흩어져 있으면, 그것이 배열 크기인지 점수 기준인지 알 수 없다. `MAX_SIZE`라는 이름을 붙이면 의도가 명확해진다.

**유지보수가 쉬워진다.** 배열 크기를 200으로 바꿔야 할 때, `#define MAX_SIZE 100`을 `#define MAX_SIZE 200`으로 한 줄만 수정하면 된다. `100`이 등장하는 모든 곳을 찾아 바꿀 필요가 없다.

**배열 크기 지정에 사용할 수 있다.** C에서 `const int`는 진정한 컴파일 타임 상수가 아니라서 배열 크기로 사용하지 못하는 경우가 있다. 하지만 `#define` 매크로는 전처리 단계에서 숫자로 치환되므로 배열 크기로 사용할 수 있다.

```c
#define SIZE 10
int arr[SIZE];       // OK

const int size = 10;
// int arr2[size];   // C89에서는 에러
```

### 2-3. 매크로의 특징

매크로는 **텍스트 치환** 이라는 점이 핵심이다. 컴파일러가 처리하는 것이 아니라 전처리기가 단순히 텍스트를 바꿔치기하는 것이다. 이 때문에 주의해야 할 점들이 있다.

**매크로 함수** 를 만들 수도 있다.

```c
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))

int result = SQUARE(5);      // ((5) * (5)) → 25
int bigger = MAX(10, 20);    // ((10) > (20) ? (10) : (20)) → 20
```

매개변수를 괄호로 감싸는 것이 매우 중요하다. 괄호가 없으면 연산자 우선순위 때문에 의도하지 않은 결과가 나올 수 있다.

```c
#define BAD_SQUARE(x) x * x

int result = BAD_SQUARE(3 + 2);
// 치환 결과: 3 + 2 * 3 + 2
// 의도: 25, 실제: 11
```

`BAD_SQUARE(3 + 2)` 는 `3 + 2 * 3 + 2`로 치환된다. 곱셈이 먼저 계산되어 `3 + 6 + 2 = 11`이 된다. `((x) * (x))`로 썼다면 `((3 + 2) * (3 + 2))` = 25로 의도한 대로 동작한다.

매크로는 **타입 검사를 하지 않는다.** 함수는 매개변수와 반환값의 타입을 컴파일러가 검사하지만, 매크로는 텍스트 치환이므로 어떤 타입이든 들어갈 수 있다. 이것이 장점이 될 수도 있지만(제네릭하게 사용 가능), 타입 관련 버그를 잡기 어렵게 만들 수도 있다.

매크로는 **디버깅이 어렵다.** 전처리 단계에서 이미 치환되어 사라지기 때문에, 디버거에서 매크로 이름으로 중단점을 잡거나 값을 확인할 수 없다.

매크로 함수의 장점은 **함수 호출 오버헤드가 없다** 는 것이다. 일반 함수는 호출 시 스택 프레임 생성, 인자 복사, 점프 등의 비용이 발생하지만, 매크로는 코드가 직접 삽입되므로 이런 비용이 없다. 아주 작고 자주 호출되는 로직에는 매크로가 유리할 수 있다. 다만 현대 컴파일러는 작은 함수를 자동으로 인라인 처리하므로, 성능 차이는 대부분의 경우 미미하다.

* * *

## 3\. 조건부 컴파일

조건부 컴파일은 **특정 조건에 따라 코드의 일부를 컴파일에 포함시키거나 제외하는 기능** 이다. `if-else`와 비슷하지만, 런타임이 아니라 **전처리 단계** 에서 평가된다. 조건에 맞지 않는 코드는 아예 컴파일러에 전달되지 않는다.

### 3-1. 지시자의 형식

#### #ifdef / #ifndef

`#ifdef` 는 매크로가 정의되어 있으면 해당 코드를 포함한다. `#ifndef` 는 반대로 정의되어 있지 않으면 포함한다.

```c
#define DEBUG

#ifdef DEBUG
    printf("디버그 모드입니다.\n");
    printf("x = %d\n", x);
#endif
```

`DEBUG`가 정의되어 있으므로 `printf`문이 컴파일에 포함된다. `#define DEBUG`를 지우면 이 코드는 아예 컴파일되지 않는다. 디버깅용 출력을 넣었다 뺐다 할 때 유용하다. 일일이 주석 처리할 필요 없이 `#define` 한 줄만 제어하면 된다.

#### #if / #elif / #else / #endif

더 세밀한 조건 분기가 필요하면 `#if`를 사용한다.

```c
#define VERSION 3

#if VERSION == 1
    printf("버전 1\n");
#elif VERSION == 2
    printf("버전 2\n");
#elif VERSION >= 3
    printf("버전 3 이상\n");
#else
    printf("알 수 없는 버전\n");
#endif
```

`#if`의 조건에는 정수 상수 표현식만 올 수 있다. 변수나 함수 호출은 사용할 수 없다. 전처리 단계에서는 아직 변수가 존재하지 않기 때문이다.

`defined()` 연산자를 `#if`와 함께 쓰면 `#ifdef`와 같은 효과를 낸다. 여러 조건을 논리 연산자로 조합할 때 유용하다.

```c
#if defined(WINDOWS) && defined(DEBUG)
    printf("Windows 디버그 모드\n");
#elif defined(LINUX)
    printf("Linux 환경\n");
#endif
```

#### 헤더 가드 (Include Guard)

조건부 컴파일의 가장 대표적인 활용 사례가 **헤더 가드** 다. 같은 헤더 파일이 여러 번 `#include`되면 구조체나 함수 선언이 중복되어 컴파일 에러가 발생한다. 헤더 가드는 이 문제를 방지한다.

```c
// student.h
#ifndef STUDENT_H
#define STUDENT_H

typedef struct {
    char name[20];
    int age;
} Student;

void printStudent(const Student *s);

#endif
```

처음 `#include "student.h"`가 실행되면 `STUDENT_H`가 정의되어 있지 않으므로 `#ifndef` 조건이 참이 되어 내용이 포함된다. 동시에 `#define STUDENT_H`로 매크로가 정의된다. 두 번째 `#include "student.h"`가 실행되면 이미 `STUDENT_H`가 정의되어 있으므로 `#ifndef` 조건이 거짓이 되어 전체 내용이 건너뛰어진다.

헤더 파일을 작성할 때는 항상 헤더 가드를 넣는 것이 기본이다. 일부 컴파일러는 `#pragma once`라는 비표준 지시자로 같은 효과를 제공한다.

```c
// student.h (pragma once 방식)
#pragma once

typedef struct {
    char name[20];
    int age;
} Student;
```

`#pragma once` 는 간결하지만 표준이 아니므로, 모든 컴파일러에서 동작을 보장하지는 않는다. 이식성이 중요한 코드에서는 `#ifndef` 방식을 사용하는 것이 안전하다.

#### 플랫폼별 분기

조건부 컴파일은 하나의 소스 코드로 여러 운영체제를 지원할 때도 사용된다.

```c
#ifdef _WIN32
    #include <windows.h>
    #define CLEAR "cls"
#elif defined(__linux__)
    #include <unistd.h>
    #define CLEAR "clear"
#elif defined(__APPLE__)
    #include <unistd.h>
    #define CLEAR "clear"
#endif

system(CLEAR);  // 플랫폼에 맞는 화면 지우기 명령
```

Windows에서는 `_WIN32`가 자동으로 정의되고, Linux에서는 `__linux__`가, macOS에서는 `__APPLE__`이 정의된다. 같은 코드를 컴파일하더라도 플랫폼에 따라 다른 코드가 포함되는 것이다.

Visual Studio에서 `scanf_s`를 쓰고 macOS에서 `scanf`를 써야 하는 상황도 이 방식으로 해결할 수 있다.

```c
#ifdef _MSC_VER
    scanf_s("%d", &x);
#else
    scanf("%d", &x);
#endif
```

* * *

## 마무리

전처리기는 컴파일이 시작되기도 전에 소스 코드를 가공하는 도구다. 매크로 상수로 매직 넘버를 제거하고 유지보수를 쉽게 만들 수 있고, 조건부 컴파일로 디버그 코드를 관리하거나 플랫폼별 분기를 처리할 수 있다. 다만 매크로는 텍스트 치환이라는 점을 항상 기억해야 한다. 괄호를 빼먹으면 찾기 어려운 버그가 생기고, 타입 검사도 되지 않는다. 단순한 상수 정의와 헤더 가드에는 매크로가 적합하지만, 복잡한 로직은 함수로 작성하는 것이 안전하다.
