본문 바로가기

프로그래머/C, C++

[포프 tv 복습] 전처리기

전처리기


전처리기로 할 수 있는 일들

  1. 다른 파일을 인클루드
    • 전처리기 지시문 #include을 사용
  2. 매크로를 다른 텍스트로 대체
    • #define, #undef와 전처리기 연산자 #, ##를 사용
  3. 소스파일의 일부를 조건부로 컴파일
    • 전처리기 지시문 #if, #ifdef, #ifndef, #else, #elif, #endif를 사용
  4. 일부로 오류를 발생시킴
    • 전처리기 지시문 #error를 사용

매크로 대체 : #define

#define 식별자 대체_목록(선택)
  • #define A (10)
    • 전처리기가 소스 코드 뒤지다가 A가 보이면 모두 (10)으로 바꿔줌
  • #define A
    • 이것도 가능
    • 하지만 바꿔줄 내용이 없음
    • 그 대신 다른 전처리기 지시어로 A가 정의 돼 있는지 판단 가능
#define TRUE(1)
#define FALSE(0)
#define 식별자(매개변수) 대체_목록
  • 심지어 함수처럼 쓰는 것도 가능
    • 이 경우는 매크로 함수라고 함

매크로 대체 : #undef

#undef 식별자
  • 이미 정의된 식별자를 없애는 것
  • 해당 식별자로 정의된 텍스트 매크로가 없다면 이 지시문은 무시됨

매크로 대체 : 미리 정의되어 있는 #define

  • 모든 C 구현이 정의하는 것들
    • FILE : 현재 파일의 이름
    • LINE : 현재 코드의 줄 번호를 정수형으로 표시
    • 두 매트로 모두 오류 출력 시 자주 사용
      fprintf(stderr, "internal error: %s, line %d.\n", __FILE__, __LINE__);
    • (C95부터 지원) STDC_VERSION : 현재 컴파일에 사용 중인 C 표준
  • 당연히 각 컴파일러가 자기 맘대로 정의하는 것들도 있음

조건부 컴팡리

  • 조건이다 보니 if/else 문과 유사한 지시문들이 대거 포진
  • 조건에 따라 특정 부분의 코드를 컴파일에 포함 또는 배제
#if 표현식
#ifdef 식별자     혹은     #if defined 식별자
#ifndef 식별자     혹은     #if !defined 식별자
#elif 표현식
#else
#endif

조건부 컴팡리 : 인클루드 가드

  • 순환 헤더 인클루드, 즉, 헤더 꼬임을 방지
    1. 어떤 상수를 #define으로 정의
    2. 그 후 컴파일러에게 조건적으로 코드를 컴파일하라고 지시
      #ifndef FOO_H
      #define FOO_H
      // 원래 헤더 파일 내용
      #endif // FOO_H

어떤 식별자가 #define 되어있는지 판단

#ifndef NULL
#define NULL (0)
#endif

#if !defined(NULL)
#define NULL (0)
#endif

#if defined(NULL)
#undef NULL
#endif

#define NULL (0)

조건부 컴파일에서 주의할 점

#define A

#if defined(A) // 참
#define LENGTH (10)
#endif

#if A
#define LENGTH (10)
#endif

조건부 컴파일 : 버전 관리

새 기능을 추가 중일 때, 버전 관리용으로 사용할 수 있음

int spawn_monster(...)
{
    get_monster_skin();
    get_monster_stat();


#if defined(FILE_VERSION_2)
    use_custom_skin(...);
#endif
    calculate_spawn_location();

    return TRUE;
}
  • 어딘가에 #define FILE_VERSION_2라는 코드가 없으면 컴파일에 포함 안 됨

#elif와 #else를 사용해서 각 버전마다 필요한 작업을 할 수 있음

...

#if defined(FILE_VERSION_2)
    use_custom_skin(...);
#elif defined(FILE_VERSION_3)
    use_custom_voice(...);
#else
    use_default_skin(...);
    use_default_voice(...);
#endif

...

조건부 컴파일 : 주석 처리를 편하게

  • #if 0와 #endif를 사용하면 보다 편하게 주석 처리가 가능

컴파일 오류 발생

#error 메세지
  • 컴파일 도중에 강제로 오류를 발생시키는 매크로
  • 메세지를 꼭 따옴표로 감쌀 필요는 없음
    // version.h
    #define VERSION 10
    

// builder.h
#if VERSION != 11
#error "unsupported version"
#endif


## 컴파일 중에 매크로 정의하기
- 컴파일 도중에 -D 옵션으로 전달 가능
> clang -std=c89 -W -Wall -pedantic-errors -DA *.c
- #define A (1)과 똑같은 결과 (#define A가 아님)
- 직접 대체할 값 지정할 수 있음
> clang -std=c89 -W -Wall -pedantic-errors -DA=52 *.c
- #define A (52)와 똑같은 결과

## 배포용으로 컴파일하기 : -DNDEBUG
> clang -std=c89 -W -Wall -pedantic-errors -DNDEBUG *.c
- 배포(release) 모드로 실행파일을 컴파일하라고 알려주는 매크로
    - NDEBUG: '디버그가 아니다'라는 뜻
    - assert()가 사라짐
    - 디버그 모드에서만 실행될 코드는 #if !defined(NDEBUG) 속에 넣을 것
- 이 대신 다음과 같은 매크로를 직접 정의해 사용하는 프로젝트 많음
    - DEBUG : 디버그용 빌드
    - RELEASE : 배포용 빌드
    - 기타 : 필요에 따라 다양한 빌드를 지정

## 매크로 함수
- #define을 할 때 '대체 가능한 매개변수 목록'을 받음
```c
#define SQUARE(a) a * a
#define ADD(a, b) a + b

// main
int num1;
int num2;
int result;

num1 = 10;
num2 = 20;
result = ADD(num1, num2);     // 30
result = SQUARE(num1);        // 100

매크로 함수에서 흔히 하는 실수

#define SQUARE(a) a * a
#define ADD(a, b) a + b

// main
int num1;
int num2;
int result;

num1 = 10;
num2 = 20;
result = 10 * ADD(num1, num2);     // 120

매크로 함수의 구현은 소괄호로 감싸준다

#define SQUARE(a) (a * a)
#define ADD(a, b) (a + b)

// main
int num1;
int num2;
int result;

num1 = 10;
num2 = 20;
result = 10 * ADD(num1, num2);     // 300

베스트 프랙티스 : 매크로에 소괄호를 쓰자

#define ADD(a,b) a+b 
#define ADD(a,b) (a+b)

매크로가 여러줄이면?

  • \를 사용하면 매크로를 여러 줄로 나눌 수 있음
    #define POW(n,p,i,r) r = 1;                    \
                      for(i = 0; i < p; ++i){    \
                          r *= n;                \
                      }

매크로 함수의 활용: 어서트 재정의

  • 어셈블리 코드를 이용한 나만의 어서트 매크로를 만들 수 있음
    #define ASSERT(condition, msg)                                        \
      if(!(condition)){                                                \
          fprintf(stderr, "%s(%s: %d)\n", msg, __FILE__, __LINE__);    \
          __asm {int 3}                                                \
      }                                                                \
    

// main
int month = 20;
ASSERT(month < 12, "invalid month number");

- 왜 어서트 매크로를 대신 사용할까?
    - assert()는 실패 시 호출 스택의 위치가 assert() 함수 속
    - __asm{int 3}는 실제로 어서트에 실패한 코드가 호출 스택의 현 위치
    - 또한 사람이 읽기 편한 설명도 눈에 딱 보임(stderr 출력은 필수 아님)
        - ASSERT(month < 12, "invalid month number");
        - assert(month < 12);
    - 단, int 3은 x86 어셈블리에서 프로그램 실행을 중지하는 인터럽트
    - 플랫폼마다 사용하는 어셈블리 명령어가 달라짐

## 전처리기 명령어 : # 명령어
```c
#define 식별자(매개변수) 대체_목록
  • 매개변수 자체를 문자열로 바꿔줌
  • 매개변수를 쌍따옴표로 감싸는 것
    #define str(s) #s
    

printf("%s\n", str(\n)); // new line
printf("%s\n", str("\n")); // "\n"
printf("%s\n", str(int main)); // int main
printf("%s\n", str("Hello World")); // "Hello World"
printf("%s\n", str(num1)); // num1


## 전처리기 명령어 : ## 명령어
```c
#define 식별자(매개변수) 대체_목록
  • 대체 목록 안에 있는 두 단어를 합쳐서 새로운 텍스트로 바꿈
    • 단어는 매개변수일 수도 아닐 수도 있음
  • #와 달리 문자열 데이터를 만들어 주는 게 아님
    #define print(n) printf("%d\n", g_id_##n)
    

int g_id_none = 0;
int g_id_teacher = 1;
int g_id_student = 2;

// main
print(number); // 컴파일 오류
print(none); // 컴파일 : 1 출력
print(student); // 컴파일 : 2 출력


## vs##
```c
#define combine1(a, b) (a#b)
#define combine2(a, b) (a##b)

// main
int student_id = 987654;

// 컴파일 오류 : student_"id"
printf("%d\n", combine1(student_, id));

// 컴파일 : student_id의 값인 987654를 출력
printf("%d\n", combine2(student_, id));

매크로 함수의 장점과 단점

  • 장점
    • 함수 호출이 아닌 곧바로 코드를 복붙하는 개념
    • 함수 호출에 따른 과부하가 없음
    • C에서 불편한 것들 중 일부는 매크로 꼼수로 해결 가능
  • 단점
    • 디버깅이 아주 어려움
    • \를 사용해서 아무리 읽기 좋게 매크로 함수를 만들어도 중단점을 사용 불가

코드보기 : 전처리기를 이용한 튜플

#include <stdio.h>

// id(int), "name"(const char*), hp(int)
#define MONSTER_DATA    \
    MONSTER_ENTRY(0, "pope",     100)    \
    MONSTER_ENTRY(1, "big rat", 30)        \
    MONSTER_ENTRY(2, "mama",     255)    \
    MONSTER_ENTRY(3, "dragon",     300000)    \

int main(void)
{
    size_t i;

    int ids[] = {
#define MONSTER_ENTRY(id, name, hp) id,
        MONSTER_DATA
#undef    MONSTER_ENTRY
    };

    const char* names[] = {
#define MONSTER_ENTRY(id, name, hp) name,
        MONSTER_DATA
#undef    MONSTER_ENTRY
    };

    int healths[] = {
#define MONSTER_ENTRY(id, name, hp) hp,
        MONSTER_DATA
#undef    MONSTER_ENTRY
    };

    for(i = 0; i < sizeof(ids) / sizeof(int); ++i)
    {
        printf("%3d %6d %s\n",
            ids[i], healths[i], names[i]);
    }

    return 0;
}

getter 만들기

#include <stdio.h>

// (type, name)
#define MONSTER_STRUCT    \
    MONSTER_MEMBER(int,            id)        \
    MONSTER_MEMBER(const char*,    name)    \
    MONSTER_MEMBER(int,            hp)        \

typedef struct{
#define    MONSTER_MEMBER(type, name) type name;
    MONSTER_STRUCT
#undef MONSTER_MEMBER
} monster_t;

#define MONSTER_MEMBER(type, name)            \
type get_mob_##name(const monster_t* mob)    \
{                                            \
    return mob->name;                        \
}                                            \

MONSTER_STRUCT

#undef MONSTER_MEMBER

int main(void)
{
    monster_t mob;
    mob.id = 0;
    mob.name = "Pope Mob";
    mob.hp = 10001;

    printf("%3d %6d %s\n",
        get_mob_id(&mob),
        get_mob_hp(&mob),
        get_mob_name(&mob));

    return 0;
}