programming/C

C // 선행처리기와 매크로

깨래 2018. 1. 8. 17:16

선행처리기


실행파일이 만들어지기까지 소스파일 -> 선행처리기 -> 컴파일러 -> 링커 -> 실행파일.exe 가 이루어지는데, 여기서의 선행처리기를 말한다. 컴파일 이전에 선행처리를 거친다.


컴파일 과정을 거치게 되면 바이너리 데이터로 이루어진 오브젝트 파일이 생성된다. 하지만 선행처리의 과정을 거쳐서 생성되는 파일은 그냥 소스파일 일 뿐이다.


선행처리기가 하는 일은 프로그래머가 삽입해 놓은 선행처리 명령문대로 소스코드의 일부를 수정할 뿐인데, 여기서 수정이란 단순치환(substitution)의 형태를 띤다.


#define PI 3.14

" PI를 만나면 3.14로 치환하라 "


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


소스코드의 모든 PI는 3.14 로 바뀌게 된다. 이는 컴파일 되기 이전에 선행처리를 거치는 단계이다.


선행처리 명령문


매크로 상수


#define PI 3.14


#define : 지시자, 이어서 등장하는 매크로를 마지막에 등장하는 매크로 몸체로 치환하라


PI : 매크로


3.14 : 매크로 몸체


결과적으로 PI라는 이름의 매크로는 상수 3.14가 된 셈이다. 이를 매크로 상수라고 한다.


ex)

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
#define NAME "홍길동"
#define AGE 24
#define PRINT_ADDR puts("주소 : 세종시 종촌동")
 
int main()
{
    printf("이름: %s \n", NAME);
    printf("나이: %d \n", AGE);
    PRINT_ADDR;
    
    return 0;
}

cs

출력결과

이름홍길동 

나이: 24 

주소 : 세종시 종촌동


매크로의 이름은 대문자로 정의하는 것이 일반적이다.


매크로 함수


#define SQUARE(X) X*X


위의 문장은 아래와 같이 해석된다.



위 매크로를 접한 선행처리기는 SQUARE(X)와 동일한 패턴을 만나면 무조껀 X*X로 치환해버린다.


SQUARE(123);     =>     123 * 123;

SQUARE(NUM);   =>    NUM * NUM;


이렇게 선행처리기에 의해서 변환되는 과정 자체를 가리켜 ' 매크로 확장 ' 이라고 한다.


ex)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#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 3.5 : %g \n",SQUARE(3.5));
    
    // 비정상출력
    printf("Square of 3+2 : %d \n",SQUARE(3+2));
    
    return 0;
}

cs

출력결과

Square of num : 400 

Square of -5 : 25 

Square of 3.5 : 12.25 

Square of 3+2 : 11 


출력이 잘 되다가 14행을 보면 25를 기대했지만, 11이 나와버렸다. 이는 3 + 2 * 3 + 2 를 해버리기 때문에 11이 나온 것이다.


SQUARE((3+2));    ==> (3+2) * (3+2) = 25;


이렇게 바꾸면 정상출력이 된다. 하지만 이는 프로그래머에게 주의를 요하는 형태이기 때문에 안정적이지 못하다.


해결책은 매크로의 몸체에 괄호를 마구 치면 된다.


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


이렇게 하면


(3+2) * (3+2) 로 치환이 된다. 하지만 다음 문장을 보자.


int num = 120 / SQUARE(2);


위의 문장은 int num = 120 / (2) * (2) 가 되어 나눗셈을 먼저 하고 다시 2를 곱하여 120이 나오게 된다.


이제 모든 문제를 해결하기 위해서는 다음과 같이 매크로 함수를 정의해야 한다.


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


X 같은 치환대상 하나하나에 괄호를 하고, 반드시 전체를 괄호로 한번 더 묶어줘야 한다.


매크로 정의 시 먼저 정의된 매크로도 사용이 가능하다.


ex)

1
2
3
4
5
6
7
8
9
10
11
12
#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("반지름 %g인 원의 넓이 : %g \n", rad, CIRCLE_AREA(rad));
 
    return 0;
}

cs

출력결과

반지름 2.1 원의 넓이 : 13.8474


매크로 함수의 장점


1. 일반함수에 비해 실행속도가 빠르다.

- 일반함수를 호출하면, 호출된 함수가 스택 메모리에 할당이 되고, 실행위치의 이동과 매개변수로의 인자 전달 후, return문에 의한 값의 반환이 이루어진다. 이 모든 과정들이 단순치환하는 매크로보다 절대로 빠를 수 없을 것이다.


2. 자료형에 따라서 별도로 함수를 정의하지 않아도 된다.

- 위의 예제어소 보이듯이, 전달인자의 자료형에 상관없이 제대로 치환이 된다. 이는 단순치환이 되기 때문이다.


매크로 함수의 단점


1. 정의하기가 까다롭다.

- if~else문이나 for문같은 실행의 흐름을 컨트롤 하는 문장을 직접 매크로 함수로 정의하려면 부담스러울 것이다.


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

- 만약 if문을 조건 연산자를 사용해 #define DIFF(X, Y) ( (x) > (y) ? (x) : (y) ) 이렇게 정의 했다고 하자. 이는 치환을 하고 나서 컴파일 시 에러 메세지가 뜬다. x와 y가 존재하지 않기 때문에!

정의된 매크로는 대문자 X, Y가 사용되었는데, 매크로 몸체 부분은 소문자 x, y가 사용되었다. 이처럼 매크로를 잘못 정의할 경우, 에러 메세지는 선행처리 이전의 소스파일을 기준으로 출력되지 않고, 선행처리 이후의 소스파일을 기준으로 출력이 된다.


 작은 크기의 함수, 호출 빈도수가 높은 함수같은 경우에만 매크로 함수로 정의 하도록 하자.


조건부 컴파일


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


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


ex)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<stdio.h>
#define ADD 1
#define MIN 0
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;
}
 

cs

출력결과

개의 정수 입력 : 3 5

3 + 5 = 8 


10 ~ 12행 : #if문 뒤에는 반드시 #endif문이 등장해야 하고, 이 두 지시자 사이에 존재하는 코드는 조건에 따라서 삽입 및 삭제가 된다. 즉, 10행의 if문이 참이라면 이어서 등장하는 #endif 사이에 있는 코드가 삽입이 되고, 거짓이면 삭제가 된다.


14 ~ 16행 : if MIN이 참이면 실행이 되는데, 거짓이므로 삭제가 됨.


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


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


ex)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<stdio.h>
//#define ADD 1
#define MIN 0
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;
}
 

cs

출력결과

개의 정수 입력 : 5 3

5 - 3 = 2 


10 ~ 13행 : #ifdef문은 ADD의 값에 상관없이 정의만 되어 있다면 #endif 사이에 존재하는 코드문이 삽입이 된다. 하지만 주석처리가 되어있으므로 이는 삭제

14 ~ 16행 : MIN이라는 매크로가 정의되어 있으므로 삽입


정의되어 있는 매크로의 값은 중요하지 않기 때문에 뒤에 값은 넣어주지 않아도 된다.


#define ADD

#define MIN


이렇게 매크로가 정의가 되면 ADD와 MIN은 선행처리 과정에서 공백으로 대체가 된다.


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


이 문장은 #ifdef의 반대의 성격을 지니고 있다. 중간에 있는 n 이 not을 의미한다.


else : #if, #ifdef, #ifndef에 해당


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


ex)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
#define NUM 5
 
int main()
{
#if NUM == 5
    printf("매크로 상수 NUM은 5이다.\n");
#else
    printf("매크로 상수 NUM은 5가 아니다.\n");
#endif
 
    return 0;
}
 

cs

출력결과

매크로 상수 NUM 5이다.


#elif : #if에만 해당


if문의 else if와 같다.


ex)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<stdio.h>
#define NUM 3
 
int main()
{
#if NUM == 5
    printf("매크로 상수 NUM은 5이다.\n");
#elif NUM == 6
    printf("매크로 상수 NUM은 5이다.\n");
#elif NUM == 7
    printf("매크로 상수 NUM은 7이다.\n");
#else
    printf("매크로 상수 NUM은 모르겠다.\n");
#endif
 
    return 0;
}
 

cs

출력결과

매크로 상수 NUM 모르겠다.


매개변수의 문자열화


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


그리고 STRING_JOB(이동춘, 나무꾼)이라고 하면


"이동춘의 직업은 나무꾼 입니다." 


라고 생각하겠지만 문자열 내에서는 매크로의 매개변수 치환이 발생하지 않기 때문에 불가능하다.


#연산자를 쓰면 가능하다.


define STR(ABC) #ABC    // 매개변수 ABC에 전달되는 인자를 문자열 "ABC"로 치환하라"


즉 #연산자는 치환의 결과를 문자열로 구성한다.


또 한, 


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


위의 두 문장은 동일하다.


ex)

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
#define STRING_JOB(A, B) #A "의 직업은 " #B " 입니다."
 
int main()
{
    printf("%s \n", STRING_JOB(이몽룡, 저격수));
    printf("%s \n", STRING_JOB(성춘향, 부사수));
 
    return 0;
}
 

cs

출력결과

이몽룡의 직업은 저격수 입니다. 

성춘향의 직업은 부사수 입니다. 


매개변수의 결합


학번을 입력할 경우


STNUM(2012, 05,010 )를 입력하면 다음과 같이 치환이 되어야한다. // 201205010


그럼 매크로의 정의는 과연 어떻게 해야할까 ?


1. #define STNUM(Y, S, P) YSP


=> 이렇게 해버리면 Y, S, P의 조합이 아닌, 하나의 YSP로 인식이 된다. 


2. #define STNUM(Y, S, P) Y S P


=> 이럴 경우, 변환과정에서 공백도 그대로 반영이 된다. // 2012 05 010


해결책은 ## 연산자의 사용이다.


## 연산자는 매크로 함수의 전달인자를 다른 대상과 단순하게 이어줄 때 사용한다.


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


ex)

1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>
#define STNUM(Y, S, P) Y ##S ##P
 
 
int main()
{
    printf("학번:%d \n",STNUM(201205010));
 
    return 0;
}
 
 

cs

출력결과

학번:201205010