매크로와 선행처리기(Preprocessor)

유석현(SeokHyun Yu)·2022년 7월 24일

C

목록 보기
25/26
post-thumbnail

1. 선행처리기와 매크로

위 그림에서 보이듯이 선행처리는 선행처리기에 의해서, 컴파일은 컴파일러에 의해서, 그리고 링크는 링커에 의해서 진행이 된다.

그런데 이 그림에서는 선행처리의 과정을 거친다는 점에 주목을 해야 한다.

컴파일 과정을 거치게 되면 바이너리 데이터로 이루어진 오브젝트 파일이 생성된다.

그렇다면 컴파일 이전에 진행되는 선행처리의 과정을 거치게 되면 어떠한 데이터로 채워진 파일이 생성될까?

선행처리의 과정을 거쳐서 생성되는 파일도 그냥 소스파일일 뿐이다.

왜냐하면 소스파일의 형태가 그대로 유지되기 때문이다.

선행처리기가 하는 일은 지극히 단순하다.

여러분이 삽입해 놓은 선행처리 명령문대로 소스코드의 일부를 수정할 뿐인데, 여기서 말하는 수정이란, 단순 치환의 형태를 띠는 경우가 대부분이다.

다음은 가장 간단한 선행처리 명령문이다.

#define PI 3.14

이처럼 선행처리 명령문은 # 문자로 시작을 하며, 컴파일러가 아닌 선행처리기에 의해서 처리되는 문장이기 때문에 명령문의 끝에 세미콜론을 붙이지 않는다.

#define PI 3.14

int main(void)
{
   num = PI * 3.5;
}
#define PI 3.14

int main(void)
{
   num = 3.14 * 3.5;
}

위 코드가 아래 코드로 변하는 과정이 선행처리 과정에서 이루어진다.

참고로 여기서 말하는 '선행처리'란, 컴파일 이전의 처리를 의미한다.

따라서 소스파일은 컴파일러에 의해서 컴파일 되기 이전에, 선행처리기에 의해서 선행처리의 과정을 거치게 된다.

자주해왔던 #include <stdio.h> 선언도 #문자로 시작하는 선행처리 명령문이다.

2. 대표적인 선행처리 명령문

앞서 정의한 #define 명령문을 다시 한 번 관찰하자.

#define PI 3.14

위 코드에서 보이듯이 선행처리 명령문은 기본적으로 세 부분으로 나뉘는데, 제일 먼저 등장하는 #define을 가리켜 '지시자'라 한다.

선행처리기가 이 부분을 보고 프로그래머가 지시하는 바를 파악하기 때문에 지시자라 하는 것이다.

그리고 #define 지시자는 선행처리기에게 다음과 같은 내용을 지시한다.

"이어서 등장하는 매크로를 마지막에 등장하는 매크로 몸체로 치환하라"

위의 코드에서 #define 뒤에 등장하는 PI를 가리켜 '매크로'라 하고, 그 뒤에 등장하는 것을 가리켜 '매크로 몸체'라 한다.

따라서 위의 선행처리 명령문은 다음의 내용을 선행처리기에 지시한다.

"매크로 PI를 매크로 몸체 3.14로 전부 치환하라"

결과적으로 PI라는 이름의 매크로는 그 자체로 상수 3.14가 된 셈이다.

참고로 PI와 같은 매크로를 가리켜 '매크로 상수'라 한다.

다음은 매크로 상수가 적용된 예이다.

#define NAME "홍길동"
#define AGE 24
#define PRINT_ADDR puts("주소: 경기도 용인시");

참고로 위 코드에서 보이듯이 매크로의 이름은 대문자로 정의하는 것이 일반적이다.


매크로는 매개변수가 존재하는 형태로도 정의할 수 있다.

그리고 이렇게 매개변수가 존재하는 매크로는 그 동작방식이 마치 함수와 유사하여 '매크로 함수'라 부르기도 한다.

다음은 매크로 함수의 예이다.

#define SQUARE(X) X*X

이 코드처럼 정의한 매크로를 접한 선행처리기는 SQUARE(X)와 동일한 패턴을 만나면, 무조건 X*X로 치환해버린다.

하지만 매크로 함수는, 생각보다 주의를 요하는 형태이기 때문에 안정적이지 못하다.

안정적인 형태가 되려면 매크로 함수를 정의할 때 매크로의 몸체부분을 구성하는 X와 같은 전달인자 하나하나괄호를 해야 함은 물론이고, 반드시 전체를 괄호로 한번 더 묶어줘야 한다.

따라서 다음과 같은 형태로 정의해야 안전하다.

#define SQUARE(X) ((X)*(X))

  • 매크로 함수의 장점
- 매크로 함수는 일반 함수에 비해 실행속도가 빠르다.

- 자료형에 따라서 별도로 함수를 정의하지 않아도 된다.
  • 매크로 함수의 단점
- 정의하기가 정말로 까다롭다.(특히 괄호를 쳐주는 부분)

- 디버깅하기가 쉽지 않다.

바로 위에서 설명한 매크로 함수의 장점과 단점을 종합해보면, 다음의 특성을 지니는 함수들을 매크로의 형태로 정의하는 것이 옳다는 결론이 나온다.

- 작은 크기의 함수

- 호출의 빈도수가 높은 함수

3. 조건부 컴파일을 위한 매크로

매크로 지시자 중에는 특정 조건에 따라 소스코드의 일부를 삽입하거나 삭제할 수 있도록 디자인 된 지시자가 있다.

if문이 조건부 실행을 위한 것이라면, #if... #endif조건부 코드 삽입을 위한 지시자이다.

이 지시자가 처리되는 방식은 다음 예제를 통해서 설명하겠다.

#include <stdio.h>

#define ADD 1
#define MIN 0

int main(void)
{
	int num1, num2;
	scanf("%d %d", &num1, &num2);
	
	#if ADD
		printf("%d", num1+num2); // 출력
	#endif
	
	#if MIN
		printf("%d", num-num2); // 출력되지 않음
	#endif
	
	return 0;
}

#if문 뒤에는 반드시 #endif문이 등장해야 하고, 이 두 지시자 사이에 존재하는 코드는 조건에 따라서 삽입 및 삭제가 된다.

매크로 ADDMIN이 각각 1과 0인 관계로 #if ADD 문장은 삽입이 되지만, #if MIN 문장은 삭제가 되어 실행되지 않는다.


#ifdef... #endif는 매크로가 정의되었느냐, 정의되지 않았느냐를 기준으로 동작한다.

#include <stdio.h>

// #define ADD 1
#define MIN 0

int main(void)
{
	int num1, num2;
	scanf("%d %d", &num1, &num2);
	
	#ifdef ADD
		printf("%d", num1+num2); // 출력되지 않음
	#endif
	
	#ifdef MIN
 		printf("%d", num1-num2); // 출력
 	#endif
 	
 	return 0;
}

#ifndef... #endif 도 있는데 이 지시자의 조합은 별도의 설명이 필요치 않을 것 같다.

#ifdef문이 '매크로 ~이 정의되어 있다면'의 의미를 지니는 반면, #ifndef문은 '매크로 ~이 정의되어 있지 않다면'의 의미를 지니기 때문이다.

참고로 #ifndef의 중간에 있는 n은 not을 의미한다.

이와 관련해서는 별도의 코드를 제시하지 않겠으나, 앞서 보인 코드에서 #ifdef를 #ifndef로 바꿔도 충분한 예제가 될 수 있다.


if문에 else를 추가할 수 있듯이 #if, #ifdef, #ifndef문에도 #else문을 추가할 수 있다.

#include <stdio.h>
#define NUM 5

int main(void)
{
	#if NUM==5
		puts("매크로 상수 NUM은 5입니다.");
	#else
		puts("매크로 상수 NUM은 5가 아닙니다.");
	#endif
	
	return 0;
}

if문에 else if를 여러 번 추가할 수 있듯이, #if문에도 #elif여러 번 추가할 수 있다.

#include <stdio.h>
#define NUM 7

int main(void)
{
	#if NUM==7
		puts("7입니다.");
	#elif NUM==8
		puts("8입니다.");
	#else
		puts("7, 8이 아닙니다.");
	#endif
	
	return 0;
}

4. 매개변수의 결합과 문자열화

문자열의 구성을 위한 매크로 함수를 다음의 형태로 정의하였다.

#define STRING(A, B) "A의 직업은 B입니다."

그리고 STRING(이동춘, 나무꾼)이라는 매크로 문장이 "이동춘의 직업은 나무꾼입니다."와 같은 문자열을 만들어낼 것을 기대했지만 실제로는 그렇지 못하다.

문자열 안에서는 매크로의 매개변수 치환이 발생하지 않기 때문이다.

그렇다면 어떻게 해야될까?

문제의 해결을 위해서 매크로의 # 연산자를 소개하겠다.

#define STR(ABC) #ABC

이는 다음과 같은 형태로 사용하는 연산자이다.

위의 문장에는 다음의 뜻이 담겨있다.

"매개변수 ABC에 전달되는 인자를 문자열 "ABC"로 치환해라"

이렇듯 # 연산자는 치환의 결과문자열로 구성하는 연산자이다.

예를 들어 다음 두 문장 STR(123), STR(12, 23, 34) 은 선행처리기에 의해서 "123", "12, 23, 34"의 문자열로 치환이 된다.

참고로 문자열은 나란히 선언하면, 하나의 문자열로 간주가 된다.

따라서 다음과 같이 문자열을 선언하는 것도 가능하다.

char * str = "ABC" "DEF";

그리고 이는 다음의 문자열 선언과 동일하다.

char * str = "ABCDEF";

따라서 다음과 같이 문장을 구성하는 것도 가능하다.

char * str = STR(12) STR(34);

그럼 이제까지 설명한 내용을 바탕으로 앞서 보인 매크로 함수 STRING을 다시 정의해보자

#include <stdio.h>
#define STRING(A, B) #A "의 직업은 " #B "입니다."

int main(void)
{
   printf("%s", STRING(홍길동, 나무꾼));

   return 0;
}

그럼 성공적으로 원하는 출력이 나오는 것을 알 수 있다.


전달인자들이 단순히 이어지기를 원한다면 ## 연산자를 사용하면 된다.

이 연산자는 매크로 함수의 전달인자를 다른 대상(전달인자, 숫자, 문자, 문자열 등)과 이어줄 때 사용한다.

#define EX1(A, B, C) A ## B ## C
#define EX2(A, B) A ## 00 # B
profile
Backend Engineer

0개의 댓글