C 전처리기(PREPROCESSOR) 지시자(DIRECTIVES)에 관한 내용을 찾다가 괜찮은 내용이 있어서 정리합니다.

아래의 내용은 WIKIPEDIA(http://en.wikipedia.org/wiki/C_preprocessor)에서 설명되어 있는 사항들을 중점적으로

하였으며 더 자세한 내용을 알고자 하시는 분은 WIKIPEDIA를 읽어보시기 바랍니다.

C 프로그램을 컴파일(COMPILE)할 때 다음과 같은 여러 단계를 거치게 됩니다.

- 전처리기(PREPROCESSOR) : C 파일(*.c)

- C 컴파일러(COMPILER) : 전처리된 C 파일

- 링커(LINKER) : 목적(OBJECT) 프로그램(*.obj)

- 실행 파일(*.exe)

전처리기는 텍스트를 텍스트로 변환하는 순수한 텍스트 처리기로 C 프로그램을 받아 C 프로그램을 결과로 만듭니다.

전처리기는 전처리기 지시자(DIRECTIVES)라고 불리는 프로그래밍 명령어들을 처리하는 역할을 하며

또한 주석(COMMENT)을 지우는 역할을 합니다.

따라서 C 컴파일러에는 전처리기를 거친 순수한 C 프로그램이 전달되게 됩니다.

참고로 많이 사용하고 있는 용어인 프리프로세서문(PREPROCESSOR STATEMENT)는

전처리기 지시자를 사용한 문장을 의미합니다.

전처리기 지시자는 반드시 '#'으로 시작하도록 되어 있습니다.

지금부터는 다양한 종류의 전처리기 지시자들을 알아보도록 하겠습니다.

1. 소스 파일을 포함하기(Source File Inclusion)

가장 많이 사용하는 "#include" 지시자에 관한 내용입니다.

아래의 간단한 [예제 1]를 이용하여 "#include" 지시자에 대해서 설명을 하겠습니다.

[예제 1]

#include <stdio.h>

int main(void)

{

printf("Hello, world!\n");

return 0;

}

[예제 1]에서

전처리기는 "#include" 지시자를 만났을 ??

"#include <stdio.h>" 문장을 stdio.h의 이름을 가지는 시스템 헤더 파일로 바꾸어 버립니다.

더 정확히 말하자면,

전처리기는 stdio.h 파일의 전체 내용을 "#include <stdio.h>" 문장이 있는 위치에 옮겨 버립니다.

참고로 stdio.h 파일에는 printf() 함수가 정의되어 있습니다.

전처리기 지시자 "#include"는 다음과 같은 두 가지 형태로 사용합니다.

(1) #include <파일 이름>

(2) #include "파일 이름"

(1)의 경우는 C에서 미리 정의된 디렉토리(Standard Directory Include Path)에서 파일을 찾게 되고

(2)의 경우는 현재의 디렉토리(Current Source Directory)에서 파일을 찾게 됩니다.

일반적으로 (2)의 경우는 프로그래머가 파일을 생성하는 경우에 해당하겠지요.

찾고자 하는 파일이 없다면, 두 경우 모두 에러가 발생합니다.

일반적으로 <파일 이름> 또는 "파일 이름"의 파일은 *.h의 확장자를 사용하지만 다른 확장자를 사용하는 것도 가능합니다.

확장자에 대한 제약이 없습니다.

프로그래머가 프로그래밍을 하다보면 "#include"가 중복(Double Inclusion)되는 경우가 종종 발생합니다.

[예제 2]

grandfather.h

struct foo {

int member;

};

father.h

#include "grandfather.h"

child.c

#include "grandfather.h"

#include "father.h"

[예제 2]의 경우

"child.c"를 컴파일을 하면 "foo"라는 타입이 두 번 정의(Double Inclusion)가 되어서 컴파일 에러가 발생합니다.

위와 같은 상황을 방지하기 위해 "grandfather.h"를 [예제 3]과 같이 변경을 합니다.

[예제 3]

grandfather.h

#ifndef GRANDFATHER_H

#define GRANDFATHER_H

struct foo {

int member;

};

#endif

또는

#pragma once

struct foo {

int member;

};

[예제 3]에서 새로운 전처리기 지시자들인 "#ifndef", "#define", "#endif", "#pragma" 등은 아래에서 설명할 예정입니다.

2. 조건부 컴파일(Conditional Compilation)

조건부 컴파일을 위해서 사용하는 전처리기 지시자들에는 "#if", "#ifdef", #ifndef", "#else", "#elif", "#endif" 등이

있는데 이들은 단독으로 사용이 불가능합니다.

- "#if" 지시자는 "#endif" 지시자와 함께 사용해야 합니다.

- "#elif", "#else" 지시자는 "#if" 지시자와 함께 사용할 때만 사용 가능합니다.

- "#ifdef", "#ifndef" 지시자는 "#endif" 지시자와 함께 사용해야 합니다.

위의 전처리기 지시자들은 보기만 해도 쉽게 이해가 가능할 것으로 보이기 때문에 간단한 설명으로 끝내고자 합니다.

먼저 "#if", "#elif", "#else", "#endif" 등의 전처리기 지시자에 대한 설명을 하겠습니다.

[예제 4]

#if (EX_MACRO > 2) /* EX_MACRO는 "#define" 전처리기 지시자에 의해 정의가 되어 있어야 합니다. */

A1 /* 문장 또는 문장들 */

#elif (EX_MACRO == 1) /* elif : else if */

A2 /* 문장 또는 문장들 */

#else

A3 /* 문장 또는 문장들 */

#endif

[예제 4]에 대해 간단한 설명을 하겠습니다.

[예제 4]는 "#if" 지시자 다음에 있는 조건(예제의 경우 "(EX_MACRO > 2)")이 참이라면 A1의 내용을 C 컴파일러에게

전달하고 거짓이라면 "#elif" 지시자 다음에 있는 조건(예제의 경우 "(EX_MACRO == 1)")을 체크하여 참이라면

A2의 내용을 C 컴파일러에게 전달하고 거짓이라면 "#else" 지시자의 A3 내용을 C 컴파일러에게 전달합니다.

위 예제에서 더 많은 조건들을 사용하고자 한다면 "#elif" 지시자를 여러 번 사용하면 되고,

간단한 조건을 사용하고자 한다면 "#elif", "#else" 지시자들을 하나 또는 모두 빼면 됩니다.

"#if", "#elif" 지시자에서 사용하는 조건은 아래와 같은 형태를 가질 수 없음에 주의해야 합니다.

[예제 4]

#if (EX_MACRO > 2.0) /* 실수를 이용한 조건은 안됩니다. */

A1 /* 문장 또는 문장들 */

#endif

#if (EX_MACRO == "OK") /* 문자열(STRING)을 이용한 조건은 안됩니다. */

A2 /* 문장 또는 문장들 */

#endif

아래에는 "#ifdef", "#ifndef" 등의 전처리기 지시자에 대한 설명을 하겠습니다.

[예제 6]

#ifdef EX_MACRO /* EX_MACRO가 "#define" 전처리기 지시자에 의해 정의가 되어 있는지 ? */

B1 /* 문장 또는 문장들 */

#endif

#ifndef EX_MACRO /* EX_MACRO가 "#define" 전처리기 지시자에 의해 정의가 되어 있지 않은지 ? */

B2 /* 문장 또는 문장들 */

#endif

[예제 6]에서는 "#ifdef" 지시자 다음의 매크로(MACRO, 예제의 경우 "EX_MACRO)가 정의되어 있으면

B1의 내용을 C 컴파일러에게 전달합니다. 그리고 "#ifndef" 지사자 다음의 매크로(MACRO, 예제의 겨우 "EX_MACRO)가

정의되어 있지 않으면 B2의 내용을 C 컴파일러에게 전달합니다.

여기서 매크로(MACRO)는 "#define" 전처리기 지시자를 이용하여 정의를 하는 것입니다.

3. 매크로 정의(MACRO DEFINITION)

매크로 정의는 매개변수(PARAMETER)의 유무에 따라서 다음과 같은 두 가지 형태를 가지고 있습니다.

(1) #define 매크로_이름 매크로_의미

: Object-like MACRO

(2) #define 매크로_이름(매개변수) 매크로_의미

: Function-like MACRO

: 일반적으로 매크로 함수라는 용어를 사용합니다.

: 이와 같은 형태의 매크로 정의를 사용할 때 주의할 점이 있습니다.

매크로_이름과 '(' 는 반드시 붙여서 사용해야 합니다.

매크로_이름과 '(' 사이에 여백이 있다면 (2)의 매크로 정의는 (1)의 매크로 정의과 같이 취급을 합니다.

전처리기는 소스 파일에서 "#define" 지시자에 의해 정의된 매크로_이름이 나타날 때마다

매크로_이름의 위치에 매크로_의미에 해당하는 내용을 그대로 바꾸어서

이 결과을 C 컴파일러에게 전달합니다.

참고로 매크로_이름에 해당하는 매크로_의미가 없다면 기술하지 않아도 상관이 없습니다.

몇 가지 예제를 이용하여 매크로 정의 및 주의할 점에 대해서 이해해 보겠습니다.

[예제 7] Object-like MACRO

#define PI 3.141592

...

area = PI * r * r; /* 원의 넓이 */

...

원의 넓이를 구하는 식인 [예제 7]에서

전처리기는 PI(매크로_이름)의 위치에 3.141592(매크로_의미)의 내용을 [예제 8]과 같이 바꾸어서 C 컴파일러에게

전달합니다. 따라서 C 컴파일러는 PI라는 값에 대해서 알지 못하게 됩니다.

[예제 8]

...

area = 3.141592 * r * r;

...

라디안(RADIAN) 값을 각도(DEGREE) 값으로 변경하는 매크로 함수의 예제([예제 9])를 보겠습니다.

[예제 9] Function-like MACRO

#deifne RADTODEG(x) (x * 57.259791)

...

deg = RADTODEG(rad); /* RADIAN -> DEGREE */

...

전차리기가 [예제 7]을 [예제 8]로 바꾸는 것과 마찬가지로

전처리기는 [예제 9]를 [예제 10]으로 바꾸어서 C 컴파일러에게 전달합니다.

마찬가지로 C 컴파일러는 RADTODEG(x)란 내용을 알지 못하게 됩니다.

[예제 10]

...

deg = (rad * 57.259791);

...

매크로 함수에 대해서 주의해야 할 내용을 하나 살펴 보겠습니다.

[예제 9]에서 "deg = RADTODEG(rad)" 문장을 "deg = RADTODEG(rad_1 + rad_2)" 문장의 형태로 사용했을

어떤 결과가 나타날까요?

위에서 설명하기를 전처리기는 "#define" 지시자에 의해 정의된 매크로_이름이 나타날 때마다

매크로_이름의 위치에 매크로_의미에 해당하는 내용을 그대로 바꾼다고 했습니다.

설명에 따라서 전처리기는 "deg = RADTODEG(rad_1 + rad_2)" 문장은 다음과 같이 바꿀 것입니다.

"deg = (rad_1 + rad_2 * 57.259791)"

위 문장은 "rad_2 * 57.259791"을 먼저 계산한 후 그 결과를 "rad_1"의 값에 더해서 "deg"에 저장합니다.

이 내용은 C 문법 중 우선순위에 관한 내용입니다.

프로그래머가 원하는 결과가 위의 결과였을까요?

프로그래머가 "rad_1 + rad_2"를 먼저 계산한 후 그 결과를 "57.259791"에 곱한 결과를 원했다면

프로그래머의 의도와는 전혀 다른 결과가 나타나게 될 것입니다.

위의 생각이 일반적일 것이라고 봅니다(프로그래머의 의도에 따라 다를 수도 있습니다).

그러면 어떻게 해야할 것일까요?

[예제 11]

#deifne RADTODEG(x) ((x) * 57.259791)

...

deg = RADTODEG(rad_1 + rad_2);

...

[예제 9]의 "#define" 지시자가 있는 문장을 [예제 11]과 같은 문장으로 변경한다면 프로그래머의 의도대로 될 것입니다.

간단한 내용이지만 주의해야 할 사항입니다.

다음은 매크로_의미가 여러 줄을 이용하여 표현될 경우를 보겠습니다.

위와 같은 상황이 발생했을 경우

각 줄의 끝에 '\'를 추가하여 매크로_의미가 여러 줄을 가진다는 것을 전처리기에 알려줍니다.

[예제 12]는 한 줄로도 가능한 매크로 정의를 여러 줄을 이용하여 표현할 수 있음을 보여줍니다.

[예제 12]

#define MIN(a, b) ((a) > (b) ? (b) : (a))

위의 정의를

#define MIN(a, b) \

((a) > (b) ? \

(b) : (a))

와 같이 표현할 수 있습니다.

마지막으로 C에서 미리 정의된 매크로를 몇 개 설명하고 매크로 정의에 대한 설명을 마치도록 하겠습니다.

프로그램들을 보다가 다음과 같은 매크로들을 여러 번 보았을 것입니다.

(1) __FILE__ : 파일 이름, 문자 스트링(STRING)

(2) __LINE__ : 파일의 현재 줄 번호, 문자 정수(INTEGER)

(3) __DATE__ : 컴파일할 때의 날짜, 문자 스트링(STRING)

(4) __TIME__ : 컴파일할 때의 시간, 문자 스트링(STRING)

[예제 13]

...

fprinft(stderr, "[FILE %s, LINE %d]\n", __FILE__, __LINE__);

...

fprinft(stderr, "[DATE %s, TIME %s]\n", __DATE__, __TIME__);

그 외에도 C에서 미리 정의된 매크로들이 많이 있습니다.

4. 전처리문 지시자에 사용할 수 있는 연산자들

전처리문 지시자에 사용할 수 있는 연산자들에는 다음과 같은 것들이 있습니다.

- # : STRINGIZING OPERATOR

- ## : TOKEN CONCATENATION

- defined

'#' 연산자는 바로 뒤의 인자를 스트링으로 바뀌어 주는 역할을 합니다.

[예제 14]

#define QUOTEME(x) #x

...

fprintf(stderr, "%s\n", QUOTEME(1+2));

...

의 경우 "QUOTEME(1+2)"가 있는 문장은 "1+2"가 스트링으로 변경되어 다음과 같습니다.

fprint(stderr, "%s\n", "1+2");

'##' 연산자는 'x##y'의 형태로 사용하는데 x와 y를 붙여 하나의 이름으로 만듭니다.

[예제 15]

#define CON(x, y) (x##y)

...

CON(i, 1) = 100;

...

의 경우 "CON(i, 1)"이 있는 문장은 다음과 같이 바뀝니다.

i1 = 100;

[예제 16]

#define MYCASE(item, id) \

case id: \

item##_##id = id; \

break

...

switch (x) {

MYCASE(widget, 23)

}

...

의 경우 "MYCASE(widget, 23)" 문장은 다음과 같이 바뀝니다.

case 23:

widget_23 = 23;

break;

[예제 17] 아래의 예제를 테스트 해 보시기 바랍니다.

enum {
OlderSmall = 0,
NewerLarge = 1
};
#define Older Newer
#define Small Large
#define _replace_1(Older, Small) Older##Small
#define _replace_2(Older, Small) _replace_1(Older, Small)
...
void printout(void)
{
/* _replace_1(Older, Small) becomes OlderSmall (not NewerLarge),
despite the #define calls above. */
printf("Check 1: %d\n", _replace_1(Older, Small));
/* The parameters to _replace_2 are substituted before the call
to _replace_1, so we get NewerLarge. */
printf("Check 2: %d\n", _replace_2(Older, Small));
}

...

위 예제의 결과는 다음과 같습니다.

Check 1: 0
Check 2: 1

마지막으로 "defined" 연산자는 "#if" 문에서 사용하는 것으로 [예제 18]과 같이 사용합니다.

[예제 18]

#if defined(EX_MACRO) /* EX_MACRO가 "#define" 전처리기 지시자에 의해 정의가 되어 있는지 ? */

A /* 문장 또는 문장들 */

#endif

이것은 연산자 이름을 보면 알 수 있듯이

"defined" 연산자 다음의 매크로(예제의 경우 "EX_MACRO)가 정의되어 있으면

A의 내용을 C 컴파일러에게 전달합니다.

[예제 18]의 경우는 "#ifdef" 지사자와 동일한 기능을 합니다.

"#ifndef" 지시자와 비슷한 기능을 하는 것이 "!defined"의 형태입니다(여기서 '!'는 "NOT"을 의미합니다).

그러나 여러 개의 매크로들이 정의되어 있는지를 판단하고자 할 때에는 [예제 19]의 경우와 같이

"defined" 연산자를 사용하는 편리한 경우가 많습니다.

[예제 18]

/* EX_MACRO_1, EX_MACRO_2가 "#define" 전처리기 지시자에 의해 정의가 되어 있고

EX_MACRO_3가 "#define" 전처리기 지시자에 의해 정의가 되어 있지 않은지 ? */

#if defined(EX_MACRO_1) && defined(EX_MACRO_2) && !defined(EX_MACRO_3)

A /* 문장 또는 문장들 */

#endif

5. 사용자 정의의 컴파일 에러(ERROR) 및 경고(WARNING) 메시지

"#error" 지시자는 전처리기에게 에러 메시지를 출력시키고 컴파일 작업을 하지 않도록 하는 것입니다.

[예제 19]

#ifdef WINDOWS
... /* 윈도우즈 관련 코드 */
#elif defined(UNIX)
... /* 유닉스 관련 코드 */
#else
#error "What's your operating system?"
#endif

[예제 19]는 "WINDOWS" 및 "UNIX"란 매크로가 정의되어 있지 않으면 "What's your operating system?"란 에러 메시지를
출력시키고 컴파일 작업을 멈춥니다.

"#warning" 지시자는 전처리기에게 경고 메시지를 출력시키고

"#error" 지시자와는 다르게 컴파일 작업을 계속 하도록 하는 것입니다.

[예제 20]

#ifdef ABC

... /* 문장 또는 문장들 */
#else
#warning "Do not use ABC, which is deprecated. Use XYZ instead."
#endif

[예제 20]은 "ABC"란 매크로가 정의되어 있지 않으면 "Do not use ABC, which is deprecated. Use XYZ instead."란

경고 메시지를 출력시키고 컴파일 작업은 계속 진행합니다.

6. 그 외의 전처리문 지시자들

- #undef

- #inline

- #pragma

"#undef" 지시자는 "#define" 지시자와는 정반대의 역할을 하는 것으로 이미 정의된 매크로를 취소시켜서

정의되지 않은 상태로 만들고자 할 때 사용합니다.

[예제 21]

#define SIZE 100
i = SIZE;
#undef SIZE
i = SIZE;
[예제 21]에서 처음의 "i = SIZE;"를 수행할 때에는 SIZE가 100으로 정의가 되어 있으므로 "i = 100;"과 같지만
두 번쩨의 "i = SIZE"는 "#undef SIZE"에 의해 SIZE의 정의가 취소되었기 때문에 에러가 발생합니다.
"#undef" 지시자를 사용하면 프로그램의 부분에 따라서 같은 이름의 매크로를 다른 의미로 지정하여
사용할 수도 있습니다([예제 22]).

[예제 22]

#define SIZE 100
i = SIZE;
#undef SIZE
#define SIZE 200
i = SIZE;
만약 [예제 22]에사 "#undef SIZE"를 사용하지 않고 [예제 23]과 같이 사용하게 되면
SIZE를 중복해서 정의한 것이 되므로 컴파일할 때 에러가 발생합니다.

[예제 23]

#define SIZE 100
i = SIZE;
#define SIZE 200
i = SIZE;
"#line" 지시자는 사용자를 위한 문장이기 보다는 컴파일러 자체를 위한 지시자입니다.
[예제 24] "test.c"
#include <stdio.h> /* test.c의 1번 라인 */
int main(void) /* test.c의 2번 라인 */
{ /* test.c의 3번 라인 */
int x, y; /* test.c의 4번 라인 */
x = 1; /* test.c의 5번 라인 */
#line 100 "main.c" /* main.c의 100번 라인 */
y = 1; /* main.c의 101번 라인 */
fprintf(stderr, "%d, %d\n", x, y); /* main.c의 102번 라인 */
} /* main.c의 102번 라인 */

[예제 24]는 "test.c"라는 파일에 대해서 "#line" 지시자를 사용한 예를 보여줍니다.

처음 5줄을 처리할 때는 파일의 이름이 "test.c"가 되고 라인 번호도 차례대로 1, 2, ... 5가 됩니다.

그런데 "line 100 "main.c""를 만나면서 파일의 이름은 "main.c"로 바뀌고 라인 번호도 6이 아닌 100부터 시작하게 됩니다.

마지막으로 "#pragma" 지시자는 컴파일러에 따라 다를 수 있기 때문에

"#pragma" 지시자에 대해서는 컴파일러에 대해 설명해 놓은 문서를 참고하기기 바랍니다.

지금까지 C 전처리기(PREPOCESSOR) 지시자(DIRECTIVES)에 대해서 설명하였습니다.

저도 C에 대해서는 초보자 수준이기 때문에 제가 공부한다는 생각으로 정리를 하였습니다.

모자란 부분이 많겠지만

이 글을 읽으시는 분들께 도움이 되었으면 합니다.

C에 대해 고수분들 또는 어떤 분들께서라도 잘못된 내용들이 있거나

도움을 주실 내용들이 있다면 따가운 충고 부탁드리겠습니다.

 

 

 

출처 : http://nmshome.tistory.com/entry/C%EC%A0%84%EC%B2%98%EB%A6%AC%EA%B8%B0-%EC%A0%84%EC%B2%98%EB%A6%AC%EA%B8%B0%EB%A5%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90

'IT > C' 카테고리의 다른 글

TODO, FIXME, XXX 태그의 의미  (0) 2017.05.18
poll과 select에는 대기큐가 존재한다.  (0) 2014.05.08
memcpy와 memmove의 차이점  (0) 2014.05.07

+ Recent posts