[C언어] 매크로와 선행처리기(Preprocessor)

김민정·2024년 9월 11일
0
post-thumbnail

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

이번 chapter에서는 C언어의 문법과 직접적인 연관은 없지만 실행파일의 생성과 관련해서 중요한 컴파일 과정의 일부로 포함되어 있는 '선행처리'에 대해 배울 예정이다.

26-1 "선행처리기와 매크로"

Chapter1에서는 실행파일이 컴파일과 링크의 과정을 거쳐서 만들어지는 것이라고 했다.
(기억을 되짚어보자...)
그러나 실제로는 컴파일 이전에 '선행처리'라는 과정을 거치게 된다.
다만 이를 컴파일 과정에 포함시켜서 이야기 하는 것이 보통이기 때문에 별도의 구분을 하지 않았었다.
이번 Chapter에서는 이 '선행처리'가 주를 이루기 때문에 별도로 구분해서 언급했다.

선행처리란?

선행처리란 컴파일 이전의 처리를 의미한다.
아래 그림과 같이 선행처리는 선행처리기에 의해, 컴파일은 컴파일러에 의해, 그리고 링크는 링커에 의해 진행된다.

컴파일 과정을 거치게 되면 바이너리 데이터로 이루어진 오브젝트 파일이 생성된다.
컴파일 이전에 진행되는 선행처리의 과정을 거치게 되면 어떠한 데이터로 채워진 파일이 생성될까?
선행처리의 과정을 거쳐서 생성되는 파일도 근야 소스파일일 뿐이다. 소스파일의 형태가 그대로 유지되기 때문이다.
선행처리기가 하는 일은 단순하다.
사용자가 삽입해 놓은 선행처리 명령문대로 소스코드의 일부를 수정할 뿐인데, 여기서 말하는 수정이란? 단순 치환(substitution)의 형태를 띠는 경우가 대부분이다.
다음 예시로 간단한 선행처리 명령문을 보자.

#define PI 3.14

우리가 계속 사용했던 #include처럼 선행처리 명령문은 #문자로 시작하며 컴파일러가 아닌 선행처리기에 의해 처리되기 때문에 세미콜론을 끝에 붙이지 않아도 된다.
위 문장이 선행처리 과정을 지나면 이후 코드에서 PI를 만날 때 무조건 3.14로 변환된다.


26-2 "대표적인 선행처리 명령문"

define: Object-like macro

위 그림처럼 선행처리 명령문은 기본적으로 세 부분으로 나뉜다.
지시자#define파트는 선행처리기가 이 부분을 보고 프로그래머가 지시하는 바를 파악한다.
이는 "이어서 등장하는 매크로를 마지막에 등장하는 매크로 몸체로 치환하라!"라는 내용을 지시한다.

#define뒤에 등장하는 것을 가리켜 매크로라고 한다.
그리고 그 뒤에 등장하는 것을 매크로 몸체(또는 대체 리스트)라 한다.
따라서 위의 선행처리 명령문은 "매크로 PI를 매크로 몸체 3.1415로 전부 치환하라!"라는 내용을 선행처리기에게 지시한다.

PI와 같은 매크로를 가리켜 오브젝트와 유사한 매크로(object-like macro) 또는 매크로 상수라 한다.

다음 예제를 통해 매크로 상수가 적용된 예와 그 결과를 확인해보자.

#include <stdio.h>

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

int main()
{
    printf("이름: %s\n", NAME);
    printf("나이: %d\n", AGE);
    PRINT_ADDR;
    return 0;
}

> 출력
이름: 홍길동
나이: 24
주소: 경기도 용인시

참고로 매크로의 이름은 대문자로 정의하는 것이 일반적이다.
대문자로 정의함으로써 이 식별자가 매크로라는 사실을 부각시킬 수 있다.

#define: Function-like macro

매크로는 매개변수가 존재하는 형태로도 정의할 수 있다.
이렇게 매개변수가 존재하는 매크로는 그 동작방식이 마치 함수와 유사해서 함수와 유사한 매크로(function-like macro)라 하는데 줄여서 매크로 함수라고도 한다.
다음은 매크로 함수의 예다.

#define SQUARE(X) X*X

이 명령문은 아래 그림과 같이 해석된다.

괄호 안 X는 정해지지 않은 임의의 값(또는 문장)을 의미한다.
이렇게 정의한 매크로를 접한 선행처리기는 SQUARE(X)와 동일한 패턴을 만나면 무조건 X*X로 치환해버린다.
이렇게 선행처리기에 의해서 변환되는 과정 자체를 매크로 확장(macro expansion)이라 한다.
다음 예제를 통해 매크로 확장의 결과를 보자.

#include <stdio.h>
#define SQUARE(X) X*X

int main()
{
    int num = 20;

    /* 정상적 결과 출력 */
    printf("Square of num: %d \n", SQUARE(num));
    printf("Square of -5: %d \n", SQUARE(-5));
    printf("Square of 2.5: %d \n", SQUARE(2.5));

    /* 비정상적 결과 출력 */
    printf("Square of 3+2: %d \n", SQUARE(3+2));
    return 0;
}

> 출력
Square of num: 400 
Square of -5: 25
Square of 2.5: 0
Square of 3+2: 11

잘못된 매크로 정의

위 예제에서 정의된 매크로에 어떤 문제가 있는지 보자.

SQUARE(3+2)

이를 함수의 관점에서 본다면 3과 2의 합인 5를 SQUARE함수의 인자로 전달하는 것으로 생각하는 것이 당연하다.
하지만 그렇게 동작하지 않았다.
먼저 연산을 하고, 그 연산결과를 가지고 함수를 호출하게끔 돕는 것은 컴파일러이고,
매크로는 선행처리기에 의해 처리되기 때문이다.
따라서 저 명령어는 3+2*3+2로 처리되어 11이 출력되는 것이다.

해결 방법은 연산을 괄호로 싸서 매개변수로 넣으면 된다.
또는 매크로 함수를 정의할 때 (X)*(X)라고 하면 된다.

하지만, 이렇게 되면 아래 상황일 때는 어떻게 될까?

int num = 120 / SQUARE(2);

120/4이기 때문에 30을 기대하지만 실제 값은 120으로 초기화 된다.
그 이유는 120 / (2) * (2)으로 치환되기 때문이다.

그렇다면 매크로 함수를 ((X)*(X))로 정의하면 해결된다.

따라서 매크로 함수를 정의할 때에는 매크로의 몸체부분을 구성하는 X와 같은 전달인자 하나하나에 괄호를 해야 함은 물론이고, 반드시 전체를 괄호로 한번 더 묶어줘야 한다는 사실을 기억하자~!

매크로를 두 줄에 걸쳐서 정의하는 방법

정의하는 매크로의 길이가 길어지는 경우에는 가독성을 위해서 두 줄에 걸쳐서 매크로를 정의하기도 한다.
매크로를 두 줄 이상에 걸쳐서 정의할 때에는 \문자를 활용해서 줄이 바뀌었음을 명시해준다.

#define SQUARE(X)	\
		((X)*(X))
매크로 정의시, 먼저 정의된 매크로도 사용 가능

먼저 정의된 매크로는 뒤에서 매크로를 정의할 때 사용할 수 있다. 예제를 통해 확인해보자.

#include <stdio.h>
#define PI 3.14
#define PRODUCT(X,Y) ((X)*(Y))
#define CIRCLE_AREA(R) (PRODUCT((R), (R))*PI)

int main()
{
    double rad = 2.1;
    printf("Radius: %f\narea: %g \n", rad, CIRCLE_AREA(rad));
    return 0;
}

> 출력
Radius: 2.100000
area: 13.8474

매크로 함수의 장단점

매크로 함수를 정의하는 것은 일반 함수를 정의하는 것보다 복잡하다.
정의하고자 하는 함수의 크기가 크면, 매크로로 정의하는 것 자체가 불가능할 수도 있다.
그럼에도 불구하고 매크로 함수를 정의하는 이유가 뭘까?
매크로 함수의 장단점을 알아보자.

매크로 함수의 장점은 아래와 같다.

  • 매크로 함수는 일반 함수에 비해 실행속도가 빠르다.
  • 자료형에 따라서 별도로 함수를 정의하지 않아도 된다.

매크로 함수의 실행속도가 빠른 이유는 일반적인 함수가 호출되면
1) 호출된 함수를 위한 스택 메모리 할당
2) 실행위치의 이동과 매개변수로의 인자 전달
3) return 문에 의한 값의 반환
이렇게 3가지 이유 때문에 함수의 빈번한 호출은 실행속도 저하로 이어지게 된다.

반면에, 매크로 함수는 선행처리기에 의해서 매크로 함수의 몸체부분이 매크로 함수의 호출 문장을 대신하기 때문에 위에 얘기한 사항들이 동반하지 않는다.

매크로 함수의 단점은 다음과 같다.

  • 정의하기가 정말로 까다롭다.
  • 디버깅하기 쉽지 않다.

만약에 두 값의 차를 계산하는 함수가 있는데 반환하는 값이 절댓값 형태여야 한다. 그럼 아래의 형태처럼 함수를 만들 수 있는데

int DrffABS(int a, int b)
{
	if(a>b)
    	return a-b;
    else
    	return b-a;
}

이것을 매크로 함수로 구현하게 된다면 정의하는 과정이 살짝 부담스럽다. (#define DIFF_ABS(X, Y) ((X)>(Y) ? (X)-(Y) : (Y)-(X))으로 하면 되지만,,, 어쨋든 부담스럽다!)
두 번째 단점은 아래 예시를 통해 알아보자.

#include <stdio.h>
#define DIFF_ABS(X, Y) ((x)>(y) ? (x)-(y) : (y)-(x))

....

이렇게 하고 다음 코드에서 DIFF_ABS 매크로 함수를 사용하게 되면 선언된 적없는 x와 y를 사용한다고 에러 메시지가 출력된다.
대문자와 소문자를 구분하기 때문에 이처럼 매크로를 잘못 정의한 경우, 에러 메시지는 선행처리 이전의 소스파일을 기준으로 출력되지 않고 선행처리 이후의 소스파일 기준으로 출력이 된다.
이런 점은 일반적인 에러 메시지보다 이해하기 힘들다는 단점이 있다.

매크로 함수 정의

따라서 다음 특성을 지니는 함수들은 매크로 형태로 정의하는 것이 옳다.

  • 작은 크기의 함수
  • 호출의 빈도수가 높은 함수

함수의 크기가 작아야 매크로의 형태로 정의하기 편하고 에러의 발생 확률도 낮아서 디버깅에 대한 염려를 덜 수 있다.
호출의 빈도수가 높아야 매크로 함수가 가져다 주는 성능 향상의 이점도 최대한 누릴 수 있다.


26-3 "조건부 컴파일(Conditional Compilation)을 위한 매크로"

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

#if...#endif: 참이라면

if문이 조건부 실행을 위한 것이라면 #if...#endif는 조건부 코드 삽입을 위한 지시자이다.
이 지시자의 처리 방식은 다음 예제를 통해 확인해보자.

#include <stdio.h>
#define ADD 1
#define MIN 2

int main()
{
    int num1, num2;
    printf("두 개의 정수 입력: ");
    scanf("%d %d", &num1, &num2);

#if ADD     // ADD가 참이라면
    printf("%d + %d = %d \n", num1, num2, num1 + num2);
#endif

#if MIN     // MIN이 참이라면
    printf("%d - %d = %d \n", num1, num2, num1-num2);
#endif

    return 0;
}

> 출력
두 개의 정수 입력: 5 4
5 + 4 = 9
5 - 4 = 1

2행과 3행에 정의되어 있는 매크로 ADD와 MIN이 각각 1과 0인 관계로 #endif에 해당되는 부분은 삭제되었다.
#if문의 구성에는 연산자도 활용할 수 있다.

#ifdef...#endif: 정의되었다면

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

#include <stdio.h>
// #define ADD 1
#define MIN 2

int main()
{
    int num1, num2;
    printf("두 개의 정수 입력: ");
    scanf("%d %d", &num1, &num2);

#ifdef ADD     // ADD가 참이라면
    printf("%d + %d = %d \n", num1, num2, num1 + num2);
#endif

#ifdef MIN     // MIN이 참이라면
    printf("%d - %d = %d \n", num1, num2, num1-num2);
#endif

    return 0;
}

> 출력
두 개의 정수 입력: 7 2
7 - 2 = 5

#ifndef...#endif: 정의되지 않았다면

이 지시자는 #ifdef...#endif의 반대이다.
위 예제를 반대로 바꿔서 진행해보면 알 수 있다.

#else의 삽입

if문에서 else를 추가할 수 있듯이 위에서 제시한 #if, #ifdef, #ifndef문 모두 #else문을 추가할 수 있다.
다음 예제를 통해서 삽입의 방식과 의미를 알아보자.

#include <stdio.h>
#define HIT_NUM 5

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

> 출력
매크로 상수 HIT_NUM은 현재 5입니다.

elif의 삽입

if문에서 else if를 여러 번 추가할 수 있듯,
#if문에서만 #elif를 여러 번 추가할 수 있다.
그리고 이 형식의 끝을 #else로 마무리 할 수 있다.

#include <stdio.h>
#define HIT_NUM 7

int main()
{
#if HIT_NUM==5
    puts("매크로 상수 HIT_NUM은 현재 5입니다.");
#elif HIT_NUM==6
    puts("매크로 상수 HIT_NUM은 현재 6입니다.");
#elif HIT_NUM==7
    puts("매크로 상수 HIT_NUM은 현재 7입니다.");
#else
    puts("매크로 상수 HIT_NUM이 5, 6, 7은 확실히 아닙니다.");
#endif
    return 0;
}

> 출력
매크로 상수 HIT_NUM은 현재 7입니다.

26-4 "매개변수의 결합과 문자열화"

이번에는 두 개의 매크로 연산자를 배울 것이다.
이 연산자를 배울 땐 각자 조건이 있다.

조건 1) 문자열 내에서는 매크로의 매개변수 치환이 발생하지 않습니다.

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

#define STRING_JOB(A, B) "A의 직업은 B이다."

그리고는 STRING_JOB(이동춘, 나무꾼)이라는 매크로 문장이 "이동춘의 직업은 나무꾼이다."와 같은 문자열을 만들어 낼 것을 기대한다.
하지만 문자열 안에서는 매크로의 매개변수 치환이 발생하지 않기 때문에 위 문자열을 만들어내지 못한다.

이를 위해 필요한 것이 #연산자이다.

# 연산자

#연산자는 문자열 내에서 매크로의 매개변수 치환이 가능하도록 한다.

#define STR(ABC) #ABC

위 문장은 매개변수 ABC에 전달되는 인자를 문자열 "ABC"로 치환하라는 의미를 담고 있다.
#연산자는 치환의 결과를 문자열로 구성하는 연산자이다.
문자열을 나란히 선언하면 하나의 문자열로 간주된다.
따라서 다음과 같이 문자열을 선언하는 것도 가능하다.

char * str = "ABC" "DEF";
char * str = "ABCDEF";

위와 아래의 문자열 선언은 동일하다.
이 내용을 바탕으로 예제를 보자.

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

int main()
{
    printf("%s \n", STRING_JOB(이동춘, 나무꾼));
    printf("%s \n", STRING_JOB(한상순, 사냥꾼));
    return 0;
}

> 출력
이동춘의 직업은 나무꾼입니다. 
한상순의 직업은 사냥꾼입니다.

조건 2) 특별한 매크로 연산자 없이 단순히 연결하는 것은 불가능하다.

대학교의 학번은 아래 그림과 같이 조합되어 발급된다.

우리는 학번을 조합하는 매크로 함수를 정의하고자 한다.
이 함수는 STNUM(10, 65, 175);와 같은 형태로 호출되고 이 문장은 선행처리기에 의해서 1065175라고 치환되어야 한다.
구현할 수 있는 경우의 수를 아래 예제를 통해 살펴보자.

#include <stdio.h>
//#define STNUM(Y, S, P) YSP
//#define STNUM(Y, S, P) Y S P
#define STNUM(Y, S, P) ((Y)*100000+(S)*1000+(P))

int main()
{
    printf("학번: %d \n", STNUM(10, 65, 175)); 
    printf("학번: %d \n", STNUM(10, 65, 075));
    return 0;
}

> 출력
[2번째 줄 사용할 때]
	> gcc .\UnivStdNum.c
.\UnivStdNum.c: In function 'main':
.\UnivStdNum.c:2:24: error: 'YSP' undeclared (first use in this function)
 #define STNUM(Y, S, P) YSP
                        ^
.\UnivStdNum.c:8:29: note: in expansion of macro 'STNUM'
     printf("학번: %d \n", STNUM(10, 65, 175));
                             ^~~~~
.\UnivStdNum.c:2:24: note: each undeclared identifier is reported only once for each function it appears in 
 #define STNUM(Y, S, P) YSP
                        ^
.\UnivStdNum.c:8:29: note: in expansion of macro 'STNUM'
     printf("학번: %d \n", STNUM(10, 65, 175));
                             ^~~~~

[3번째 줄 사용할 때]
	> gcc .\UnivStdNum.c
.\UnivStdNum.c: In function 'main':
.\UnivStdNum.c:8:39: error: expected ')' before numeric constant
     printf("학번: %d \n", STNUM(10, 65, 175));
                                       ^
.\UnivStdNum.c:3:26: note: in definition of macro 'STNUM'
 #define STNUM(Y, S, P) Y S P
                          ^
.\UnivStdNum.c:9:39: error: expected ')' before numeric constant
     printf("학번: %d \n", STNUM(10, 65, 075));
                                       ^
.\UnivStdNum.c:3:26: note: in definition of macro 'STNUM'
 #define STNUM(Y, S, P) Y S P
                          ^

[4번째 줄 사용할 때]
학번: 1065175 
학번: 1065061

2번째 줄을 사용할 경우 printf("학번: %d \n", YSP);가 되어 에러가 발생하고
3번째 줄을 사용할 경우 printf("학번: %d \n", 10 65 175);로 치환되어 에러가 발생한다.
4번재 줄을 사용할 경우 에러가 발생하진 않지만 마지막 매개변수가 0으로 시작하게 되면 8진수로 이해하게 되어 원하는 형태의 답을 얻진 못한다.

## 연산자

##연산자는 필요한 형태대로 단순하게 결합할 수 있도록 도와준다. 즉, 매크로 함수의 전달인자를 다른 대상(전달인자, 숫자, 문자, 문자열 등)과 이어줄 때 사용한다.

#define CON(UPP, LOW) UPP ## 00 ## LOW

위 매크로 몸체에는 UPP와 00과 LOW가 순서대로 이어질 수 있도록 ##연산자가 사용되었다.

#include <stdio.h>
//#define STNUM(Y, S, P) YSP
//#define STNUM(Y, S, P) Y S P
//#define STNUM(Y, S, P) ((Y)*100000+(S)*1000+(P))
#define STNUM(Y, S, P) Y ## S ## P

int main()
{
    printf("학번: %d \n", STNUM(10, 65, 175)); 
    printf("학번: %d \n", STNUM(10, 65, 075));
    return 0;
}

> 출력
학번: 1065175 
학번: 1065075

따라서 우리가 원하는 형태의 학번을 얻을 수 있다...!


<Review>

후.... 새벽 2시가 넘어서야 스터디할 분량을 다 마무리 했다!
매크로 함수 아주 매력적인 친구다.
그리고 그 친구와 함께 매크로 함수를 더욱 풍부하게 사용할 수 있게 해주는 연산자 ###도 기억해야겠다.

이만 Bonne Nuit~🌜

profile
백엔드 코린이😁

0개의 댓글