6 minute read

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들

  1. Directive는 반드시 #로 시작해야 한다. # 앞에 white space만 있다면 line의 시작이 꼭 아니어도 되지만, # 뒤에 나오는 부분은 반드시 그 directive의 정보에 대한 것이어야 한다.
  2. # define N 100과 같이, 사이사이에 space나 tab으로 분리되어도 괜찮다.
  3. Directive는 \n이 나올 때 끝난다. 여러 줄에 걸쳐서 쓰고 싶으면. \로 줄바꿈을 해서 쓴다.
  4. Directive는 프로그램의 어느 부분에서든 나와도 된다. 꼭 맨 앞일 필요는 없다.
  5. 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의 장점

  1. function에 비해 빠르다. Run-time overhead가 없기 때문이다.
  2. Generic하다. 함수의 경우 argument의 타입이 정해져 있지만, macro는 그렇지 않다.

parameterized macro의 단점

  1. 컴파일 후 source code가 아주 길어질 수 있다. 특히 nested되었을 경우 함수를 쓰는 것보다 훨씬 코드가 길어진다.
  2. Argument의 type-check가 이루어지지 않는다. preprocessor가 잡아내지도 못하고, type conversion도 받지 못한다.
  3. 함수 포인터와 다르게 macro는 포인터를 가질 수 없다.
  4. 특정 상황에서 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

  1. Macro의 replacement-list에는 다른 macro가 들어갈 수 있다.
    #define PI 3.14
    #define PI_SQUARE (PI*PI)
    
  2. 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라는 출력을 하게 된다.

  1. Macro는 선언된 이후, 파일 끝까지 쭉 적용된다. 해제하기 위해서는
    #undef identifiet
    

를 사용하면 된다.

  1. 매크로는 다르게 두 번 선언될 수 없다. 이때 ‘다르다’는 것은 공백이나 이런 형식적인 것 말고, 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인 위 파일을 컴파일 후 실행해 보면, 다음과 같이 출력된다.

image

__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 등을 추적하는 디버깅에서 사용될 수 있다.

Tags:

Categories:

Updated: