C언어는 몇몇의 이미 정의된(predefined) macro를 가지고 있다. 각각의 macro는 정수형 상수나 문자열 리터럴을 표현한다. 아래의 테이블이 보여주듯, 이러한 macro들은 현재의 컴파일에 대한 정보를 제공하거나 컴파일러 자체에 대한 정보를 제공한다.
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)
__DATE__
와 __TIME__
macro는 프로그램이 언제 컴파일되었는지 식별한다. 예를 들어 아래의 구문으로 시작하는 프로그램을 생각해보자.
printf("Wacky Windows (c) 2010 Wacky Software, Inc.\n");
printf("Compiled on %s at %s\n" __DATE__, __TIME__);
프로그램이 시작될때마다 프로그램은 아래의 행을 출력한다.
Wacky Windows (c) 2010 Wacky Software, Inc.
Compiled on Dec 23 2010 at 22:18:48
이 정보는 같은 프로그램의 다른 version들을 구분하는 것에 도움이 된다.
__LINE__
과 __FILE__
mcaro는 에러를 발견하는 것에 도움을 주기 위해 사용할 수 있다.
division by zero의 위치를 찾는 문제를 생각해보자. C 프로그램이 divided by zero 때문에 빨리 종료되었을 때, 어떠한 divison이 문제를 발생시켰는지에 대해 보통 나타내지 않는다. 아래의 macro는 에러의 소스를 핀포인트(pin-point)로 알려주는 것에 도움을 준다.
#define CHECK_ZERO(divisor) \
if (divisor == 0) \
printf("*** Attempt to divide by zero on line %d " \
"of file %s ***\n", __LINE__, __FILE__)
아래의 CHECK_ZERO
macro가 나눗셈(division)보다 먼저 발생할(invoked) 것이다.
CHECK_ZERO(j);
k = i / j;
j
가 0이 되는 일이 있다면, 아래의 형태의 메시지가 출력된다.
*** Attempt to divide by zero on line 9 of file foo.c ***
이와 같은 에러 감지(error-detecting) macro는 꽤 유용하다. 사실, C 라이브러리는 일반적인 목적의 에러 감지 macro인 assert
를 가지고 있다.
__STDC__
macro가 존재하고 1의 값을 가진다면, 컴파일러는 C 표준(C89, C99 모두)에 속한다. 전처리기가 이 매크로를 검사하는 것으로, 프로그램은 C89보다 더 이전의 컴파일러에 적응할 수 있다(Section 14.4에서 예시를 볼 것이다).
C99는 많은 추가적인 predefined macro를 제공한다.
Name Description
__STDC__HOSTED__ 1 if this is a hosted implementaion; 0 if it is free standing
__STDC__VERSION__ Version of C standard supported
__STDC__IEC_559__ 1 if IEC 60559 floating-point arithmetic is supported
__STDC_IEC_559_COMPLEX__ 1 if IEC 60559 complex arithmetic is supported
__STDC_ISO_10646__ yyyymmL if wchar_t values match the ISO 10646 standard of the
specified year and month
__STDC__HOSTED__
의 의미를 이해하기 위해서는, 우리는 새로운 단어가 필요하다. C의 구현(implementation)은 컴파일러와 C 프로그램을 실행하기 위해 필요한 다른 소프트웨어로 구성된다. C99는 implementaion을 2개의 카테고리로 나누는데, hosted와 freestanding이다. hosted implementation은 반드시 C99의 표준에 따르는 프로그램을 받아들여야 하고, 반면에 freestanding implementation은 가장 기본적인 것을 넘어서는 complex 자료형이나 표준 헤더를 사용한 프로그램을 컴파일할 수 없다.(특히, freestanding implementation은 <stdio.h>
헤더를 지원할 수 없다.) __STDC__HOSTED__
macro는 컴파일러가 hosted implementation이면 상수 1을 표현한다. 그렇지 않다면 macro의 값은 0이다.
__STDC__VERSION__
macro는 컴파일러가 인지한 C 표준의 version이 무엇인지 확인하는 방법을 제공한다. C89 표준 수정안 1(Amendment 1)에서 처음 등장했는데, 이 macro의 값은 long integer constant 199409L
로 명시되었다(수정안의 year과 month를 표현). 만약 컴파일러가 C99 표준에 따른다면, 이 값은 199901L
이다. 표준의 후속 version들에 대해서는 이 macro가 각각 다른 값을 가질 것이다.
C99 컴파일러는 추가적인 3가지 macro를 정의할 수도 있다(없을 수도 있다). 각각의 macro는 컴파일러가 특정한 조건을 만족할 때만 정의된다.
__STDC_IEC_559__
는 컴파일러가 IEC 60559 표준(IEEE 754 표준의 또다른 이름)에 따르는 floating-point arithmetic을 수행한다면 정의되고, 그 값은 1이다.__STDC_IEC_559_COMPLEX__
는 컴파일러가 IEC 60559 표준에 따르는 complex arithmetic을 수행한다면 정의되고, 그 값은 1이다.__STDC_ISO_10646__
은 wchar_t
자료형의 값이 ISO/IEC 10646 표준 코드로 표현될 때, yyyymmL
의 형태인 정수형 상수로써 정의된다.C99는 macro 내부의 어떠한 argument나 모든 argument가 빈 상태로 호출되는 것을 허용한다. 그러나 이러한 호출은 일반적인 호출과 동일한 개수의 comma(,
)를 포함한다.(이 방법으로, argument가 생략된 것을 쉽게 확인할 수 있다)
대부분의 경우에, 빈 argument의 효과는 명확하다. replacement-list
에 나타나는 parameter 이름에 대응될 때마다 아무것도 대체되지 않는다. 간단하게 replacement-list
로부터 사라진다. 아래의 예시를 보자.
#define ADD(x,y) (x+y)
i = ADD(j,k);
위의 구문은 전처리이후에 아래의 구문이 된다.
i = (j+k);
i = ADD(,k);
반면에 위의 구문은 아래의 구문이 된다.
i = (+k);
빈 argument가 #
또는 ##
연산자의 피연산자일 때, 특별한 규칙이 적용된다. 만약 빈 argument가 #
연산자에 의해 "stringized"되었다면, 결과는 ""이다(빈 string).
#define MK_STR(x) #x
...
char empty_string[] = MK_STR();
전처리과정 이후에 위의 선언은 아래와 같은 모습을 보인다.
char empty_string[] = "";
만약 ##
연산자의 argument 중 하나가 비어있다면, 보이지 않는 "placemarker" token으로 대체된다. placemarker token과 일반적인 token을 잇는 것(concatenating)은 원래의 token을 만들어낸다(placemarker가 사라진다). 만약 두 개의 placemarker가 이어졌다면, 결과는 단일 placemarker이다. 일단 macro 확장이 완료되고 나면, placemarker token은 프로그램으로부터 사라진다. 아래의 예시를 보자.
#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;
비어있는(missing) argument들은 placemarker token에 의해 대체되는데, 어떠한 비어있지 않은 argument와 함께 이어졌을 때 placemarker token이 사라진다. JOIN
macro에 있는 모든 3개의 argument들이 비어있을 때, 비어있는(empty) 결과를 생산한다.
C89에서는, macro가 가지는 argument의 개수가 반드시 고정되어있어야 한다. C99에서는 상황이 약간 완화되어, macro가 argument의 개수를 무제한으로 가질 수 있다. 이 특징은 함수에도 이용가능하기 때문에 macro가 마침내 같은 위치에 놓이게 된 것이 전혀 놀랍지 않다.
macro가 argument의 개수를 variable하게 가질 수 있게 된 주요한 이유는 printf
나 scanf
같은 argument의 개수를 variable하게 가질 수 있는 함수에 이러한 argument들을 전달하기 위해서이다. 아래의 예시를 보자.
#define TEST(condition, ...) ((condition)? \
printf("passed test: %s\n", #condition): \
printf(__VA_ARGS__))
일반적인 parameter보다 더 앞에 오고, macro의 parameter list의 끝부분에 나오는 ...
token은 ellipsis(줄임말)로도 알려져있다. __VA_ARGS__
는 variable한 argument의 개수를 가지는 macro의 replacement-list
에서만 나타나는 특별한 식별자(identifier)이다. 이 식별자는 ellipsis에 대응되는 arguments를 나타낸다(비록 argument가 비어있더라도 반드시 ellipsis에 대응되는 최소 한 개 이상의 argument가 있어야 한다). TEST
macro는 최소 2개의 argument를 필요로 한다. 첫번째 argumnet는 condition
parameter와 일치한다. 남은 argument는 ellipsis와 일치한다.
아래의 예시는 TEST
macro가 어떻게 사용되는지 보여준다.
TEST(voltage <= max_voltage,
"Voltage %d exceeds %d\n", voltage, max_voltage);
전처리기는 아래와 같은 output을 생성한다.(가독성을 위해 다시 구성함)
((voltage <= max_voltage)?
printf("Passed test: %s\n", "voltage <= max_voltage"):
printf("Voltage %d exceeds %d\n", voltage, max_voltage));
프로그램이 실행되었을 때, voltage
가 max_voltage
보다 작다면 프로그램은 아래의 메시지를 출력한다.
Passed test: voltage <= max_voltage
만약 그렇지 못한다면, voltage
와 max_voltage
의 값을 아래와 같은 형태로 출력한다.
Voltage 125 exceeds 120
__func__
IdentifierC99의 또다른 새로운 특징은 __func__
식별자(identifier)이다. __func__
는 전처리기와 어떠한 관련도 없지만, 그래서 실질적으로는 이 chapter에 속하지는 않는다. 그러나 많은 전처리기의 특징과 비슷하게, 이것은 디버깅에 유용하고 그래서 이 주제를 알아볼 것이다.
모든 함수는 __func__
식별자에 접근하는데, __func__
는 현재 실행하고 있는 함수의 이름을 저장하는 문자열 변수(string variable)와 비슷하게 행동한다. 각각의 함수가 아래의 선언을 body 내부의 시작부분에 포함한 것과 같은 효과이다.
static const char __func__[] = "function-name";
function-name
의 자리는 함수의 이름이 들어간다. 이 식별자의 존재는 macro를 아래와 같이 디버깅하는 것이 가능하도록 만든다.
#define FUNCTION_CALLED() printf("%s called\n", __func__);
#define FUNCTION_RETURNS() printf("%s returns\n", __func__);
이러한 macro의 호출은 함수의 호출을 추적하기 위해 사용될 수 있다.
void f(void)
{
FUNCTION_CALLED(); /* displays "f called" */
...
FUNCTION_RETURNS(); /* displays "f returns" */
}
__func__
는 함수에 전달되어 이것을 호출한 함수의 이름을 알려준다.
C 전처리기는 conditional compilation
을 지원하는 많은 directive를 인정한다. conditional compilation을 통해 전처리기가 수행하는 검사의 결과로 프로그램 텍스트의 섹션을 포함하거나 제외시킨다.
#if
and #endif
Directives우리가 프로그램 디버깅과정에 있다고 생각해보자. 프로그램이 특정한 변수들의 값을 출력하도록 하기 위해 프로그램의 중요한 부분에 printf
의 호출을 넣었다. 버그를 찾아내고 나면, 나중에 필요할 때를 대비하여 printf
의 호출을 남겨놓는 것은 좋은 생각이다. conditional compilation은 호출을 그 위치에 남겨도 되게 하지만, 컴파일러가 이것을 무시하게 한다.
이 과정이 어떻게 수행되는지 보자. 첫번째로, macro를 정의하여 0이 아닌 값을 부여한다.
#define DEBUG 1
macro의 이름은 문제가 되지 않는다. 다음으로, printf
호출을 #if
-#endif
쌍으로 그룹을 만들자.
#if DEBUG
printf("Value of i: %d\n", i);
printf("Value of j: %d\n", j);
#endif
전처리과정도중에, #if
directive는 DEBUG
의 값을 검사할 것이다. DEBUG
의 값이 0이 아니기 때문에, 전처리기는 두 개의 printf
호출을 프로그램 안에 남길 것이다(그러나 #if
와 #endif
행은 사라질 것이다). 만약 DEBUG
의 값을 0으로 바꾸고 다시 프로그램을 컴파일 한다면, 전처리기는 프로그램으로부터 위의 4개의 행을 전부 제거할 것이다. 컴파일러는 printf
의 호출을 보지 못하기 때문에, printf
의 호출은 object code에 어떠한 공간도 차지하지 않을 것이고 프로그램이 동작하는 동안 어떠한 비용(cost)도 사용하지 않는다. 우리는 #if
-#endif
블록을 최종 프로그램에 남기는 것으로, 나중에 어떠한 문제가 발생하였을 때 진단에 사용할 정보(diagnostic information)를 생성하도록 허용할 수 있다(DEBUG
를 1로 설정하고 다시 컴파일하는 것으로).
일반적으로 #if
directive는 아래와 같은 형태를 가진다.
#if constant-expression
#endif
는 더 간단하다.
#endif
전처리기가 #if
directive를 마주쳤을 때, 전처리기는 상수 표현식을 평가할 것이다. 만약 표현식의 값이 0이라면, #if
와 #endif
사이의 행들은 전처리과정 중에 프로그램에서 모두 제거될 것이다. 그렇지 않다면, #if
와 #endif
사이의 행들은 컴파일러에 의해 처리된 프로그램 안에 남아있을 것이다. #if
와 #endif
자체는 프로그램에 어떠한 영향도 미치지 않는다.
#if
direcitve가 0의 값을 가진 매크로처럼 정의되지 않은 identifiers을 처리한다는 점에서 주목할만하다. 그래서 만약 DEBUG
정의를 무시하기 위해서, 아래의 검사는 실패할 것이다.(그렇지만 에러메시지가 발생하지 않는다)
#if DEBUG
하지만 아래의 검사는 성공할 것이다.
#if !DEBUG
defined
Operator우리는 Section 14.3에서 #
과 ##
연산자를 알아보았었다. 여기에 전처리기에 특정되는 defined
라는 또다른 하나의 연산자가 있다. identifier에 적용되었을 때, identifier가 현재 정의된 macro인 경우 defined
는 1의 값을 생성한다. 그렇지 않다면 0의 값을 생성한다. defined
연산자는 일반적으로 #if
directive와 결합되어 사용된다.
#if defined(DEBUG)
...
#endif
#if
와 #endif
directive 사이의 행은 DEBUG
가 macro로써 정의되어있을 때에만 프로그램에 포함될 것이다. DEBUG
를 둘러싸는 괄호는 필요하지는 않다. 아래와 같이도 작성할 수 있다.
#if defined DEBUG
defined
가 DEBUG
가 정의가 되었는지 아닌지만 검사하기 때문에, DEBUG
에 특정한 값을 주는 것은 필수적이지 않다.
#define DEBUG
#ifdef
and #ifndef
Directives#ifdef
directive는 identifier가 현재 macro로써 정의되었는지 검사한다.
#ifdef identifier
#ifdef
는 #if
를 사용하는 것과 비슷하다.
#ifdef identifier
Lines to be included if identifier is defined as a macro
#endif
엄밀히 말해서 #ifdef
는 필요하지 않은데, 왜냐하면 #if
directive와 defined
연산자를 결합하는 것으로 동일한 효과를 얻을 수 있기 때문이다.
#ifdef identifier
#if defined(identifier)
다른말로 말하면, 전자와 후자의 directive는 동일하다.
#ifndef
는 #ifdef
와 비슷하지만, identifier가 macro로써 정의되지 않았는지 검사한다.
#ifndef identifier
#ifndef identifier
#if !defined(identifier)
전자와 후자의 구문은 동일하다.
#elif
and #else
Directives#if
, #ifdef
, #ifndef
블록은 일반적인 if
구문과 비슷하게 중첩될 수 있다.
중첩이 일어났을 때, 중첩된 만큼 들여쓰기의 개수를 늘리는 것이 좋다. 몇몇 프로그래머들은 각각의 #endif
에 #if
가 검사하는 조건과 일치하는 것을 주석으로 나타낸다.
#if DEBUG
...
#endif /* DEBUG */
이러한 기술은 코드를 읽는 사람이 #if
블록의 시작부분을 찾기 쉽도록 만든다.
추가적인 편리함을 위해, 전처리기는 #elif
와 #else
directive를 지원한다.
#elif constant-expression
#else
#elif
과 #else
는 #if
, #ifdef
, #ifndef
와 결합하여 사용하는 것으로 일련의 조건들을 검사할 수 있다.
#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
directive를 보여주었지만, #ifdef
나 #ifndef
directive가 대신 사용될 수 있다. #if
와 #endif
사이의 #elif
directive 개수에는 상관이 없지만, #else
는 하나여야한다.
conditional compilation은 디버깅에 특히 편리하지만, 디버깅에만 사용되는 것은 아니다. 다른 일반적인 적용의 사례도 많다.
WIN32
, MAC_OS
, LINUX
중 어떤 것이 정의 되어 있는지에 의존하여 프로그램에 포함한다.#if defined(WIN32)
...
#elif defined(MAC_OS)
...
#elif defined(LINUX)
...
#endif
프로그램은 이러한 #if
블록을 많이 포함할 수 있다. 프로그램의 초반 부분에, macro중 하나가 정의되어 있을 것이고 그러므로 특정한 operating system을 선택할 수 있다. 예를 들어 LINUX
macro를 정의하는 것은 프로그램이 Linux perating system에서 작동한다는 것을 나타낸다.
__STDC__
macro는 컴파일러가 표준(C89나 C99)에 따르는지 전처리기가 감지하도록 한다. 만약 그렇지 않다면, 우리는 프로그램의 특정한 부분을 바꿀 필요가 생긴다. 특히, 함수 prototype 대신에 오래된 스타일의 함수 선언을 사용해야만 할 수도 있다(Chapter 9 끝부분의 Q&A에서 본 것처럼). 이러한 함수가 선언된 지점마다, 우리는 아래의 행을 넣을 수 있다.#if __STDC__
Function prototypes
#else
Old-style function declarations
#endif
BUFFER_SIZE
가 정의되지 않았다면 BUFFER_SIZE
를 정의하도록 한다.#ifndef BUFFER_SIZE
#define BUFFER_SIZE 256
#endif
/*...*/
주석을 포함하고 있는 코드를 "주석 처리(comment out)"하기 위해 /*...*/
를 사용할 수 없다. 대신에 우리는 #if
directive를 사용할 수 있다.#if 0
Lines containing comments
#endif
이러한 방법으로 코드를 비활성화하는 것은 자주 "conditioning out"이라고 불린다.
Section 15.2에서 conditional compilation에 대한 또다른 일반적인 사용에 대해 논의할 것이다.
#error
, #line
, #pragma
directive에 대해서 간결하게 알아볼 것이다. 이 directive는 우리가 이미 알아본 것들보다 훨씬 더 특별하고 빈번하게 사용되지 않는다.
#error
Directive#error
directive는 아래의 형태를 가진다.
#error message
message
의 부분은 어떠한 token의 연속이 될 수 있다. 만약 전처리기가 #error
directive를 마주쳤다면, 반드시 message
를 포함한 에러 메시지를 출력할 것이다. 에러 메시지의 실제 형태는 컴파일러마다 다양할 수 있다. 어떤 것은 아래와 같을 수도 있다.
Error directive: message
또 어떤 것은 그냥 그대로 나올 수도 있다.
#error message
#error
directive를 마주하는 것은 프로그램 내부의 심각한 결함을 나타낸다. 어떤 컴파일러들은 다른 에러를 찾으려는 시도 없이 컴파일을 즉각적으로 종료할 수도 있다.
#error
directive는 conditional compilation과 함께 결합되어서, 일반적인 컴파일중에 발생해서는 안되는 상황을 체크하는 것에 사용된다. 예를 들어 int
가 100,000까지 저장하지 못하는 machine에서는 프로그램이 컴파일되지 않도록 하고 싶다고 생각해보자. INT_MAX
macro를 통해 int
값의 가능한 최대값이 표현되고, 그래서 우리가 해야할 것은 INT_MAX
가 최소 100,000에 도달하지 못했다면 #error
directive를 발생시키는 것(invoke)이다.
#if INT_MAX < 100000
#error int type is too small
#endif
16bits로 정수를 저장하는 machine에서 프로그램을 컴파일하는 시도를 하는 것은 아래와 같은 메시지를 생성할 것이다.
Error directive: int type is too small
#error
directive는 #if
-#elif
-#else
의 #else
부분에서 종종 발견된다.
#if defined(WIN32)
...
#elif defined(MAC_OS)
...
#elif defined(LINUX)
...
#else
#error No operating system specified
#endif
#line
Direcitve#line
directive는 숫자가 매겨진 프로그램 행들을 수정하는 방법으로 사용된다(행은 보통 1, 2, 3과 같이 숫자가 매겨진다). 이 directive를 사용하는 것으로 컴파일러가 다른 이름의 파일에서 프로그램을 읽고 있다고 생각하도록 만들 수 있다.
#line
directive는 두 가지의 형태를 가진다. 하나의 형태는 행의 숫자를 명시하는 것이다.
#line n
n
은 반드시 1과 32767 사이의 정수(C99에서는 2147483647)를 표현하는 일련의 숫자여야 한다. 이 directive는 프로그램 내부의 후속 행들에 n
, n+1
, n+2
,...으로 숫자가 매겨지도록 한다.
#line
directive의 두번째 형태는 행의 숫자와 파일의 이름을 명시하는 것이다.
#line n "file"
이 directive를 따르는 행은 n
에서 시작하는 행과 함께 file
로부터 오고있다고 여길 것이다. n
의 값과 file
문자열은 macro를 사용하여 명시될 수 있다.
#line
directive의 효과 중 하나는 __LINE__
macro의 값을 바꾼다는 것이다(__FILE__
macro에도 가능하다). 더 중요한 것은 대부분의 컴파일러가 에러메시지를 생성할 때 #line
directive의 정보를 사용한다는 것이다.
예를 들어 아래의 directive가 파일 foo.c
의 시작부분에 나타난다고 가정해보자.
#line 10 "bar.c"
컴파일러가 foo.c
의 5번 행에서 에러를 발견했다고 가정해보자. 에러메시지는 foo.c
의 5번 행이 아니고, bar.c
의 13번 행을 참조할 것이다. (왜 13번 행일까? directive는 foo.c
의 1번 행을 차지하고 있고 그렇기 때문에 foo.c
가 2번 행에서 시작하도록 숫자를 다시 매기는데, 이는 bar.c
의 10번 행으로 처리된다.)
한눈에 보면 #line
directive는 신기하게 보인다. 왜 에러메시지가 다른 행이나 다른 파일을 참조하도록 원하는걸까? 이러면 프로그래머가 디버그하는 것이 더 어렵지 않을까?
사실, #line
directive는 프로그래머에 의해 거의 사용되지 않는다. 대신에, output으로 C 코드를 생성하는 프로그램에서 주로 사용된다. 가장 유명한 예시는 yacc
(Yet Another Compiler-Compiler)과 같은 프로그램인데, 컴파일러의 일부를 자동적으로 생성하는 UNIX의 유틸리티이다.(yacc
의 GNU verison의 이름은 bison
이다) yacc
을 사용하기 이전에, 프로그래머들은 C 코드들 뿐만 아니라 yacc
에 대한 정보를 포함한 파일을 준비해야 한다. 이 파일에서 yacc
은 y.tab.c
와 같은 C프로그램을 생성하는데, 이는 프로그래머에 의해 전달받은 코드를 병합한다. 프로그래머는 그 후 일반적인 방식으로 y.tab.c
를 컴파일할 것이다. #line
directive를 y.tab.c
에 삽입하는 것으로, yacc
은 컴파일러가 프로그래머가 작성한 코드인 원래 파일로부터 코드가 오고있다고 생각하게 만든다. 결과적으로, y.tab.c
의 컴파일 도중에 생성된 어떠한 에러메시지라도 y.tab.c
가 아닌 원래의 파일 내부의 행을 참조하게 된다. 이것은 디버깅을 더 쉽게 만드는데, 에러메시지가 프로그래머에 의해 작성된 파일을 참조하고 yacc
에 의해 생성된 파일을 참조하지 않기 때문이다.
#pragma
Directive#pragma
directive는 컴파일러로부터 특별한 행동을 요청하는 방법을 제공한다. 이 directive는 비정상적으로 크거나, 특별한 컴파일러의 이점을 이용할 필요가 있는 프로그램에 아주 유용하다.
#pragma
directive는 아래의 형태를 가진다.
#pragma tokens
token
의 자리에는 자의적인(arbitrary) token이 온다. #pragma
directive는 아주 간단하거나(single token) 더 정교해질 수 있다.
#pragma data(heap_size => 1000, stack_size => 2000)
#pragma
directives 안에서 나타날 수 있는 커맨드의 집합은 각각의 컴파일러마다 다르다. 어떠한 커맨드를 허용하고, 이 커맨드가 무엇을 하는지에 대해 컴파일러의 문서를 확인해봐야 할 것이다. 전처리기는 인지되지 않는 커맨드를 포함하는 #pragma
directive를 무시할 것이다. 에러 메시지를 제공하는 것은 허용되지 않는다.
C89에서는 표준 pragma가 없다. 전부다 implementation-defined이다.
C99는 3개의 표준 pragma를 가지고 있는데, 모두다 STDC
를 #pragma
의 첫번째 token으로 사용한다. 이 pragma는 FP_CONTRACT
(Section 23.4), CS_LIMITED_RANGE
(Section 27.4), FENV_ACCESS
(Section 27.6)이다.
_Pragma
OperatorC99는 _Pragma
연산자를 도입했는데, 이는 #pragma
directive와 결합하여 사용된다. _Pragma
표현식은 아래와 같은 형태를 가진다.
_Pragma ( string-literal )
표현식같은 것들을 마주하면, 전처리기가 문자열을 둘러싼 큰따옴표를 제거하고 esacpe sequence인 \"
나 \\
를 문자 "
와 \
로 대체하는 것으로 문자열 리터럴을 "비문자열화(destringizes)"한다(이 용어는 C99 표준에서 사용되었다). 결과는 일련의 token인데, 이 token은 #pragma
directive에 등장한 것처럼 다루어진다. 아래의 예시를 보자.
_Pragma("data(heap_size => 1000, stack_size => 2000)")
위의 행은 아래의 행과 동일하다.
#pragma data(heap_size => 1000, stack_size => 2000)
_Pragma
연산자는 전처리기의 한계를 해결하도록 한다. 전처리 directive는 또다른 directive를 생성할 수 없다. 그러나 _Pragma
는 directive가 아니라 연산자(operator)이기 때문에 macro 정의 내부에서 나타날 수 있다. 이것은 #pragma
를 남기는 macro 확장(expansion)이 가능하게 한다.
GCC manual에서 예시를 보자. 아래의 macro는 _Pragma
연산자를 사용했다.
#define DO_PRAGMA(x) _Pragma(#x)
위의 macro는 아래와 같이 등장(invoke)시킬 수 있다.
DO_PRAGMA(GCC dependency "parse.y")
확장(expansion) 이후에 결과는 아래와 같다.
#pragma GCC dependency "parse.y"
위의 예시는 GCC에 의해 지원되는 pragma중 하나이다. (명시된 파일(이 예시에는 parse.y
)이 현재의 파일보다 더 최근의 것일 때 경고를 발생시킨다.) DO_PRAGMA
의 호출에 대한 argument는 일련의 token임을 주목해야 한다. DO_PRAGMA
의 정의 내부의 #
연산자는 token을 "GCC dependency \"parse.y\""
로 문자열화 시킨다. 이 문자열은 _Pragma
연산자에 전달되어 비문자열화(destringize)되고, 원래의 token을 포함하는 #pragma
directive 생성한다.
#
하나만 행에 포함하고 있는 경우가 있었다. 이는 규칙에 맞는가?
맞다. 이를 null directive라고 부른다. 이는 어떠한 효과도 없다. 어떤 프로그래머들은 conditional compilation block에 공백을 넣기 위해서 null directive를 사용하기도 한다.
#if INT_MAX < 100000
#
#error int type is too small
#
#endif
당연히, 빈 칸이 있더라도 작동하고, #
은 블록의 범위를 알아보기 쉽도록 만든다.
프로그램에서 어떤 상수가 macro로 정의되는 것이 필요한지 확신하지 못하겠다. 여기에 대한 가이드라인이 있는가?
경험에 따르면, 0과 1을 제외한 모든 상수는 macro가 되어야 한다. 문자와 문자열 상수는 문제가 있는데, 문자와 문자열을 상수를 macro로 대체하는 것이 항상 가독성을 올려주지 않기 때문이다.
(1)상수가 두 번 이상 사용되거나,
(2)상수가 어느날 수정되어야할 가능성이 존재한다면
문자 상수와 문자열 상수 대신에 macro를 사용하는 것을 추천한다. (2)의 이유 때문에 아래와 같은 매크로는 잘 사용하지 않는다. 어떤 프로그래머들은 사용한다고 해도 말이다.
#define NUL '\0'
"문자열화(stringize)"에서 "
또는 \
문자가 포함되어 있는 argument가 있다면 #
연산자는 어떻게 작동하는가?
#
연산자는 "
를 \"
로, \
를 \\
로 변환한다. 아래의 macro를 보자.
#define STRINGIZE(x) #x
전처리기는 STRINGIZE("foo")
를 "\"foo\""
로 대체할 것이다.
아래의 macro가 적절하게 작동하는지 이해하지 못하겠다. CONCAT(a,b)
는 예상한 것처럼 ab
의 결과를 내지만, CONCAT(a,CONCAT(b,c))
는 이상한 결과가 나타난다. 무슨 일이 일어나는 것인가?
#define CONCAT(x,y) x##y
Kernighan과 Ritchie가 "bizarre"라고 부르는 규칙 덕분에, ##
에 의존하는 replacement-list
는 일반적으로 중첩되게 호출될 수 없다. 문제는 CONCAT(a,CONCAT(b,c))
가 CONCAT(b,c)
는 bc
가 되고, CONCAT(a,bc)
가 abc
가 되는 것처럼 "일반적인" 의도로는 확장되지 않는다는 것이다. replacement-list
에 ##
보다 앞서는 macro parameter는 동시에 치환되어 확장될 수 없다. 결과적으로, CONCAT(a,CONCAT(b,c))
는 aCONCAT(b,c)
로 확장되고, 그 이상으로 확장되지 않는다. 왜냐하면 aCONCAT
이라는 이름의 macro는 없기 때문이다.
이 문제를 해결할 수 있는 방법이 있지만 좋지는 않다. 이 트릭은 첫번째 macro를 호출하는 두번째 macro를 정의하는 것이다.
#define CONCAT2(x,y) CONCAT(x,y)
CONCAT2(a,CONCAT2(b,c))
를 작성하는 것으로 이제 의도한 결과를 생성할 것이다. 전처리기가 바깥 CONCAT2
의 호출을 확장하면서, CONCAT2(b,c)
또한 확장된다. 차이점은 CONCAT2
의 replacement-list
가 ##
을 포함하지 않는다는 것이다. 이것을 이해하지 못했더라도 걱정할 필요가 없다. 자주 발생하는 문제가 아니기 때문이다.
#
연산자도 비슷하게 어렵다. 만약 #x
가 replacement-list
에 나타났다고 한다면, macro parameter인 x
에 대응하는 argument는 확장되지 않는다. 그래서 N
이 10을 나타내는 macro라고 했을 때, STR(N)
은 "10"
이 아닌 "N"
을 나타낸다. 이에 대한 해결책은 우리가 CONCAT
에 사용한 것과 비슷하다. STR
을 호출하는 일을 하는 두번째 macro를 정의하는 것이다.
전처리기가 재확인(rescanning) 과정에서 원래의 macro 이름을 마주했다고 생각해보자. 아래의 예시를 보면, N
은 (2*M)
으로 대체될 것이고, 그 후 M
은 (N+1)
로 대체될 것이다. 전처리기가 N
을 다시 대체하는 것으로 무한 루프에 빠지는 것은 아닌가?
#define N (2*M)
#define M (N+1)
i = N; /* infinite loop? */
오래된 어떤 전처리기들은 무한 루프에 빠질 수 있지만, 최근의 것들은 그러면 안된다. C의 표준에 따르면, 만약 원래의 macro 이름이 macro의 확장 동안에 다시 나타난다면 그 이름은 다시 대체되지 않는다. 아래의 i
의 대입이 전처리이후에 어떻게 되는지 보자.
i = (2*(N+1));
어떤 진취적인 프로그래머들은 표준 라이브러리에 있는 예약된 단어나 함수와 일치하는 이름의 macro를 작성하느 것으로 이 동작을 이용한다. sqrt
라이브러리 함수를 생각해보자. sqrt
는 argument의 제곱근을 계산하고, argument가 음수라면 implementation-defined 값을 반환한다. 아마도 우리는 argument가 음수라면 sqrt
가 0을 반환하는 것을 선호할 수도 있다. sqrt
는 표준 라이브러리의 일부이기 때문에 이것을 쉽게 바꿀 수는 없다. 하지만 argumnet가 음수일 때 0으로 계산하도록 하는 sqrt
macro를 정의할 수 있다.
#undef sqrt
#define sqrt(x) ((x)>=0?sqrt(x):0)
나중에 호출되는 sqrt
는 전처리기에 의해 가로채지고, conditional expression으로 확장될 것이다. sqrt
의 호출 내부의 conditional expression은 재확인(rescanning)과정에서 대체되지 않는데, 그래서 컴파일러가 제어할 수 있도록 남아있다.(#undef
의 사용은 이전에 정의된 sqrt
macro의 정의를 제거한다(undefine). Section 21.1에서 볼것이지만, 표준 라이브러리는 macro와 함수가 같은 이름을 가지는 것을 허용한다. 라이브러리에 이미 sqrt
가 macro로써 정의되어 있는 경우에는 우리의 sqrt
macro 이전에 정의된 sqrt
를 제거하는 것은 방어적인 수단이다.)
__LINE__
이나 __FILE__
과 같은 이미 정의된 macro를 사용할 때 오류가 발생한다. 내가 include 해야하는 특별한 헤더가 있는 것인가?
그렇지 않다. 이 macro들은 전처리기에 의해 자동적으로 인지된다. 각각의 macro 이름의 시작과 끝이 한 개가 아닌 두 개의 _
인지 확인해야 할 것이다.
"hosted implementation"과 "freestanding implementation"의 구분을 두는 의도가 무엇인가? 만약 freestanding implementation이 <stdio.h>
헤더를 지원하지 않는다면 무엇을 사용해야 하는가?
hosted implementation은 operating system에 대한 input/output과 필수적인 서비스에 따르는 대부분의 프로그램에 필요하다. C의 freestanding implementation은 operating system을 필요로 하지 않는 프로그램에 사용된다. 예를 들어 freestanding implementation은 operating system의 kernel을 작성하는 것에 필요하다(전통적인 input/output이 필요하지 않기 때문에 <stdio.h>
가 필요하지 않다). freestanding implementation은 임베디드 시스템의 소프트웨어를 작성하는 것에도 유용하다.
내 생각에는 전처리기는 단지 에디터라고 생각이 든다. 어떻게 상수 표현식을 계산하는 것인가?
전처리기는 당신이 예상하는 것보다 훨씬 더 정교하다. 전처리기는 상수 표현식들을 평가할 수 있을만큼 충분히 C에 대해 많이 알고 있지만, 컴파일러가 완전히 동일한 방식으로 수행하지는 않는다. (한가지더 말해보면, 전처리기는 정의되지 않은 이름은 0의 값을 가진다고 처리한다. 또다른 차이점을 여기서 말하기에는 너무 난해하다.) 실제로, 전처리기 상수 표현식 안의 피연산자들은 일반적으로 상수, 상수를 표현하는 매크로, defined
연산자가 적용된 것이다.
왜 C언어는 #ifdef
와 #ifndef
directive를 제공하는 것인가? #if
directive와 defined
연산자를 사용하는 것으로 동일한 효과를 얻을 수 있지 않은가?
#ifdef
와 #ifndef
directive는 1970년도에 C의 부분이 되었다. 하지만 defined
연산자는 표준화 과정에서 1980년에 C언어에 추가되었다. 그래서 진정한 질문은 이것이다. 왜 defined
는 언어에 추가되었는가? 정답은 defined
가 유연성(flexibility)를 추가한다는 것이다. 단일 macro인 #ifdef
나 #ifndef
의 존재를 검사할 수 있는 것 대신, 우리는 이제 어떤 개수의 macro든 상관 없이 #if
와 defined
를 함께 사용하는 것으로 검사할 수 있다. 예를 들어 아래의 directive는 FOO
와 BAR
가 정의되어 있지만 BAZ
가 정의되어 있지 않은지 확인하는 예시이다.
#if defined(FOO) && defined(BAR) && !defined(BAZ)
아직 다 작성하지 못한 프로그램을 컴파일하고 싶다. 그래서 다 완성하지 못한 부분을 "conditioned out(false로 처리)"처리 하였다. 내가 프로그램을 컴파일했을 때, #if
와 #endif
사이의 행 중 하나를 참조하는 에러메시지가 발생했다. 전처리기는 이 행들을 무시하는 것이 아닌가?
#if 0
...
#endif
아니다. 행들이 완벽하게 무시되지 않는다. 주석들은 전처리 directive들이 실행되기 이전에 처리되고, 소스 코드는 전처리 token으로 나누어진다. 그래서, #if
와 #endif
사이의 종료되지 않은 주석은 에러 메시지를 발생시킨다. 또한 짝지어지지 않은 작은따옴표나 큰따옴표 문자는 undefined behavior를 야기한다.