Chapter 14. The Preprocessor

지환·2022년 2월 1일
0

14.1 How the Preprocessor Works

preprocessor는 preprocessing directive (# character로 시작하는 명령어) 로 작동한다.

C program --Preprocessor--> Modified C program --Compiler--> Object code

Modified C program에서 directive는 모두 삭제된다. (line을 지워버리는게 아니라 Blank line 처리)

#include directive는 해당 파일을 열고 정보를 가져와 붙여넣고
(요즘엔 굳이 복사해서 붙여넣지 않고 헤더파일의 함수를 사용하는 정도의 기능만 하기도 한다),
#define directive는 macro들을 정의된 값으로 치환한다.

이렇게 directive만 처리하는게 아니라, comment가 있다면 그건 single space character로 치환한다.
(몇몇 preprocessor는 필요없는 space나 들여쓰기 한 부분을 없애버리기도 한다.)

주의 : preprocessor는 C에 대한 제한된 지식을 가지고 기능하므로 본인이 잘 봐야한다. 원본 파일에서 찾기 어려운 error가 있다면 preprocessing만 진행해보고 문제점 파악해보는 것도 좋다.


14.2 Preprocessing Directives

Preprocessing Directives

  1. Macro definition : #define, #undef
  2. File inclusion : #include
  3. Conditional compilation : #if, #ifdef, #ifndef, #elif, #else, #endif
  4. #error, #line, #pragma

Rules apply to direcitves

  1. Directives always begin with the # symbol
    # 앞에 white space 있는거 OK, 꼭 line 제일 앞에서부터 시작할 필요는 없음
  2. Any number of spaeces and horizontal tab may separate the tokens in a directive
  3. Directives always end at the first new-line character, unless explicitly continued
    다음 line으로 이어서 쓰고 싶으면, 뒤에 \ character 사용하면 됨. (string이랑 같음)
  4. Directives can appear anywhere in a program
    보통 제일 위에 오지만 중간에 와도 OK
  5. Comments may appear on the same line as a directive
    그 의미를 적어두면 오히려 좋음

14.3 Macro Definitions

Simple Macros (object-like macro)

#define identifier replacement-list

replacement-list : any sequence of preprocessing tokens
(replacement list may include identifiers, keywords, numeric constants, character constants, string literals, operators, and punctuation)
replacement list 비어있어도 OK

Simple macro는 주로 constants의 이름을 짓는데 사용된다.
그 외 다른 용도
1. Making minor changes to the syntax of C (not good idea)
#define BEGIN {
#define END }
#define LOOP for(;;)
2. Renaming types (type definition 있음)
#define BOOL int
3. Controlling conditional compilation
#define DEBUG
conditional compilatoin을 control 하는데 중요한 역할을 할 수 있다.

Parameterized Macros (function-like macro)

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

x1, x2 : macro's parameters -> replacement-list에 많이 나와도 OK
identifier과 왼쪽 괄호 사이에는 space가 없어야 한다. space가 있으면 그냥 simple macro임
parameter list는 비어있어도 OK (비울거면 굳이 쓸 필요도 없지만, 함수와 닮아서 그렇게 씀)
#define getchar() getc(stdin)
대개는 간단한 함수처럼 사용함.

  • advantages
  1. The program may be slightly faster
    그냥 함수는 argument 복사하는 등의 overhead로 인해 시간이 좀 걸림 (C99에선 inline 함수를 사용하면 overhead를 피할 수 있음)
  2. Macros are "generic"
    일반 함수와 다르게 다양한 type이 parameter로 들어와도 처리할 수 있다.
  3. 반복되는 패턴은 묶어버릴 수 있다.(p.323)
    #define PRINT_INT(n) printf("%d\n", n)
  • disadvantages
  1. The compiled code will often be larger
    특히 겹치며 있을 때 엄청 복잡해지고 길어진다. MAX 구하는 macro를 생각해보자.
  2. Arguments aren't type-checked
    위에선 장점이었지만, argument의 type을 체크하지 않고 변환 하지 않는게 단점이 될 수 있다.
  3. It's not possible to have a pointer to a macro
    전처리 후 삭제되므로 pointer to a macro라는 개념 자체가 없다.
  4. A macro may evaluation its arguments more than once
    argument가 side-effect를 가지고 있다면 예상치 못한 결과가 나올 수 있다. 이건 그냥 되도록 피하는게 좋다.

The # Opeator

preprocessing 중에 처리하기 때문에 compiler는 알아보지 못하는 operator다.

macro arguments를 string literal로 변환한다. (stringization)
parameterized macro의 replacement list에서만 쓸 수 있다.

ex)
#define PRINT_INT(n) printf(#n " = %d\n", n)
여기서 PRINT_INT(90); 을 하면
printf("90" " = %d\n", 90); 과 같다.
C에선 붙어있는 두 문자열은 하나로 합치니까 printf("90 = %d\n", 90); 이 된다.

The ## Operator

preprocessing 중에 처리하기 때문에 compiler는 알아보지 못하는 operator다.

두개의 token을 붙여서 하나의 token으로 만든다. (token pasting)
macro parameter가 끼여있으면 argument로 replace된 후에 paste한다.

ex)

#define MK_ID(n) i##n
int MK_ID(1), MK_ID(2), MK_ID(3); --> int i1, i2, i3;
#define GENERIC_MAX(type)		\
type type##_max(type x, type y)	\
{								\
  return x > y ? x : y;			\
}

General Properties of Macros

simple macro, parameterized macro 둘 다에 적용되는 rules

  1. A macro's replacement list may contain invocations of other macros
    : 처음 preprocessor가 replace하고 나면, 그 안에 또 다른 macro가 있는지 rescan한다. macro name이 하나도 안보일때 까지 rescan한다.
  2. The preporcessor replaces only entire tokens, not portion of tokens
    : identifier, character constants, string literals에 포함된 token은 무시한다, 전체 token일 경우만 replace 한다. 예를들어 #define a 1 이라고 define해도 int abc;int 1bc;가 되지는 않는다.
  3. A macro defintion normally remains in effect until the end of the file in which it appears
    : 기존 scope rule을 따르지 않는다. 함수 안에서 정의된 macro 이더라도 그 함수에서만 적용되는건 아니다.
  4. A macro may not be defined twice unless the new definition is identical to the old one
    : spacing이 다른 것 까지는 허용된다.

undef directive

#undef identifier

identifier에 대한 현재 definition을 삭제한다.

Parentheses in Macro Definitions

macro definition에서 괄호를 쳐야하는 위치 두곳이 있다.

  1. macro's replacement list가 operator를 포함한다면, 그 전체를 괄호를 쳐야한다.
  2. parameter가 있다면, replacement list에 등장하는 모든 parameter에 괄호를 쳐야한다.

위 규칙을 안지킨다고해서 오류가 뜨는건 아니지만, 예상한 결과와 다른 결과가 나올 수 있으므로 지켜야한다.

1번 규칙을 지키지 않으면,

#define TWO_PI 2*3.14159	//replacement list에 괄호가 필요하지만 치지 않겠다.
conversion_factor = 360/TWO_PI;
라고 했을 경우,
conversion_factor = 360/2*3.14159
가 돼버려서 나누기가 먼저 계산되기 때문에 원하는 값을 얻지 못한다.
원하는 값 : 360/(2*3.14159)

2번 규칙을 지키지 않으면,

#define SCALE(x) (x*10)		//parameter인 x에 괄호가 필요하지만 치지 않겠다.
j = SCALE(i+1);
라고 했을 경우,
j = (i+1*10);
가 돼버려서 곱하기가 먼저 계산되기 때문에 원하는 값을 얻지 못한다.
원하는 값 : (i+1)*10

Creating Longer Macros

  1. #define ECHO(s) (gets(s), puts(s))
    두 함수를 호출하는건 expression이니 comma operator 쓸 수 있다. 근데 이 경우 expression이 아니라 statement가 여러개 오려면 해결할 수가 없다.(예를들어 return statement) 그래서 2번 대안이 있다.
  2. #define ECHO(s) { gets(s); puts(s); }
    이 경우 쓰려면 ECHO(s) 뒤에 semicolon을 붙이지 말아야 된다는걸 기억해야된다. 근데 안붙이면 다른 statement랑 일관성도 안맞고 이상해 보인다. 그래서 3번 대안이 있다.
  3. 아래와 같이 do-while문을 이용한다. 원래는 while뒤에 semicolon와야되지만, 사용할때 붙일거기 때문에 괜찮다.
#define ECHO(s)			\
	do {				\
    		gets(s);	\
        	puts(s);	\
        } while(0)

Predefined Macros

NameDescription
__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은 compile된 시간을 알려주므로 실행할때마다 상단에 보이게 하면 해당 프로그램의 버전을 알 수 있다.
LINE이나 FILE은 error를 찾는데 쓰일 수 있다.
STDC는 표준 적용되는지 확인용으로 사용할 수 있다.

Additional Predefined Macros in C99

NameDescription
__STDC__HOSTED__1 if this is a hosted implementation; 0 if is freestanding
__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

C에서 implementation이라 함은, C 프로그램을 실행시키는데 필요한 여러 software다(Compiler 포함).
C99에선 implementation을 hosted implementation(C99 standard를 지키는 프로그램을 accept하는 implementation)과 freestanding implementation(몇몇 header나 complex type은 지원하지 않아도 됨) 두가지 종류로 나눴다.

__STDC__VERSION__ : compiler가 인정하는 C 표준 버전을 확인한다. C99면 값은 199901L 을 가진다. 각각 standard와 amendment에 따라 다른 값을 가진다.

아래 세개 macro는 해당 조건을 충족하지 않으면 정의도 안돼있다.

Empty Macro Arguments

comma는 남겨두고, argument 비우는거 OK
그럼 그냥 해당 argument 자리는 지워짐.

만약 해당 argument가 # operator의 피연산자라면
""(empty string) -> 결과로 나옴

만약 해당 argument가 ## operator의 피연산자라면
placemarker token이 그 자리에 들어가고, macro expansion이 끝난 후 placemarker token은 사라짐.

Macros with a Variable Number of Arguments

(variable number of function은 chapter 26.1)

이 특징을 사용하는 주된 이유는 입력받은 arguments를 printf나 scanf 같이 argument 개수가 다양한 다른 함수에 넘겨줄 수 있기 때문이다.

#define TEST(condition, ...) ((condition)? \
  printf("Passed test: %s\n", #condition): \
  printf(__VA_ARGS__))

... : ellipsis 라고 한다. parameter list 마지막에 온다.
__VA_ARGS__ : ellipsis에 해당되는 모든 arguments를 나타낸다. macro의 replacement list 에만 올 수 있다.

The __func__ Identifier

macro가 아니라 preprocessor와 아무 관계 없지만, debugging하기 좋아서 여기서 소개한다.

__func__ indentifier는 현재 진행되는 함수 이름을 알려주는 identifier로,
각각의 함수가
static const char __func__[] = "function-name";
을 함수 body 제일 윗부분에 적어둔 것과 같은 효과를 가진다.

이걸 이용해서,
어떤 함수가 시작 돼고 끝났는지를 알 수 있고,
다른 함수를 호출할 때 어떤 함수가 그 함수를 호출했는지 argument로 넘겨줘서 알게 할 수도 있다.


14.4 Conditional Compilation

program text의 일부분을 특정 조건에 따라 포함하거나 제외시킬 수 있다.

The #if and #endif Directives

#if constant-expression : 정의되어있고 + 0이 아니라면 true
#endif : #if block의 끝을 표시한다.

ex)

//아래 DEBUG 값을 바꾸거나 정의하지 않음으로써 출력할지 말지 정함
#define DEBUG 1

#if DEBUG
printf("Value of i: %d\n", i);
#endif

The defined Operator

해당 macro가 정의되어 있다면 1, 아니면 0 반환

#if defined(DEBUG)
#if defined DEBUG
꼭 괄호가 필요한 건 아님, #if 와 주로 같이 쓰인다.

정의가 됐냐 안됐냐의 문제이므로 값을 줄 필요는 없다.

The #ifdef and #ifndef Directives

얘도 위 operator처럼 해당 identifier가 macro로 정의 돼있는지 확인한다.

#ifdef indentifier : 해당 identifier가 macro로 정의돼있다면 1
#ifndef indentifier : 해당 identifier가 macro로 정의돼있지 않다면 1

defined operator가 있는데 왜 이게 필요하지?
: 원래 #ifdefdefined보다 먼저였다. 여러 조건의 묶음을 만들거나 할 때 #ifdef를 사용하기 복잡하므로 flexibility를 위해 defined가 나중에 추가되었다.

The #elif and #else Directives

#elif constant-expression
#else

기본 구조는,
#if or #ifdef or #ifndef

#endif
가 처음과 끝을 이뤄야 되고,
그 사이에 여러개의 elif가 올 수 있다.
else는 그 사이에 딱 하나만 올 수 있다.
그리고 #endif 뒤에 주석으로 #if의 condition을 적어주던가 해서 시작을 찾기쉽게 해주면 좋다.

Uses of Conditional Compilation

  1. Debugging하는데 좋다.
  2. 여러 machines이나 OS에서 작동하도록 할 수 있다. (portability)
#if defined(WIN32)
...
#elif defined(MAC_OS)
...
#elif defined(LINUX)
...
#endif
  1. 서로 다른 compiler에서도 compile될 수 있도록 할 수 있다.
#if __STDC__
function prototypes
#else
Old-style function declarations
#endif
  1. macro의 default definition을 작성할 수 있다.
#ifndef BUFFER_SIZE
#define BUFFER_SIZE 256
#endif
  1. 주석을 포함하는 code를 일시적으로 없앨 수 있다.
#if 0
Lines containing comments
#endif

이렇게 코드를 일시적으로 막는걸 conditioning out 이라고 부른다.

  1. header file이 multiple inclusion 되는 걸 막는다.(chapter 15.2)

14.5 Miscellaneous Directives

The #error Directive

#error message       //message는 아무 tokens이면 된다

주로 conditional compilation과 같이 쓰여서, 보통 compile하는 동안 있어서 안될 일들을 잡아내 에러 메세지를 띄우고 중지시킨다.

예를들어 int가 100,000 이상의 숫자를 저장하지 못하면 안될 경우에
아래와 같이 작성할 수 있다.

#if INT_MAX < 100000
#error int type is too small
#endif

The #line Directive

#line n

이 이후에 오는 line의 숫자를 n, n+1, ... 이런 식으로 만듦.
__line__ macro의 값을 바꾼다.
#line n "file"

위 효과에 더해 파일 이름도 저 안에 있는 걸로 인식하게 한다.
__file__ macro의 값도 바꾼다.

n 이나 file은 macro로 정의할 수도 있다.

이게 왜 필요하지?
: yacc 예시(p.240)
간단하게 설명하자면, 중간에 다른 프로그램을 거쳐서 compile할 경우 마지막에 에러가 떴을 때 내가 원래 작성한 code를 기준으로 line 번호를 알려줘서 debugging하기 편하게 할 수 있다. #line directive는 프로그래머보다 C file을 output으로 가지는 프로그램이 주로 사용한다.

The #pragme Directive

compiler에게 특정 행동을 취하도록 요청한다.

#pragma tokens

각각 compiler에 맞는 command를 사용해야 한다.(그렇지 않으면 ignore)

C99에는 3개의 standard pragma가 있다.
1. FP_CONTRACT
2. CX_LIMITED_RANGE
3. FENV_ACCESS
사용하려면 #pragma 뒤에 STDC 뒤에 얘네를 작성해야 함.(그리고 얘네 뒤엔 또 ON or OFF or DEFAULT(자세한건 웹서핑))

The _Pragma Operator

_Pragma ( string-literal )

string-literal의 것들을 destringizes(C99에서 사용하는 용어) 해버린다. \""로 바꾸고, \\\로 바꾸고 등등
즉, _Pragma("abc")#pragma abc 와 같다.

얘도 operator가 따로 있는 이유는 macro 정의 안에서도 쓰이거나 할 수 있기 때문


Q&A

# 만 덩그러니 있으면 null directive로, conditional compilation block을 확인하기 좋다

macro로 만들어야하는 constant의 가이드라인이 있나?
경험에 의하면 0과 1을 제외한 모든 numeric constant는 macro가 돼야 한다. character나 string literal의 경우는 (1)한번 이상 쓰이고 (2)수정될 가능성이 있는 경우 macro로 하면 좋다.(저자는 이렇게 하는거고 참고정도만 하자)

" \가 포함돼 있으면 # oprator는 어떻게 작동?
반대로 작동한다. "\"으로 만들고, \\\으로 만든다.

#define CONCAT(x, y) x##y라고 했을 때 CONCAT(a,CONCAT(b,c)) 라고 하면 원하는 결과가 안나온다, 왜?
## 주변에 있는 parameter는 한번에 expand되지 않는다. 따라서 결과로 aCONCAT(b,c)가 나오게 되고 오류가 뜰 것이다.(aCONCAT 이라는 함수나 macro가 없으면)

#도 마찬가지로 한번에 expand되지 않는데, #define STR(x) #x라고 했을 때, #define N 10에서 N을 넣으면 "10"이 되는게 아니라 "N"이 된다.

이를 해결하는 방법은 #과 ##을 포함하지 않는 두번째 함수를 하나 더 만드는 것이다. ex) #define CONCAT2(x,y) CONCAT(x,y)

macro 특징에 보면, 전처리할때 macro가 없어질 때 까지 rescan한다고 하는데, 서로서로 엉켜있으면 무한 루프가 만들어지나?
아니다. original macro가 expansion 도중에 다시 나오면 이는 다시 치환되지 않는다.

이를 이용할 수도 있다.

#undef sqrt
#define sqrt(x) ((x)>=0 ? sqrt(x) : 0)

이렇게 하면 sqrt macro가 한번 0 미만인 경우 0으로 만들어주고,
그 다음에 또 sqrt가 나오면 이는 치환 안되고 sqrt 라이브러리 함수로 처리된다.
이렇게 라이브러리 함수와 같은 이름의 macro를 정의하는 것은 가능하다.(chapter 21.1)

hosted implementation / freestanding implementation ??
hosted의 경우 input, output이나 다른 필요한 기능을 제공하는 OS가 있어야 한다. freestanding은 OS가 없는 경우, OS의 kernel을 작성하는 경우, embedded system의 software를 작성하는 경우 등에 사용된다.

preprocessor는 editor 수준일 줄 알았는데, 연산 기능도 되네?
생각보다 preprocessor는 정교하다. compiler처럼 계산하진 않지만, 상수를 계산하는 정도는 충분히 해낸다.

conditioning out 했는데도 그 라인 안에서 오류가 뜬다.
그 라인이 완전히 무시되지는 않는다. comments의 경우 preprocessing directives보다 더 빨리 처리된다. 그래서 그 사이에 (1)unterminated comment가 있으면 error가 뜰 수 있고, (2)unpaired sigle quote or double quote 가 있으면 UB이다.

0개의 댓글