Skip to content

00_06.ThePreprocessor

jdeokkim edited this page Apr 26, 2022 · 3 revisions

전처리기

우리는 '빌드 시스템'에서 C 프로그램의 빌드 과정이 전처리 (preprocessing), 컴파일링 (compiling), 링킹 (linking)의 3단계로 이루어진다는 것을 배웠다. 이제 컴파일러의 전처리기 (preprocessor)라는 프로그램이 #define, #include 등의 지시문 (preprocessor directive)을 어떻게 처리하는지 알아보자.

전처리기가 수행할 동작을 알려주는 명령을 전처리기 지시문이라고 하며, 모든 전처리기 지시문은 #define, #include, #error처럼 #으로 시작한다. 특히 #define 지시문은 60, 1.5f 등의 상수 (constant)와 자주 사용되는 표현식 (expression)을 나타내는 이름인 매크로 (macro)를 만들 때 사용한다.

예를 들어, 우리가 #define 매크로를 사용하여 아래와 같은 프로그램을 만들었다고 해보자.

// 우리는 `N`이라는 매크로 변수를 정의하였다.
#define N 100

/* ... */

int main(void) {
    // 여기서 `N`이 사용된다.
    for (int i = 0; i < N; i++)
        printf("정신나갈것같애!\n");

    return 0;
}

컴파일 과정에서, 전처리기는 제일 먼저 소스 파일에서 매크로를 사용한 부분을 #define에 정의된 상수 값으로 대체하는데, 이렇게 전처리기가 매크로를 #define에 정의된 상수나 표현식으로 대체하는 것을 매크로의 확장 (expand)이라고 한다.

int main(void) {
    // 전처리기는 소스 파일의 모든 `N`을 100으로 대체한다.
    for (int i = 0; i < 100; i++)
        printf("정신나갈것같애!\n");

    return 0;
}

#define 지시문

#define 지시문은 소스 파일에서 자주 사용되는 상수를 만들거나, 함수와 비슷한 기능을 하는 매크로 (parameterized macro / function-like macro)를 만들 때 사용한다.

#define <식별자> (대체할 내용)

#define 지시문을 사용하여 상수를 정의하면, 다른 사람이 소스 파일의 내용을 쉽게 이해할 수 있어 프로젝트의 협업에 도움이 되고, 상수 값을 수정할 일이 있으면 그 매크로만 수정하면 되기 때문에 프로그램 수정에 걸리는 시간을 줄일 수 있다.

// 게임의 최대 FPS를 나타내는 매크로 변수.
#define TARGET_FPS     60

// 게임 창의 가로 길이를 나타내는 매크로 변수.
#define SCREEN_WIDTH   1024

// 게임 창의 세로 길이를 나타내는 매크로 변수.
#define SCREEN_HEIGHT  768

#define 지시문을 사용하면 함수와 비슷한 기능을 수행하는 매크로도 만들 수 있다.

#define MAX(x, y)      ((x) > (y) ? (x) : (y))

#include 지시문

#include 지시문은 전처리기에게 "주어진 헤더 파일을 찾고, 그 파일의 내용을 복사해서 소스 파일에 붙여넣어줘"라는 명령을 내린다.

// <stdio.h> 헤더 파일을 이 소스 파일에 '포함' (include)한다.
#include <stdio.h>

#define N 100

/* ... */

int main(void) {
    for (int i = 0; i < 100; i++)
        printf("정신나갈것같애!\n");

    return 0;
}

전처리 과정이 실행되면, 우리가 작성한 소스 파일은 아래와 같이 수정된다.

#ifndef _STDIO_H
# if !defined _ISOMAC && defined _IO_MTSAFE_IO
#  include <stdio-lock.h>
# endif

/* Workaround PR90731 with GCC 9 when using ldbl redirects in C++.  */
# include <bits/floatn.h>
# if defined __cplusplus && __LDOUBLE_REDIRECTS_TO_FLOAT128_ABI == 1
#  if __GNUC_PREREQ (9, 0) && !__GNUC_PREREQ (9, 3)
#    pragma GCC system_header
#  endif
# endif

# include <libio/stdio.h>
# ifndef _ISOMAC

#  define _LIBC_STDIO_H 1
#  include <libio/libio.h>

/* 그 외 <stdio.h>의 모든 내용... */

int main(void) {
    for (int i = 0; i < 100; i++)
        printf("정신나갈것같애!\n");

    return 0;
}

헤더 파일의 재정의

그러면 여기서 한번 생각해보자. 만약 똑같은 헤더 파일을 여러 번 #include하면 어떻게 될까? 당연히 똑같은 헤더 파일의 내용이 여러 번 복사될 것이다. 아래 예제를 통해 이것이 왜 문제가 되는지 확인해보자.

/* common.h */

#include "geometry.h"

/* ... */
/* geometry.h */

/* 2차원 벡터를 나타내는 구조체. */
typedef struct {
    float x;
    float y;
} Vector2;

/* ... */
/* main.c */

#include "common.h"
#include "geometry.h"

/* ... */

int main(void) {
    /* ... */
}

여기서 main.c 소스 파일은 전처리 과정에서 아래와 같이 수정될 것이다.

/* main.c */

/* `#include "common.h"` => `#include "geometry.h" */
typedef struct {
    float x;
    float y;
} Vector2;

/* `#include "geometry.h"` */
typedef struct {
    float x;
    float y;
} Vector2;

/* ... */

int main(void) {
    /* ... */
}

이렇게 되면 "geometry.h" 헤더 파일이 여러 번 복사되고, Vector2가 한 번 이상 정의되었기 때문에, 컴파일 오류가 발생한다. 그렇다면 이러한 문제를 어떻게 해결할 수 있을까? 바로 이럴 때 필요한 것이 헤더 가드 (header guard, include guard)라는 전처리기 지시문 코드이다.

헤더 가드는 아래와 같이 헤더 파일을 세 줄의 전처리기 지시문 (#ifndef, #define, #endif)으로 감싸는 것을 뜻한다. 여기서 #ifndef는 일단 "이 매크로가 정의되어 있지 않으면"이라는 뜻이라는 것만 알아두자.

/* geometry.h */

/*
    `GEOMETRY_H` 매크로가 정의되어 있지 않으면, 
    `GEOMETRY_H` 매크로를 새로 정의한다.
*/
#ifndef GEOMETRY_H
#define GEOMETRY_H

/* 2차원 벡터를 나타내는 구조체. */
typedef struct {
    float x;
    float y;
} Vector2;

/* ... */

#endif /* GEOMETRY_H */

도대체 이 세 줄이 어떻게 여러 번의 #include를 방지한다는 것일까? 답은 간단하다. 만약 GEOMETRY_H 매크로가 정의되어 있지 않다면, main.c 소스 파일이 아직 #include "geometry.h"를 하지 않았다는 것을 의미하므로, "geometry.h"의 내용이 main.c에 복사된다. 만약 GEOMETRY_H 매크로가 이미 정의되어 있다면, 그것은 #include "geometry.h"를 앞에서 했다는 뜻이기 때문에, 전처리기가 "geometry.h"의 내용을 복사하지 않게 되는 것이다.

앞으로 우리가 만들 모든 헤더 파일은 헤더 가드를 사용할 것이기 때문에, 헤더 가드가 무엇인지 꼭 기억하도록 하자.


조건부 지시문

이제 소스 파일을 주어진 매크로 조건에 따라 수정하는 조건부 지시문 (conditional directives)에 대해 알아보자. 조건부 지시문에는 #if, #ifdef, #ifndef, #elif, #else, #endif 등이 있다. #if로 시작하는 모든 조건부 지시문은 반드시 #endif로 끝나야 한다.

#if <상수 표현식>

#if#endif의 예시를 확인해보자. 아래 예시에서 DEBUG의 값은 1이기 때문에, do_something() 함수를 호출하면 "do_something(): 함수가 호출되었습니다." 메시지도 같이 출력될 것이다.

/* header.h */

// 헤더 가드로 헤더 파일이 여러 번 포함되는 것을 방지한다.
#ifndef HEADER_H
#define HEADER_H

#define DEBUG 1

void do_something(void) {
// 이 `#if`문에 주목하자.
#if DEBUG
    printf("do_something(): 함수가 호출되었습니다.");
#endif

    printf("...");
}

#endif // HEADER_H

#ifdef#ifndef는 주어진 매크로의 값과 상관없이 매크로가 정의되어 있는지를 확인하는 지시문이다.

#ifdef <매크로의 식별자>
#ifndef <매크로의 식별자>
/* header.h */

// `HEADER_H` 매크로가 정의되어 있는지 확인한다.
#ifndef HEADER_H
#define HEADER_H

#endif // HEADER_H

Clone this wiki locally