지금까지 #define, #include와 같은 directive(지시문)들을 자세한 설명 없이 써 왔다.

이 지시문들은 preprocessor(전처리기) - 컴파일 전에 C 프로그램을 수정하는 소프트웨어 - 에 의해 handle된다.

preprocessor는 강력한 도구이나 찾기가 어려운 버그의 원인이 되기도 한다.

어떤 C 프로그래머들은 preprocessor에 깊이 의존하는 경향이 있는데, 적당한 사용이 좋다.


14.1 how the preprocessor works

14.2 general rules that affect all preprocessing directives

14.3 macro definition

14.4 conditional compilation

14.5 lesser-used directives: #error, #line, #pragma



14.1 How the Preprocessor Works


preprocessor -> PP로 축약함.


PP의 행동은 preprocessing directives - #가 앞에 오는 명령문 - 에 의해 제어된다.


#define : defines a macro(상수나 자주 쓰이는 expression을 나타내는 이름)

PP는 #define directive에 이렇게 대응한다-> 매크로의 이름과 정의를 저장한다.

이후 매크로가 프로그램 내에서 쓰이게 되면, PP는 매크로를 "확장"해서, 정의된 값으로 교체한다.


#include : 특정한 파일을 열어서 컴파일하는 파일의 한 부분으로 "include"하도록 PP에게 시킨다.



directive들을 가지고 있는(가능성이 있는) C program이 preprocessor에 input된다.

PP는 이 directive들을 프로세스 내에서 제거한다.

PP의 output은 편집된 C program이며, 더이상 directive들은 존재하지 않는다.

PP의 output은 직접 컴파일러 - 프로그램의 에러를 검사하고 object code(usualy a  machine language)로 번역 - 에 전달된다.

PP는 directive들을 실행하는것 외에도, 주석을 single space character으로 대체한다.

또 어떤 PP는 불필요한 white-space 문자들 - space, 들여쓰기에 쓰인 tab 같은 - 도 제거한다.


C의 초창기에는 PP가 컴파일러가 별개의 프로그램으로, 그 아웃풋이 컴파일러에 전달되었었다.

요즘의 PP는 컴파일러의 한 부분인 경우가 많으며, 아웃풋 중 일부는 꼭 C 코드가 아니어도 된다.

예를 들어, #include <stdio.h> 같이 standard header를 include하는 것이 그 헤더를 프로그램 내로 복사하지 않으면서도 이뤄진다.

대부분의 C 컴파일러들은 PP의 output을 볼 수 있는 기능이 있다. GCC에선 -E option으로 가능.


주의할 점은 PP는 C에 대한 제한적인 지식밖에 없다. 그래서 directive를 처리하면서 잘못된 프로그램을 만들 가능성이 존재한다. 원래 프로그램이 멀쩡해 보이는데도, 그렇게 에러가 발생해서 찾기 힘들수도 있다. 복잡한 프로그램을 만들 때, PP의 output을 검색하는 것이 이러한 에러에 대처하는 유용한 방법이 될 수 있다.



14.2 Preprocessing Directives


Macro definition

#define directive는 매크로를 정의한다.

#undef directive는 정의된 매크로를 제거한다.


File inclusion

#include directive는 특정한 파일의 내용을 프로그램에 포함시킨다.


Conditional compilation

#if, #ifdef, #ifndef, #elif, #else, #endif directive는 PP에서 테스트하는 조건문의 결과에 따라, block of text를 프로그램에 포함시키거나 또는 포함시키지 않는다.


나머지 directive - #error, #line, #pragma 는 더 특수하며 자주 쓰이지 않는다. 이 챕터에서는 이 지시문들에 대해 더 자세히 다룬다. 단 #include는 15.2에서 다루고 여기서는 제외한다.


다음은 모든 directive지시문에 적용되는 규칙들이다.


directive는 항상 기호 #으로 시작된다. #는 화이트스페이스를 제외하면 줄 가장 앞에 와야 한다. #이 오고, directive의 이름이 오고, 그 다음엔 기타 정보들이 온다.

directive의 token들 사이에는 공백이나 horizontal tab이 얼마든지 올 수 있다.

directive는 명시적으로 이어지지 않는 이상 new-line character에서 끝난다. 만약 여러 줄로 만들고 싶다면 줄 마지막에 '\'를 써 줘야 한다.

directive는 프로그램 어디에나 올 수 있다. 통상적으로 #define, #include를 파일의 가장 처음에 배치하기는 하지만, 다른 지시문들은 더 나중에 쓰기도 하고, 함수 정의 내에서 쓰기도 한다.

주석이 directive와 같은 줄에 올 수 있다. directive 옆에 주석을 달아 매크로에 대한 설명을 써놓는 것이 좋다.



14.3 Macro Definitions


Chapter 2부터 지금까지 계속 써온 매크로는 'simple macro'이다. 왜냐면 파라미터가 없기 때문.


Simple Macros

C 표준에서는 ojbect-like macro


#define identifier replacement-list


replacement-list is any sequence of preprocessing tokens.

2.8에서 나온 token(의미단위로 쪼갠 최소 단위)과 유사하다.

replacement list는 identifier, keyword, numeric constant, character constant, string literal, operator, punctuation등을 포함할 수 있다.

PP는 파일의 이후에 identifier가 나타나면, 그것을 replacement-list로 대체한다.


#define STR_LEN 80

#define TRUE     1

#define FALSE    0

#define PI       3.14159

#define CR       '\r'

#define EOS      '\0'

#define MEM_ERR  "Error: not enough memory" 


#define으로 상수들에게 이름을 부여하는 것은 다음과 같은 장점이 있다.


- 프로그램을 읽기 쉽다. 알수 없는 숫자들로 이뤄진 프로그램은 읽는 사람을 혼동시킨다.

- 프로그램을 수정하기 쉽다. "하드코드"된 상수들은 고치기가 매우 번거롭다.

- 비일관성과 오타로 발생하는 에러를 피하게 해 준다.

- C의 기호를 약간 변경시킬 수 있다. 나만의 언어도 만들어버릴 수 있다.

  #define LOOP for (;;)

  기호를 너무 많이 바꾸면 다른사람은 읽기 힘들어질 것이다.

- type의 이름을 다시 부여할 수 있다.

  #define BOOL int

- "conditional compilation"을 사용하는 데 중요하다. 14.4에서 다룸.


매크로가 상수로 사용되었을 때, 보통은 그 이름을 전부 대문자로 사용한다. 다른 목적일 때는 책마다 다른데 매크로(특히 parameterized macros)가 버그의 원인이 될 수 있으므로 주의를 위해 전부 대문자로 사용하기도 한다. K&R의 책에서는 소문자 이름을 선호한다.


Parameterized Macros

function-like macro라고도 한다.


#define identifier( x1 , x2 , ... , xn ) replacement-list


x1, x2, ..., xn은 identifiers임(macro's parameters). 이 파라미터들은 replacement-list에 몇번이고 나올 수 있다.

parameter list가 비어 있어도 된다.

identifier과 여는 괄호 사이에는 공백이 있어서는 안 된다. 만약 공백이 있다면 PP는 이것을 simple macro로 간주하고 괄호부터 전부 replacement list로 간주한다.


directive 이후의 파일에 identifier(y1, y2, ..., yn) 같은 표현이 등장하면, 이것을 replacement-list의 형태로 바꾼다.


#define MAX(x, y)    ((x)>(y)?(x):(y))

#define IS_EVEN(n)   ((N)%2==0))

#define TOUPPER(c)   ('a'<=(c)&&(c)<='z'?(c)-'a'+'A':(c))


parameterized macro를 사용하는 것의 장점

- 프로그램이 약간 더 빨라질 수 있다. 함수 호출은 약간의 부하를 일으킬 수 있다 - context information 저장, 인자 복사 등 - 매크로를 사용하면 그런 부하가 없다. (하지만 C99의 'inline function'은 매크로를 사용하지 않아도 부하를 겪지 않게 해 준다)

- 매크로는 "generic"하다. 함수의 파라미터와 달리 매크로의 파라미터는 특정한 type이 없다. 따라서 type이 달라져도 함수를 새로 써야 하는 불편함이 없다.


단점

- compiled code가 종종 더 길어진다. 매크로 이름이 대응하는 replacement list로 대체되면서, 소스 프로그램의 길이가 길어진다.

- argument의 타입이 체크되지 않는다. 함수가 호출될 때는 컴파일러에서 인자의 type이 적절한지를 검사하고 적절한 type으로 자동변환하거나 에러 메시지를 출력한다. 매크로의 인자는 PP에 의해 검사되지도 않고 변환되지도 않는다.

- 포인터가 매크로를 가리키게 할 수 없다. 반면 포인터는 함수를 가리킬 수 있다.

- 매크로는 인자의 값을 한번보다 더 많이 evaluate할 수 있다. 만약 인자에 side effect가 있다면, 원치 않는 결과가 나타날 수 있다.

n = MAX(i++, j);

n = ((i++)>(j)?(i++):(j));    // PPing 이후의 라인


parameterized macro는 함수를 simulate하는 것 이외에도, 반복적인 코드를 더 간결하게 만들어줄 수 있다.

printf("%d\n", i);

#define PRINT_INT(n) printf("%d\n", n)



The # Operator

macro 정의시에 쓰이는 특별한 연산자: #, ##

둘 다 컴파일러가 인지하지 않으며 PP단계에서 실행된다.

# 연산자는 매크로의 argument를 string literal로 변환한다. 

parameterized macro의 replacement list에서만 사용할 수 있다.

# operator의 기능을 "stringization"이라고 부른다(사전에 없는 단어)


#define PRINT_INT(n) printf(#n " = %d\n", n)


PRINT_INT(i/j);

printf("i/j" " = %d\n", i/j);    // PP를 거치면서 이렇게 치환된다    

printf("i/j = %d\n", i/j);       // compiler는 서로 인접한 string literal을 하나로 합친다


만약 i = 11, j = 2라면

i/j = 5 라고 출력됨


The ## Operator

## 연산자는 두개의 토큰(identifiers, for example)을 붙여서 하나의 토큰으로 만든다. token-pasting 이라고 알려져 있다.

피연산자 중 하나가 매크로 파라미터일 때에는, 먼저 parameter가 해당되는 argument로 대체된 후에 붙여진다.


#define MK_ID(n) i##n

int MK_ID(1), MK_ID(2), MK_ID(3);    //먼저 n이 1, 2, 3으로 각각 대체된 후에 i와 붙는다.

int i1, i2, i3;      // PP 처리 후


## 연산자는 자주 사용되지는 않지만, 다음과 같은 상황에서는 의미가 있다


#define GENERIC_MAX(type)            \

type type##_max(type x, type y)      \

{                                    \

    return x > y ? x : y;            \

}


각 type에 대한 MAX함수를 쉽게 만들어 준다.

GENERIC_MAX(float) -> float 변수에 대한 max 함수 -  float float_max(float x, float y)  - 생성


General Properties of Macros


- 매크로의 replacement list에는 다른 매크로의 invocation이 들어올 수 있다.

#define PI        3.14159

#define TWO_PI    (2*PI)

 PP는 모든 매크로 name이 제거될 때까지 replacement list를 계속 rescan한다

- PP는 온전한 토큰만 대체한다. 다른 변수 이름 등에 매크로의 이름이 들어있다고 해서 바꾸지 않는다.

- 매크로의 scope는 보통 매크로가 정의된 시점부터 파일의 끝까지이다. 함수 내에서 선언된다고 함수 안에서 끝나지 않는다.

- 매크로는 두번 정의될 수 없다. 두번째 정의가 space를 제외하고 첫번째것과 동일하지 않은 한.

- 매크로는 #undef directive에 의해서 "undefined"될 수 있다.

#undef indentifier

identifier가 매크로의 이름일 때, 그 매크로에 대한 정의를 제거하고, 그 이름을 다르게 정의할 수 있게 해 준다. 만약 그 이름을 가진 매크로가 없다면 아무 효과가 없다.


Parentheses in Macro Definitions


앞서 선언한 매크로들의 replacement list 주위에 괄호를 계속 둘렀는데 이것이 필요한 것인가?

답은 Yes. 만약 괄호를 사용해서 replacement list들을 감싸주지 않으면 기대하지 않은 결과를 가져올 수 있다.

매크로 정의시 괄호 사용에 대한 두 가지 규칙.

1. 만약 replacement list에 operator가 있다면, replacement list를 항상 괄호로 감싼다.

2. 만약 매크로에 parameter가 있다면, replacement list 안에 있는 모든 parameter를 괄호로 감싼다.

괄호를 사용하지 않았을 때, 매크로 주변에 있는, 또는 argument의 연산자들의 precedence, 그리고 associativity가 의도하지 않은 방향으로 결과를 만들어낼 수 있다.

#define TWO_PI 2*3.14159      // replacement list에 괄호가 없음

cnv_factor = 360/TWO_PI;      

cnv_factor = 360/2*3.14159;   // 실제 결과

cnv_factor = 360/(2*3.14159); // 원하는 결과


#define scale(x) (x*10)       // parameter에 괄호가 없음

j = SCALE(i+1);             

j = (i+1*10);                 // 실제 결과

j = (i+1)*10;                 // 원하는 결과


괄호를 제대로 사용하지 않으면, 프로그램이 잘 컴파일되고 매크로도 작동되는 것처럼 보이지만, 찾기 힘든 오류를 발생시킨다.


Creating Longer Macros


콤마 연산자를 사용해서 replacement list에 expression을 연속으로 넣고 좀더 복잡한 매크로를 만들 수 있다.

#define ECHO(s) (gets(s), puts(s))


콤마 연산자를 사용하지 않고 중괄호를 이용해서 여러 개의 expression들을 compound statement로 만들어도 되는가?

#define ECHO(s) { gets(s); puts(s); }

if문 내에서 매크로를 사용시 compound statement와 세미콜론으로 인해 뒤 else문은 짝을 찾지 못해 에러가 발생함.


만약 매크로에 expression이 아닌 여러 개의 statement를 담아야 한다면? 콤마 연산자를 사용할 수 없다. 콤마 연산자는 expression은 묶어줘도 statement를 묶어 주지 못함. 해결책은 do.. while (false) loop를 사용하는 것. 가장 뒤에 세미콜론을 붙이지 않은 채로.


do while 문법 복습 -> 가장 뒤에 세미콜론이 붙는다.

do statement while ( expression ) ;


#define ECHO(s)    \

    do {           \

        gets(s);   \

        puts(s);   \

    } while(0)     \


매크로 정의시 while(0) 뒤에 세미콜론이 붙지 않는 점을 유의.


ECHO(str);    /* becomes do { gets(str); puts(str); } while (0);


Predefined Macros


미리 정의되어 있는 매크로들이다. 각각의 매크로는 정수 상수, 또는 string literal을 나타낸다. 컴파일에 대한 정보, 또는 컴파일러에 대한 정보를 제공한다.


Name 

Description 

__LINE__ 

Line number of file being compiled 

__FILE__ 

Name of file being compiled 

__DATE__ 

Date of compilation (in the form "Mmm dd yyyy") 

__TIME__ 

Time of compilation (in the form "hh:mm:ss") 

__STDC__ 

1 if the compiler conforms to the C standard(C89 or C99) 

* Mmm은 세글자로 된 달 이름의 축약어. ex) Dec Feb..


__DATE__, __TIME__ 매크로는 프로그램이 언제 컴파일했는지 알려준다.

__LINE__, __FILE__ 매크로를 에러를 찾는 데 사용할 수 있다.


division by zero 에러를 찾아내 주는 매크로

#define CHECK_ZERO(divisor)  \

    if (divisior == 0)       \

        printf("*** Atempt to divide by zero on line %d "  \

                "of file %s ***\n", __LINE__, __FILE__)

// CHECK_ZERO 매크로를 나눗셈 전에 넣어 준다.

CHECK_ZERO(j);

k = i / j;

*** Attempt to divide by zero on line 9 of file foo.c *** // 0으로 나누려고 하면 이런 메시지를 출력한다


에러를 탐지하는 매크로는 상당이 유용해서 C 라이브러리에는 assert라는 에러를 찾는 매크로가 들어 있다.


Empty Macro Arguments (C99)


C99에선 매크로의 argument가 비어있어도 된다. 콤마의 갯수는 argument를 넣어서 호출할 때와 같게 한다(arg가 생략된 것을 쉽게 알수 있음).

argument를 빈칸으로 두면, 대응하는 parameter에는 아무것도 들어가지 않게 된다.


#define ADD(x, y) (x+y)

i = ADD(,k);

i = (+k);


비어있는 argument 앞에 #연산자가 오면, "" 비어있는 문자열이 된다.

#define MK_STR(x) #x

char empty_string[] = MK_STR();

char empty_string[] = "";


##연산자의 피연산자 중 하나가 비어있는 경우, 보이지 않는 "placemarker" 토큰이 그 자리를 대체한다. 연산의 결과는 비어있지 않은 다른 피연산자가 그대로 나온다. 두개의 placemarker 끼리 연산하면, 결과는 placemarker 하나가 나온다.

#define JOIN(x,y,z)  x##y##z

int JOIN(a,b,c), JOIN(a,b,), JOIN(a,,c), JOIN(,,c);

int abc, ab, ac, c;


Macros with a Variable Number of Arguments


C89에서는 매크로의 argument 갯수는 고정되어 있다. C99에선 제한이 완화되어 argument의 갯수가 무한대도 될 수 있다. 

함수에 대해서는 이 기능이 예전부터 가능했었다(variable-length argument lists). 매크로에서도 가능하게 된 것.

매크로가 가변 인자를 가져야 하는 주된 이유는 printf나 scanf 처럼 가변 인자를 받는 함수에 인자를 전달하기 위해서이다.


#define TEST(condition, ...) ((condition)?    \

    printf("Passed test: %s\n", #condition):  \

    printf(__VA_ARGS__))


Test(voltage <= max_voltage, "Voltage %d exceeds %d\n", voltage, max_voltage);

... 토큰은 ellipsis라고 하며 매크로의 다른 일반적인 매개변수 뒤 맨 마지막에 자리잡는다. 

__VA_ARGS__ 는 가변 인자를 갖는 매크로에서만 replacement list에 들어가는 특별한 identifier이다. ellipsis에 대응하는 모든 인자들을 나타낸다. ellipsis에 대응하는 최소 하나의 인자가 존재해야 한다(empty argument도 가능).


The __func__ Identifier(C99)


__func__ 자체는 PP와는 관계가 없지만 다른 PP의 기능과 비슷하게 디버깅에 유용하다.

모든 함수는 __func__ identifier에 접근할 수 있으며, 현재 실행되고 있는 함수의 이름이 들어있는 문자열 변수와 같다.

#define FUNCTION_CALLED() printf("%s called\n", __func__);

#define FUNCTION_RETURNS() printf("%s returns\n", __func__);


void f(void)    {

    FUNCTION_CALLED();   /* displays "f called" */

    ...

    FUNCTION_RETURNS();  /* displays "f returns" */


__func__의 다른 용도로, 함수 a내에서 함수 b를 호출하면서 인자로 전달되면 함수 a의 이름을 전달해 준다.



14.4 Conditional Compilation


PP에 의해 테스트되는 조건에 따라 프로그램의 일부가 포함되고, 제외된다.


The #if and #endif Directives


디버그를 위해 어떤 값을 프로그램의 중요한 위치에서 printf함수로 출력하고 싶을 때가 있다. 버그를 찾았을 때, 나중을 위해서 그 printf 함수를 남겨놓고 싶지만 컴파일러가 그 함수를 무시하게 하려면?

#define DEBUG 1


#if DEBUG

printf("Value of i: %d\n", i);

printf("Value of j: %d\n", j);

#endif


PP는 #if 지시문이 DEBUG의 값이 0인지 아닌지 test하고, 0이 아니라면 아래에 있는 두 printf함수를 프로그램에 포함시키고 #if, #endif 지시문은 제외시킨다. 다. DEBUG의 값이 0이라면 위 4줄 전부를 프로그램에서 제외시킨다.


일반적인 형태

#if constant-expression

#endif


The defined Operator


앞서 #, ##연산자를 다뤘다. 남은 하나의 연산자는 defined 연산자이다. defined 연산자는 PP에서만 사용되고 그 identifier가 정의된 매크로라면 1을, 정의되지 않은 매크로라면 0을 생산한다.

#if defined(DEBUG)

...

#endif

#if defined DEBUG    // 괄호를 사용하지 않아도 된다.


defined 연산자는 매크로 이름이 정의되었는지 아닌지만을 확인하기 때문에, 그 매크로 이름에 값을 주지 않아도 된다.

#define DEBUG


The #ifdef and #ifnef Directives

#ifdef identifier

#ifdef는 #if와 사용법이 유사하다. 단지 조건이 매크로의 이름이 정의되었는지만을 따진다.


#ifdef identifier

#if defined(identifier)

위 두 지시문의 의미는 동일하다.


#ifndef는 #ifdef와 비슷하지만, 매크로의 이름이 정의되지 않았는지를 테스트한다.


#ifndef identifier

#if !defined(identifier)

동일


The #elif and #else Directives


#if, #ifdef, #ifndef block은 평범한 if문처럼 nested형태로 사용 가능하다. 또한 #elif와 #else directive도 사용 가능하다.


#if expr1

Lines to be included if expr1 is nonzero

#elif expr2

Lines to be included if expr1 is zero but expr2 is nonzero

#else

Lines to be included otherwise

#endif


#if 대신 #ifdef, #ifndef가 와도 된다. #elif는 여러개가 올 수 있으나, #else는 #if와 #endif 사이에 단 하나만 올 수 있다.


Uses of Conditional Compilation

Conditional compilation은 디버깅에도 유용하지만, 다음과 같이 유용하게 쓸 수 있다.


다양한 기계나 운영체제에서 사용 가능한 프로그램을 만들 때

WIN32, MAC_OS, LINUX중 어떤 것이 정의되어 있는 매크로인지에 따라 세 그룹 중 하나의 그룹만 프로그램에 포함된다.


#if defined(WIN32)

...

#elif defined(MAC_OS)

...

#elif defined(LINUX)

...

#endif


서로 다른 컴파일러에서 컴파일 가능한 프로그램을 만들때

어떤 컴파일러는 C의 표준을 받아들이고, 어떤 것은 그렇지 않다. __STDC__ 매크로를 통해 PP는 컴파일러가 표준을 따르는지 아닌지를 알 수 있다. (Visual Studio 2015에서는 __STDC__ 매크로가 정의되어있지 않은듯함)


#if __STDC__

Function Prototypes

#else

Old-style function declarations

#endif


매크로가 정의되어 있는지 확인해서, 안되어 있으면 새로 정의

#ifndef BUFFER_SIZE

#define BUFFER_SIZE 256

#endif


일시적으로 코드 일부를 disabling(conditioning out). /* */가 들어있는 코드는 또 /* */를 사용해서 주석으로 만들 수 없다. 그러나 주석 처리하는것과는 다르며 사이에 있는 코드에 끝나지 않은 statement가 있거나 paired되지 않은 따옴표가 있거나 하면 오류 발생.

#if 0

Lines containing comments

#endif



14.5 Miscellaneous Directives


#error, #line, #pragma 지시문(간단히)


The #error Directive

#error message

message는 any sequence of tokens


PP가 #error directive를 만나면, message가 담겨 있는 에러 메시지를 출력한다. VS, DEV-C++에선 컴파일도 해주지 않는다.


The #line Directive

1. 프로그램 라인에 번호가 붙는 방법을 바꾼다.(보통은 1,2,3...과 같이 붙는다)

2. 컴파일러에게 다른 이름을 가진 파일에서 프로그램을 읽어들인다고 생각하도록 만들 수 있다.


#line n

n은 1과 32767(C99에선 2147483647) 사이의 정수여야 한다.

이 지시문이 있으면 그 다음 줄의 번호가 n이 된다. n, n+1, n+2...

#line n "file"

이 지시문 뒤의 줄 번호가 n이 되며, "file"에서 왔다고 간주된다. 

#line 지시문 사용시에 __LINE__ 매크로의 값이 바뀌며, __FILE__ 매크로의 값도 바뀔 수 있다. 대부분의 컴파일러들이 이 정보를 이용해서 #line 지시문을 에러 메시지를 만드는 데 사용한다.

#line 지시문은 주로 C 코드를 만들어내는 프로그램들이 사용한다(yacc).


The #pragma Directive

#pragma directive는 컴파일러가 특별한 행동을 하도록 요청한다. #pragma 뒤에 오는 명령어들은 컴파일러에 따라 다르다. #pragma 뒤에 알 수 없는 명령어가 오더라도, PP는 에러메시지를 출력해서는 안 되고 단지 무시하기만 한다.






+ Recent posts