-
Notifications
You must be signed in to change notification settings - Fork 1
00_06.ThePreprocessor
우리는 '빌드 시스템'에서 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
지시문은 소스 파일에서 자주 사용되는 상수를 만들거나, 함수와 비슷한 기능을 하는 매크로 (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
지시문은 전처리기에게 "주어진 헤더 파일을 찾고, 그 파일의 내용을 복사해서 소스 파일에 붙여넣어줘"라는 명령을 내린다.
// <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
- 오픈 소스 소프트웨어란?
- Git과 버전 관리 시스템
- GitHub를 이용한 저장소 호스팅
- 프로젝트의 기여 및 관리
- 라이브러리 소개
- 개발 환경 구축
- 첫 번째 프로그램
- 게임 창과 커서 관리
- 프레임, 시간과 타이머
- 픽셀, 선분과 기본 도형
- 마우스와 키보드 입력
- 벡터 글꼴과 비트맵 글꼴
- 이미지와 텍스처의 사용
- 카메라와 렌더 텍스처
- 충돌 감지와 충돌 해결
- 효과음과 음악 재생
- 그 외 유용한 함수