본문 바로가기
독서/C Programming : A Modern Approach

[KNK] chapter 14 : 전처리기

by 도리언 옐로우 2024. 12. 31.

1. 전처리기 기본

전처리기(preprocessor)는 말 그대로 전처리 작업을 수행하는 프로그램이다. chapter 2 에서 C 프로그램은 전처리, 컴파일, 어셈블, 링킹 과정을 거쳐야 컴퓨터가 실행할 수 있는 형태가 된다는 것을 배웠다. 이번 파트에서는 전처리 과정에 대해 좀더 구체적으로 살펴본다.

전처리기에는 C 프로그램이 입력으로 들어가는데, 이 C 프로그램에 지시자들이 포함되어 있을 수 있다. 전처리기는 이 지시자들을 제거하면서 해당 지시자의 역할을 실행해주게 되고, 이러한 작업을 마치면 입력된 C 프로그램의 수정본인 새로운 C 프로그램을 출력해준다. 아래 예시를 살펴보자.

/* 원의 면적을 구하는 프로그램 */
#include <stdio.h>
#define PI 3.14

int main() {
    printf("원의 면적: %f\n", PI * 5 * 5);
    return 0;
}

 

위 코드가 전처리를 거치면 다음과 같이 변환된다.

빈 줄
stdio.h에서 가져온 줄들
빈 줄
int main() {
    printf("원의 면적: %f\n", 3.14 * 5 * 5);
    return 0;
}

변환된 코드를 잘 살펴보면 전처리기는 주석을 제거해주고, #include 지시자를 보고 stdio.h의 내용물을 가져오며, #define 지시자가 있던 줄을 비우고 매크로 정의된 값을 대체해줌을 알 수 있다. 참고로, 어떤 전처리기들은 들여 쓴 줄의 탭, 띄어쓰기 등 불필요한 공란문자를 지워준다고 한다.

2. 전처리 지시자

앞서 살펴본 전처리 작업이란 결국 소스코드가 컴파일 되기 전에 수행되는 단계인데, 1) 매크로 정의 및 치환, 2) 파일 추가, 3) 조건부 컴파일 등을 처리하게 된다. 그리고 이 전처리기의 동작은 전처리 지시자에 의해 제어된다. 지금까지 살펴본 많은 예제코드에서 #define과 #include 지시자를 찾아볼 수 있었다.

 

지시자와 관련된 규칙을 살펴보자. 우선 1) 지시자는 #으로 시작되고 2) 세미콜론을 붙이지 않는다는 점을 chapter2에서 언급했었다. 이외에도 3) 지시자는 명시적으로 이어붙이지 않는 한 반드시 첫번째 개행문자로 끝나야 하고, 4) 프로그램 어디서든 등장할 수 있으며, 5) 지시자와 같은 줄에 주석이 나올 수 도 있다. 

(1) 매크로 정의

#define 지시자는 매크로를 정의해주고 #undef 지시자는 매크로 정의를 없애준다. 관련하여 우선 단순 매크로(=유사개체 매크로 object-like macro)를 살펴보자. 단순 매크로는 다음과 같은 서식을 갖는다.

#define 식별자 대체목록

직관적으로 파일에 식별자가 등장하게 되면 전처리기는 이를 대체목록으로 대체해준다는 것을 알 수 있다. 일반적으로 단순 매크로는 고유 상수를 정의하기 위해 주로 사용되는데, 이를 통해 프로그램의 가독성과 수정성을 높여줄 수 있게 된다. 이외에도 다음과 같은 활용이 가능하다.

  • C의 문법 수정 : #define LOOP for (;;)
  • 형 이름 개명 : #define BOOL int
  • 디버깅 목적 : #define DEBUG
    -> 이 경우 조건부 컴파일과 함께 디버깅 모드로 프로그램을 컴파일 하도록 할 수 있다.
    #ifdef DEBUG
        printf("디버깅 모드입니다.\n");
    #endif

매개변수 매크로는 다음과 같은 서식을 갖는다.

#define 식별자(x1, x2, ... xn) 대체목록

간단한 예로서 #define MAX(x, y) ((x) > (y) ? (x) : (y)) 와 같이 활용할 있는데. 이렇게 매개변수 매크로는 간단한 함수로서 동작하게 된다. 그렇다면 왜 함수대신 매개변수 매크로를 사용할까? 매개변수 매크로를 사용하면 1) 함수 호출 오버헤드가 없어 성능이 더 좋을 수 있고 2) 코드의 가독성을 높일 수 있다는 장점이 있다. 다만 1) 디버깅이 어렵고 2) 타입 체크를 하지 않기 때문에 타입 안정성이 부족하며 3) 매크로를 가리키는 포인터를 가질 수 없다는 단점이 존재한다.

 

매크로의 인자를 처리하는데 유용하게 사용되는 연산자인 #연산자와 ##연산자에 대해 정리해보자.

특성 #연산자 ##연산자
용도 매개변수 문자열화 두 매개변수를 결합
사용가능성 매개변수 매크로에서만 사용가능 단순 매크로, 매개변수 매크로 모두 사용가능
예시 #define TO_STRING(x) #x #define MK_ID(n) i##n
결과 인자를 문자열로 변환 두 인자를 하나의 토큰으로 결합
적용 예 TO_STRING(Hello) -> "Hello" MK_ID(1) -> i1

 

마지막으로 C언어의 사전정의 매크로를 살펴보자.

  • __LINE__ : 컴파일하는 파일의 줄 번호
  • __FILE__ : 컴파일하는 파일 이름
  • __DATE__ : 컴파일 날짜 (Mmm dd yyyy)
  • __TIME__ : 컴파일 시간 (hh::mm::ss)
  • __STDC__ 컴파일러가 C표준일시 1

(2) 파일 추가

#include 지시자는 전처리기에게 주어진 파일을 열어 그 내용을 현재 파일에 전부 붙여넣도록 한다. 이를 통해 여러 소스파일간에 정보들을 공유할 수 있게 해준다. #include 지시자는 다음 두 가지 형식을 갖는다.

  • #inlcude <filename> : C의 자체 라이브러리에 존재하는 헤더파일을 추가할 경우
  • #include "filename" : 직접 작성한 헤더파일을 추가할 경우 (경로를 입력할 수 도 있음)

두 형식은 지시자의 탐색 규칙에서 차이가 있는데, (컴파일러에 따라 다르지만) 두번째 형식에서는 현재 폴더에서 파일을 찾은 뒤 존재하지 않을 경우 첫번째 방식처럼 시스템 헤더 파일이 존재하는 폴더에서 찾는다고 한다.

(3) 조건부 컴파일

조건부 컴파일이란 특정 조건에 따라 코드의 일부를 컴파일할지 여부가 결정되는 것을 말한다. 이를 ㅌ통해 다양한 환경이나 설정에 맞게 코드를 조정할 수 있는데, 특히 디버깅시에 유용하게 사용될 수 있다.

  • #if와 #endif : #if const-expression 에서 상수 표현식의 값이 0이면 #if ~ #endif 사이의 줄이 전처리중 프로그램에서 제외된다. 그렇지 않다면 프로그램에 남아 컴파일의 대상이 된다. 참고로 정의되지 않은 식별자의 경우 값이 0인 매크로로 취급된다.
  • #ifdef, #ifndef와 #endif : 식별자가 현재 매크로로 정의되어 있는지 여부를 확인한다.
  • #elif, #else : if문처럼 여러겹으로 작성해줄 수 있게 한다

(4) 기타 지시자

  • #error : #error message에서, 컴파일러에게 오류 메세지를 출력하고 컴파일을 중단하도록 지시한다.
  • #line : #line n "파일명"에서, 컴파일러에게 소스코드의 현재 파일 이름과 행 번호를 변경하도록 지시한다.
  • #pragma : #pragma tokens에서, 컴파일러에게 특정한 행동을 지시한다.

3. 기타 알게된 것들

  • C 초창기에는 전처리기는 컴파일러에 넣을 출력물을 만들던 별도의 프로그램이었으나, 요즘의 전처리기는 컴파일러의 일부분인 경우가 많으며 그 결과물이 C 코드가 아닐때도 있다. 다만 대부분의 C 컴파일러는 전처리기의 출력물을 볼 수 있게 해준다.
  • 매크로는 전처리기에 의해 처리되므로 일반적인 스코프 규칙을 따르지 않고, 메크로 정의는 정의된 해당 파일 전체에 적용된다. 예를들어 함수 안에 정의된 매크로는 함수에 지역적으로 적용되는 것이 아니고  파일 전체에 적용된다.