Chapter 14. The Preprocessor
K.N.KING C PROGRAMMING A Modern Approach
14.1 How the Preprocessor Works
Macro definition
#define
marco를 정의하기 위한 directive이다. Preprocessor는 #define을 통해 정의된 macro의 이름(=syntax)과 정의(=semantic)를 저장하고, 프로그램에서 매크로가 사용될 때 정의된 값으로 대체한다.
File inclusion
#include
특정 파일의 content를 해당 프로그램에 포함시킨다.
Conditional compilation
#if
, #ifdef
, ifndef
, #elif
, #else
특정 조건에 따라 코드의 각 부분들이 프로그램에서 포함될지 제외될지를 결정한다. preprocessing 단계에서 이루어진다.
14.2 Preprocessing Directives
Directive 작성의 몇 가지 rule들
- Directive는 반드시
#
로 시작해야 한다.#
앞에 white space만 있다면 line의 시작이 꼭 아니어도 되지만,#
뒤에 나오는 부분은 반드시 그 directive의 정보에 대한 것이어야 한다. # define N 100
과 같이, 사이사이에 space나 tab으로 분리되어도 괜찮다.- Directive는
\n
이 나올 때 끝난다. 여러 줄에 걸쳐서 쓰고 싶으면.\
로 줄바꿈을 해서 쓴다. - Directive는 프로그램의 어느 부분에서든 나와도 된다. 꼭 맨 앞일 필요는 없다.
- Directive 오른쪽에 주석은 써도 된다. 설명을 쓰는 것이 가독성에 좋다.
14.3 Macro Definition
Simple Macros
아무 파라미터 없이, 단순한 replacement를 위한 macro이다.
#define identifier replacement-list
특정 상수를 정의하거나, 자주 쓰이는 문구를 축약하는 데 좋다. 프로그램의 가독성을 높이는 데 도움이 되고, 프로그램 수정을 쉽게 만들어 주는 장점이 있다.
Parameterized Macros
#define identifier(x1, x2, ..., xn) replacement-list
마치 함수 같은 형태를 가지고 있다.
Preprocessor는 프로그램에서 저 형태의 코드를 발견했을 때, 예를 들어 #define identifier(y1, y2, ..., yn)
이 있으면 각 y1 ~ yn을 replacement-list에 정의된
것들로 치환하게 된다.
(예시)
#define MAX(x, y) ((x)>(y) ? (x) : (y))
i = MAX(j+k, m-n);
/* i = ((j+k)>(m-n) ? (j+k) : (m-n)) 과 동일 */
간편한 함수를 하나 만드는 것과 사실상 동일하다.
parameter가 없는 parameterized macro도 만들 수 있다.
#define getchar() getc(stdin)
parameterized macro의 장점
- function에 비해 빠르다. Run-time overhead가 없기 때문이다.
- Generic하다. 함수의 경우 argument의 타입이 정해져 있지만, macro는 그렇지 않다.
parameterized macro의 단점
- 컴파일 후 source code가 아주 길어질 수 있다. 특히 nested되었을 경우 함수를 쓰는 것보다 훨씬 코드가 길어진다.
- Argument의 type-check가 이루어지지 않는다. preprocessor가 잡아내지도 못하고, type conversion도 받지 못한다.
- 함수 포인터와 다르게 macro는 포인터를 가질 수 없다.
- 특정 상황에서 unexpected behavior를 보일 수 있는데 :
#define MAX(x, y) ((x)>(y) ? (x) : (y)) int n = MAX(i++, j);
이런 코드의 경우, 함수로 구현되었을 때는 i++가 당연히 한 번만 수행되지만, 매크로로 정의하면 (x)가 등장하는 두 군데에서 모두 ++ 연산을 하게 된다.
The # Operator
# operator는 “stringization”을 하는 operator이다.
사용법은 다음과 같다.
#define PRINT_INT(n) printf(#n " = $%d\n", n)
PRINT_INT(i/j);
위 코드는 preprocessor를 거치면 아래와 같이 바뀌게 된다.
#define PRINT_INT(n) printf(#n " = %d\n", n)
printf("i/j" " = %d\n", n);
macro의 replacement-list에 #n이 있고, preprocessor는 코드의 PRINT_INT()
를 보고 이곳이구나! 하고 n 대신 i/j로 replace하게 된다.
이때 양 옆으로 “ “ 가 감싸진다.
그리고 printf("string1" "string2");
는 알아서 printf("string1", "string2");
로 바뀌기 때문에 콤마는 없어도 된다.
The ## Operator
## operator는 두 token을 하나의 token으로 paste해 주는 역할을 한다. 예시를 보자.
#define MK_ID(n) i##n
#define MK_KD(n) k##n
int MK_ID(1)=3;
int MK_KD(2)=4;
Preprocessor는 코드의 MK_ID를 보고 이곳이구나! 하고 작업을 시작한다. 두 token i와 n이 있다. 이중 n만 유저가 써준 1을 가져와서 int i1=3;
이 된다.
MK_KD의 경우도 int k2=4;
가 된다. 변수 이름을 규칙적으로 지을 때 쓸모가 있어 보이나.. 사실 ## operator는 잘 쓰이지 않는다고 한다.
대신 동일한 Generic한 함수를 구현하는 데 유용하게 사용된다.
#define GENERIC_MAX(type) type type_##max(type x, type y) \
{ \
return x>y ? x : y; \
}
GENERIC_MAX(float);
위 macro를 살펴 보면, parameter type으로 뭔가가 들어오면 replacement-list의 type들이 전부 그것으로 대체된다. ## operator가 있는 부분은 concat되어 대체된다.
따라서 preprocessor의 처리가 끝나면 아래와 같이 변경된다.
#define GENERIC_MAX(type) type type_##max(type x, type y) \
{ \
return x>y ? x : y; \
}
float float_max(float x, float y)
{
return x>y ? x : y;
}
parameter ‘type’에 넣고 싶은 자료형을 넣으면 해당 자료형에 대한 그 함수를 만들 수 있는 것이다.
General Properties of Macros
- Macro의 replacement-list에는 다른 macro가 들어갈 수 있다.
#define PI 3.14 #define PI_SQUARE (PI*PI)
- Preprocessor는 token이 통째로 독립적으로 있을 때만 replace한다. 문자열이나, 상수나, 그런 곳에 embedded된 부분은 대체하지 않는다.
#define print 5555 int main() { printf("%d %s", print, "print"); }
위 코드에서 정의된 macro는 print
라는 토큰을 5555
로 대체할텐데, 코드에서 printf
의 일부분인 print나, "print"
의 일부분인 print를 5555로 대체하지는 않는다. 오직 통째로 token인 %d에 해당하는 print만 5555로 대체되어서, 5555 print
라는 출력을 하게 된다.
- Macro는 선언된 이후, 파일 끝까지 쭉 적용된다. 해제하기 위해서는
#undef identifiet
를 사용하면 된다.
- 매크로는 다르게 두 번 선언될 수 없다. 이때 ‘다르다’는 것은 공백이나 이런 형식적인 것 말고, token이나 parameter이나 replacement-list가 다른 경우를 말한다.
Parentheses in Macro Definition
Macro를 선언할 때는 과하다 싶을 정도로 모든 곳에 괄호를 쳐 주는 것이 좋다. Unexpected behavior가 일어나기 정말 쉬운 부분이기 때문이다.
예를 들어,
#define SCALE(x) (x*10)
int a = 3;
int b = SCALE(a+5);
이렇게 매크로를 정의했다고 하자. b의 값은 무엇일까?
원하는 결과는 (3+5)*10=80이겠지만 그렇지 않다.
#define SCALE(x) (x*10)
int a = 3;
int b = (3+5*10);
실제로는 위와 같이 replace되어서, b에는 53이 저장되게 된다. 따라서 SCALE(a+5);
가 아닌, SCALE((a)+5);
와 같이 괄호를 꼭 쳐 줘야 한다.
Creating Longer Macros
여러 statement를 동시에 macro로 묶을 수 있다.
#define ECHO(n) (gets(s), puts(s))
ECHO("hi!");
이런 표현은 아무 문제가 없다. (사실 gets()는 컴파일 해 보면 쓰지 말라고 나온다. 좀 옛날 책이라…)
하지만 {}로 묶어 compound statement로 정의하면 문제가 되는 부분이 생긴다.
#define ECHO(n) {gets(s); puts(s);}
if(s=="hi!")
ECHO(s);
else
printf("bye!");
이는 아래와 같이 바뀌게 된다.
#define ECHO(n) {gets(s); puts(s);}
if(s=="hi!")
{gets(s); puts(s);};
else
printf("bye!");
세미콜론의 존재 때문에, 마치 첫 if문에서 문장이 끝나고, else가 속한 곳이 없어져 컴파일 에러가 나게 된다.
이것을 막으려면 ECHO(s)
에 세미콜론을 빼야 하는데, 통일된 문법을 해치는 것 같아 보여서 좋지 않다.
막는 방법은
do { gets(s); puts(s); } while(0)
위와 같이 do-while문을 사용하면 된다. 그러면 자연스럽게 ECHO(s);
에 세미콜론이 제 역할을 하게 된다. 사실 쓸 일은 없을 것 같다.
Predefined Macros
C에는 기본으로 탑재된 매크로들이 있다.
Name | Description |
---|---|
__LINE__ |
해당 파일에서 __LINE__ 이 위치하는 곳의 줄 번호 |
__FILE__ |
컴파일 되는 파일의 이름 |
__DATE__ |
컴파일 한 날짜. 형식은 Mmm dd yyyy |
__TIME__ |
컴파일 한 시각, 형식은 hh:mm:ss |
__STDC__ |
컴파일러가 C standard(C89, C99)와 호환 되면 1 |
#include <stdio.h>
int main()
{
printf("%d\n", __LINE__);
printf("%s\n", __FILE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
printf("%d\n", __STDC__);
}
파일명이 hello.c인 위 파일을 컴파일 후 실행해 보면, 다음과 같이 출력된다.
__LINE__
이나 __FILE__
는 디버그 및 버그 추적 용도로 활용할 수 있다.
Empty Macro Arguments
Parameterized macro에서 parameter를 빼먹으면 어떻게 될까?
답은 ‘괜찮다’이다. 그냥 빈 칸, 혹은 아무 것도 없는 것이 된다.
#define JOIN(a, b, c) a##b##c
int JOIN(q, w, r), JOIN(q, , r), JOIN(, , r);
위 코드에서는 세 개의 정수형 변수, qwr
, qr
, r
가 만들어진다. JOIN(,,)
는 그냥 완전히 빈 칸이 된다.
‘Stringization’을 하는 # operator의 경우도 empty argument가 허용된다.
#define MK_STR(x) #x
char str[] = MK_STR();
위 코드는 char str[] = ""
가 된다. ““만 남는 것이다.
Macros with a Variable Number of Arguments
가변 개수의 argument를 받는 macro도 만들 수 있다.
#define TEST(condition, ...) ((condition)? printf("Passed TEST : %s\n", condition) : printf(__VA_ARGS__))
...
는 ellipsis 라고 불리는 token으로, 가변이 아닌 parameter가 모두 쓰인 뒤, 맨 마지막에 나온다.
사용 예시는 아래와 같다.
int i=100; int j=50;
TEST(i>=j, "i=%d is smaller than j=%d", i, j);
이는 아래와 같이 바뀐다.
int i=100; int j=50;
((i>=j)? printf("Passed TEST : %s\n", condition) : printf("i=%d is smaller than j=%d", i, j));
즉, 추가로 붙은 모든 argument들이 __VA_ARGS__ 자리에 자동으로 들어가게 된다.
The __func__ Identifier
__func__ identifier는 preprocessor이 처리하는 부분은 아니지만 기능이 유사하고 디버깅에 쓰기 좋아서 이 챕터에서 소개되었다.
다음 두 코드는 같은 출력을 하게 된다.
#define FUNCTION_CALLED() printf("%s is called\n", __func__);
void function1()
{
FUNCTION_CALLED();
}
void function1()
{
const static char __func__[] = "function1";
printf("%s is called\n", __func__);
}
출력 결과는 함수의 이름인 “function1 is called” 가 된다. 즉 __func__
는 해당 함수의 이름에 해당하는 identifier이고, 두 번째 코드처럼 매 함수 body의 시작 부분에 함수 이름을 값으로 갖는 string을 정의한 것과 동일하다.
이 기능은 함수의 call, return 등을 추적하는 디버깅에서 사용될 수 있다.