전처리기

전처리는 컴파일 전에 코드의 내용을 바꿔주는 것을 말합니다. 이러한 동작은 전처리기가 수행합니다. #으로 시작하는 문장들은 전처리기를 위한 것으로, #include, #pragma, #define 등이 있습니다.

#include <>는 단어 그대로, <> 안의 것을 포함시키라는 뜻입니다. 예를 들어, 우리가 주구장창 써오던 #include <stdio.h>는 정해진 위치의 stdio.h 헤더 파일을 포함시키라는 뜻입니다. 이렇게 함으로서 우리는 stdio.h의 함수와 객체 등을 사용할 수 있게 됩니다. stdio.h 이외에도 stdint.h(고정된 크기의 자료형), setjmp.h(함수를 넘어 점프, 정확히 말하면 함수 호출 스택의 내용을 저장해 두었다가 복구), memory.h(memcpy와 같이 메모리에 관련), math.h(삼각 함수나 로그 함수같이 수학에 관련), string.h(문자열 관련) 등 여러 헤더 파일들이 있습니다. 프로그래머가 직접 헤더 파일을 작성하는 것도 가능합니다. 

#pragma는 컴파일에 관련된 중요한 무언가를 수정하기 위해 사용합니다. 아까 잠시 말한 #pragma pack이나 헤더 파일을 한 번만 포함하게 해 주는 #pragma once같은 것이 있습니다.

#define은 치환 매크로입니다. 말 그대로 내용을 바꿔줍니다. #define 식별자 바꿀 내용의 형식으로 사용합니다.

#include <stdio.h>
#define MAX 128
int main() {
      int a[ MAX ][ MAX ] = { 0, };
      printf("%d\n", MAX);
      return 0;
}

이런 식으로, 프로그램 상에서 자주 쓰이는 수에 사용합니다. 만약 한 번에 수정할 일이 생겼을 때, MAX를 정의한 줄만 바꿔주면 되기 때문에 무척 유용합니다. 매크로 이름은 관습적으로 대문자를 사용하며, 매크로는 const와는 다르게 컴파일 타임에 상수로 취급됩니다.

#include <stdio.h>
#define add(a, b) a + b
int main() {
      printf("%d\n", add(3, 5));
      return 0;
}

8

별 문제가 없어 보이는 것 같기도 합니다. 이 코드는 다음과 같이 치환될 것입니다.

#include <stdio.h>
int main() {
      printf("%d\n", 3 + 5);
      return 0;
}

그리고, 제 생각에는 조만간 대참사를 일으킬 것 같군요.

#include <stdio.h>
#define add(a, b) a + b
int main() {
      printf("%d\n", add(3, 5) * 8);      //절대 64가 아닙니다!
      return 0;
}

43

이해할 수 없다면, 위의 코드가 어떻게 치환되는지 한 번 봅시다.

#include <stdio.h>
int main() {
      printf("%d\n", 3 + 5 * 8);      //절대 64가 아닙니다!
      return 0;
}

매크로 사용 시에는 꼭! (괄호)를 사용해 줍시다.

#include <stdio.h>
#define add(a, b) (a + b)
int main() {
      printf("%d\n", add(3, 5) * 8);      //이제 64가 되었습니다!
      return 0;
}

64

하지만 이런 문제는 피해갈 수 없습니다.

#include <stdio.h>
#define square(p) (p * p)
int main() {
      int a = 10;
      printf("a의 제곱은 %d\n", square(a++));
      printf("%d\n", a);
      return 0;
}

a의 제곱은 110
12

이렇게 치환되기 때문이지요.

#include <stdio.h>
int main() {
      int a = 10;
      printf("a의 제곱은 %d\n", a++ * a++);
      printf("%d\n", a);
      return 0;
}

스스로 연산자 우선순위를 찾아보며 이유를 찾아 보도록 합시다.

이런 골치아픔으로부터 벗어나는 가장 좋은 방법은 매크로 대신 인라인 함수를 사용하는 것입니다. 인라인 함수를 사용하면 경우에 따라서는 성능상의 이득을 얻을 수 있는 경우도 있지요. 하지만 매크로는 인라인 함수보다 좀 더 유연합니다.

#include <stdio.h>
#define FOR(i, a) for(int i = 0; i < a; ++i)
int main() {
      FOR(i, 3) {
            FOR(j, 4)
                  putchar('*');
            putchar('\n');
      }
      return 0;
}

매크로의 남용은 다른 사람뿐 아니라 자신도 읽기 어려운 코드를 만듭니다. 적당히 사용하도록 합시다.

함수가 그렇듯 매크로에도 가변 인자라는 것이 있습니다. 하지만 어렵기 때문에, 여기까지만 하도록 하겠습니다.