흔히 소스파일을 빌드하여 실행파일을 만든다고 말합니다. 하지만 엄밀히 말하면 소스파일이 컴파일이 되기전에 거치는 하나의 과정이 있어요. 이를 퉁쳐서 그냥 컴파일과정에 포함시키기도 하는데 엄밀히 말하면 '선행처리'라는 하나의 과정입니다.

predefine

그렇다면 선행처리에서는 어떤일이 일어나느냐?

지금까지 #include 라는 문장을 수도없이 많이 봐왔을겁니다. 말 그대로 해석하자면 stdio.h라는 헤더파일의 소스코드를 복사해서 그대로 붙여넣으라는 의미입니다. 저 문장은 선행처리를 거치면서 stdio.h 헤더파일의 전체 소스코드로 치환이 됩니다.

모든 선행처리 명령문에는 #이 붙습니다. #을 이용하는 선행처리 명령문은 #include뿐만이 아닙니다. #include 만큼이나 엄청 많이 쓰는게 #define 선언문입니다. 다음의 코드를 봐주세요.

#include <stdio.h>
#define PI 3.141592
#define TRUE 1
#define FALSE 0

int main(void)
{
    int rad = 10;
    printf(“The area of circle is %f\n”, rad*rad*PI);

    if(TRUE)
    {
        printf(“TRUE is 1\n”);
    }

    if(FALSE)
    {
        printf(“FALSE is 0\n”);
    }

    return 0;
}

위의 코드는 선행처리기를 거친 이후 어떻게 바뀔까요? 선행처리기를 거쳐서 나오면 위의 #define 명령에 따라 아래 소스코드의 모든 PI들이 3.141592로 바뀌고 TRUE가 1, FALSE가 0으로 바뀝니다. #define은 선행처리 과정에서 우리가 임의로 만들어 쓴 이름값들에 실제 데이터의 값을 대입해주는 과정을 거치도록 해줍니다.

실행결과

The area of circle is 314.159200

TRUE is 1

계속하려면 아무 키나 누르십시오 . . .

#define 명령문의 구조에 대해서 확실히 짚고 넘어가보겠습니다.

predefine

#define은 사용자의 지시를 뜻하기때문에 ‘지시자’ 라고 합니다. 그리고 우리가 임의로 정하는 이름을 ‘매크로’라고 하고 매크로가 치환 될 데이터를 ‘매크로 몸체’라고 합니다. 직관적으로 이해가 쉽기때문에 충분히 잘 사용하시리라 봅니다.

그리고 지시자는 위와같은 상수만 대입이 가능한게 아니에요. 함수의 형태로도 치환명령이 가능합니다. 다음의 코드를 봐주세요.

#define HALF(X) X/2
#define SQUARE(X) X*X

위와 같은 형태의 패턴이 나오면 뒤의 매크로 몸체의 식으로 연산을 시키라는 의미입니다. 가령 HALF(num)이 나오면 그것을 num/2로 치환하고 SQUARE(15)가 나오면 15*15로 치환이 됩니다.

먼저 설명한 PI같은 경우 매크로상수라 하고 방금 말씀드린 함수형태의 매크로를 가리켜 매크로함수라고 합니다.

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

int  main(void)
{
    int rad = 10;
    double area = PI*SQUARE(rad);

    printf(“The circle area = %f\n”, area);
}

실행결과

The circle area = 314.159200

계속하려면 아무 키나 누르십시오…

한 가지 질문을 던지겠습니다.

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

int  main(void)
{
    int num1 = 3, num2 = 5;

    printf(“%d\n”, SQUARE(num1+num2));

    return 0;

} 

위 코드의 출력결과는 무엇이 될 것 같나요? 아마 대부분은 주저없이 3과 5를 더해 8이 되고 그것을 제곱하면 64가 나오겠네 라고 생각할겁니다.

출력결과

23

계속하려면 아무 키나 누르십시오…

근데 결과는 23이 나왔습니다. 왜 이런결과가 나왔는지는 연산이 어느 과정에서 이루어지느냐 생각해보시면 의외로 간단히 나옵니다. 선행처리기는 그저 치환만 해줄 뿐입니다. 계산을 해주지 않아요. 위의 소스코드가 선행처리기를 거치면 printf함수 내부의 SQUARE(num1+num2) 부분이 num1+num2num1+num2로 바뀌고 연산은 이후에 컴파일 과정에서 일어나는겁니다. 단순히 치환만 해주기 때문에 이런결과가 나오는거에요. 저 식은 3+53+5 이기때문에 컴파일러는 3+15+5 = 23으로 연산을 하는 것입니다.

위와같은 결과를 피하시려면 우리는 num1+num2에 괄호를 씌워주면 됩니다. 이같은 실수는 매크로를 사용하면서 흔히 저지르는 실수입니다. 따라서 #define 명령문을 정의할 때에 각 변수별로 괄호를 치고 먼저 연산이 되길 바라는 부분을 괄호를 치고 부담스러울정도로 괄호를 치시면 됩니다. 그래야 실수를 안합니다.

사실 매크로함수를 사용하지 않아도 따로 함수를 정의하면 됩니다. 하지만 많은 현업 프로그래머들이 매크로를 사용해요. 충분히 메리트가 있다는 이야기입니다.

먼저 매크로함수는 자료형에 구애받지 않아 전달인자를 고려해 정의해야하는 함수보다 사용이 편리합니다. 게다가 함수처럼 스택메모리의 할당이나 매개변수로의 인자전달, 값의 반환 등의 과정이 없으므로 프로그램의 성능이 좋아집니다.

하지만 조심해야할건 아까 제시한 예제코드에서도 확인할 수 있듯이 정의를 할 때 신경을 많이 써야합니다. 그리고 만약 매크로 부분에서 에러가 났다면 컴파일러는 선행처리가 끝난 이후부터 디버깅을 지원하기때문에 제대로 에러가 난 부분을 찾아줄 수 없다는 치명적인 단점도 존재합니다.

이러한 이유들로 정의하기가 그렇게 복잡하지 않은 간단한 함수나 엄청나게 자주 호출하는 함수 정도만 매크로함수로 정의해주는것이 좋습니다.

또 전처리에서 우리가 유용하게 사용하는명령문이 있어요. #if ~ #endif와 #ifdef ~ #endif 그리고 #ifndef ~ #endif 명령문들입니다. 어떤 것인지 코드로 봅시다.

#if ~ #endif : 참일 때 컴파일 수행

#ifdef ~ #endif : 정의되었을 때 컴파일 수행

#ifndef ~ #endif : 정의되지않았을 때 컴파일 수행

하나씩 살펴봅시다.

#include <stdio.h>
#define TRUE 1
#define FAlSE 0

int main(void)
{

    int num1=10, num2=20;
    int add;

    #if TRUE
        add = num1+num2; printf(“%d\n”, add);
    #endif

    #if FALSE //조건 미일치로 다음 문장 컴파일 제외
        add = num1-num2; printf(“%d\n”, add);
    #endif

    return 0;
}

실행결과

30

계속하려면 아무 키나 누르십시오 . . .

위의 코드에서 #if FALSE 다음에 삽입된 문장은 전처리기에서 아예 삭제를 시켜버려 컴파일조차 되지 않는 문장입니다. 일반 if문과 달라요. 일반 if문은 컴파일되고 실행이 되지 않을 뿐 메모리공간을 차지하지만 위의 #if ~ #endif문은 조건이 일치하지 않으면 아예 컴파일조차 되지않는 문장입니다.

이번에는 #ifdef ~ #endif문에 관해 알아보겠습니다.

#include <stdio.h>
#define ROW 5
#define COL 10

int main(void)
{

    int row = 0, col = 0, height = 0;
    #ifdef ROW
        row = ROW;
    #endif

    #ifdef COL
        col = COL;
    #endif

    #ifdef HEIGHT
        height = HEIGHT; //HEIGHT 정의되지 않았으므로 전처리 과정에서 제외되는 문장
    #endif

    printf(“%d %d %d\n”, row, col, height);

    return 0;
}

실행결과

5 10 0

계속하려면 아무 키나 누르십시오 . . .

ROW와 COL은 #define에 의해서 정의되어있지만 HEIGHT는 정의되지 않아 height = HEIGHT;라는 문장은 컴파일조차 되지 않습니다. 출력결과가 보여주듯이 height의 값은 변수를 선언하며 초기화시킨 값인 0을 그대로 가지고 있습니다.

마지막으로 #ifndef ~ #endif를 보겠습니다.

#include <stdio.h>
#define ROW 5
#define COL 10

int main(void)
{
    int row = 0, col = 0, height = 0;
    #ifdef ROW
        row = ROW;
    #endif

    #ifdef COL
        col = COL;
    #endif

    #ifdef HEIGHT
        height = HEIGHT;
        printf(“3차원 도형이며 부피는 %d입니다\n”, row*col*height);
    #endif

    #ifndef HEIGHT
        printf(“2차원 도형이며 넓이는 %d입니다\n”, row*col);
    #endif

    return 0;
}

실행결과

2차원 도형이며 넓이는 50입니다

계속하려면 아무 키나 누르십시오 . . .

위에서 HEIGHT는 정의되지 않았고 따라서 ifndef HEIGHT 명령문의 다음 문장은 전처리기에서 컴파일에 포함시킵니다. 따라서 위와같은 출력결과가 나온것입니다.

그리고 #if~#endif / #ifdef~#endif / #ifndef~#endif 모두 if문과 마찬가지로 #else를 추가할 수 있습니다.

#include <stdioh>
#define ROW 5
#define COL 10

int main(void)
{
    int row = 0, col = 0, height = 0;

    #ifdef ROW
        row = ROW;
    #endif

    #ifdef COL
        col = COL;
    #endif

    #ifdef HEIGHT
        height = HEIGHT;
        printf(“3차원 도형이며 부피는 %d입니다\n”, row*col*height);
    #else
        printf(“2차원 도형이며 넓이는 %d입니다\n”, row*col);
    #endif

    return 0;
}

실행결과

2차원 도형이며 넓이는 50입니다

계속하려면 아무 키나 누르십시오 . . .

대표로 #ifdef문에만 추가시켜보았는데 #if / #ifdef / #ifndef 명령문에 모두 추가가 가능합니다. 그리고 if문에 else if가 있는것처럼 매크로 조건문에는 #elif라는게 있어요. 하지만 #elif는 #if문에만 추가가 가능합니다. #ifdef와 #ifndef문에는 추가할 수 없어요.

#include 
#define NUM 10

int main(void)
{
    #if NUM == 3
        printf(“NUM is 3\n”);
    #elif NUM == 5
        printf(“NUM is 5\n”);
    #elif NUM == 8
        printf(“NUM is 8\n”);
    #elif NUM == 10
        printf(“NUM is 10\n”);
    #else
        printf(“I don’t know\n”);
    #endif

    return 0;
}

실행결과

NUM is 10

계속하려면 아무 키나 누르십시오 . . .

if문과 사용법이 비슷하기때문에 직관적으로도 바로바로 이해 되시죠? 오늘 이야기하는부분이 조금 다른것은 실행의 유무가 아닌 컴파일 자체의 유무라는것만 다시 한 번 명심해주세요.

이제 하나 남았습니다. 매크로의 몸체에 문자열이 있는 경우에요.

#include 
#define LOVE(A,B) “A loves B.”

int main(void)
{
    printf(“%s\n”, LOVE(Kelly, John));
    printf(“%s\n”, LOVE(Victor, Peter));

    return 0;
}

위와 같은 매크로함수를 정의했을 때 위 코드의 출력결과는 “Kelly loves John”과 “Victor loves Peter”가 나올거라고 생각하실수도 있는데 그렇게 되지 않습니다.

실행결과

A loves B.

A loves B.

계속하려면 아무 키나 누르십시오 . . .

문자열 내에서는 매크로의 치환이 일어나지 않습니다. 그럼 어떻게해야하는지 해결방법을 코드로 바로 보여드리겠습니다.

#include 
#define LOVE(A,B) #A ” loves ” #B “.”

int main(void)
{
    printf(“%s\n”, LOVE(Kelly, John));
    printf(“%s\n”, LOVE(Victor, Peter));

    return 0;
}

실행결과

Kelly loves John.

Victor loves Peter.

계속하려면 아무 키나 누르십시오 . . .

됩니다. 문자열 내에 포함된 매개변수를 큰따음표(” “) 밖으로 빼내어 #을 붙여주면 매크로의 치환이 일어납니다.

선행처리와 매크로는 현업에서도 자주 쓰는 기능들입니다. 특히나 절대 변경되어서는 안되는 하나의 고유값의 의미를 가지는 데이터들은 #define으로 매크로상수로 만들어버리고 성능에 민감한 프로그램을 만드는 경우는 매크로함수도 많이 씁니다.

+ Recent posts