본문 바로가기
Programming/C++

매크로와 전처리기

by OKOK 2017. 7. 27.

c언어의 모든 문법은 매크로에서 귀결됩니다. c 언어에서 매크로 차지하는 위치는 비교적 높습니다. 이번 포스팅에서 매크로가 무엇인지를 이해하고, 매크로의 장점과 단점, 더불어 좋은 프로그래밍 스타일까지도 함께 보도록 합니다. 


전처리기에 의한 매크로 처리

소스 코드는 컴파일이라는 과정과 링크라는 과정을 거쳐서 실행 파일이 생성됩니다. 여기서 컴파일이라는 과정을 좀 더 구체적으로 세분화한다면 전처리 단계와 컴파일 단계로 나눌 수 있습니다. 전처리라는 과정은 전처리기에 의해서 처리되고, 컴파일은 컴파일러에 의해서 처리되고, 링크는 링커에 의해서 처리됩니다. #으로 시작하는 문장의 처리과정을 가리켜 전처리라 합니다. 전처리 작업은 컴파일러에 의해 처리되는 컴파일보다 훨씬 단순한 구조로 이뤄집니다. 


#define 으로 시작하는 전처리기 지시자

# 문자로 시작하는 문장을 가리켜 전처리기 지시자라고 합니다. 컴파일러가 아니라 전처리기에게 적절한 처리를 욕할 때 사용됩니다. 이는 단순 치환 작업을 요청할 떄 사용되는 지시자 입니다. #define PI 3.1415 의 예제를 통해서 보도록 하겠습니다. #define 전처리기 지시자는 전처리기에게 치환 작업의 요청을 선언하는 것입니다. 전처리기 아저시에게 부탁합니다. 다음에 나오는 것들을 치환하는데 사용해 주세요. 치환을 하려면 치환할 대상과 치환될 내용이 필요합니다. 두 번째 영역에 등장하는 것을 가리켜 '매크로'라 하며, 이 영역에 선언된 문장이 치환의 대상이 됩니다. 두 번째 영역에 등장하는 것을 가리켜 매크로라 하며, 이 영역에 선언된 문장이 치환의 대상이 됩니다. 그렇다면 치환할 내용은 무엇인가요? 다음에 등장하는 대체 리스트입니다. 전처리기에게 부탁합니다. 소스 코드에 존재하는 매크로 PI를 3.1415로 치환해 주세요. 매크로 선언은 문장의 끝을 의미하는 세미콜론을 붙여주지 않습니다. 


#include <stdio.h>

#define PI 3.1415


int main(void)

{

double area;

double radius;


fputs("radius: ", stdout);

scanf_s("%lf", &radius);


area = radius*radius*PI;

printf("width is %f\n", area);


return 0;


전처리기 지시자를 이용해서 매크로 PI를 선언하고 있음을 볼 수 있습니다. 따라서 소스코드에 존재하는 PI는 3.1415로 치환될 것입니다. 여기서 PI는 마치 상수처럼 사용되고 있다는 것입니다. 따라서 이와 같은 선언을 두고 매크로 상수라고 합니다. 이러한 매크로 상수를 이용하여 배열을 선언하는 것도 가능합니다.


#include <stdio.h>


#define NUM 10


int main(void)

{

int arr[NUM] = { 1,2,3 };

int i;


for (i = 0; i < NUM; i++)

printf("%d", arr[i]);


return 0;



#include <stdio.h>


#define string "C++ Compatible C"

#define cal (3*4)+(12/4)


#define ONE 1

#define TWO ONE+ONE

#define THREE TWO + ONE


int main(void)

{

printf("string : %s \n", string);

printf("cal : %d\n", cal);

printf("ONE=%d, TWO=%d, THREE=%d\n", ONE, TWO, THREE);


return 0;


매크로 선언 시 다양한 문장이 올 수 있다는 것을 알 수 있습니다. 이미 선언된 매크로를 다른 매크로 선언에서 사용할 수 있습니다. 대체 리스트 영역에는 공백도 존재할 수 있습니다. 문자열 내에 존재하는 ONE, TWO, THREE 도 매크로에 의해서 치환이 이루어지는 것인가? 대답은 아니요 입니다. 매크로에 의한 치환은 문자열 내에서는 이루어 지지 않습니다. 



매크로를 이용한 함수의 구현

매크로를 이요하면 함수 같은 매크로를 만들 수 있습니다. 함수 같은 매크로란? 매크로는 매크로인데 함수의 기능을 하는 매크로란 뜻입니다. 함수처럼 인자를 전달받을 수도 있습니다. 함수의 선언이 아니라, 함수 같은 매크로 선언입니다. 일반적으로 매크로 함수라고 표현합니다. #define SQUARE(x) x*x 전처리기 지시자, 매크로, 대체 리스트


#define SQUARE(x) x*x


int main(void)

{

int a;

float d;


printf("Input a number(int) : ");

scanf_s("%d", &a);


printf("Square of %d : %d", a, SQUARE(a));


printf("Inputer a number(floast) : ");

scanf_s("%f", &d);


printf("Square of %f : %f\n", d, SQUARE(d));


return 0;


매크로 함수를 선언하고 있습니다. 매크로 함수를 호출하고 있습니다. 즉 전처리기에 의해서 13과 17의 매크로 함수 호출은 각각 a*a, d*d로 치환될 것입니다. 중요한 것은 바뀌는 시점인데, 컴파일러에 의해서 컴파일 되기 이전에 전처리기에 의해 바뀐다는 것을 명심하세요. 전처리기 인식 코드, 컴파일러 인식 코드로 따로 존재합니다. 매크로 함수는 자료형에 독립적입니다. 매크로 함수는 전처리기에 의한 단순한 치환 방식으로 구현되므로, 전달 인자의 자료형을 명시할 필요가 없고, 또 어떠한 자료형 변수를 인자로 전달해도 잘 동작합니다.


실행 속도가 향상됩니다. 함수의 호출 때문에 발생하는 성능의 저하가 일어나지 않습니다. 단, 함수 호출 문장이 전부 매크로 함수 몸체 부분으로 완전히 치환되어 버리기 때문에 코드의 크기는 커질 수 밖에 없습니다. 따라서 모든 함수를 매크로로 정의하는 것은 좋지 않습니다. 더불어 매크로 함수를 정의하는 것이 일반 함수를 정의하는 것보다 어렵고, 오류 발생의 소지가 높습니다. 그렇다면 어떠한 함수를 매크로로 정의해야 손해 없이 속도 향상의 이점을 얻을 수 있을까요? 함수의 크기가 작아야 합니다. 한 두줄 정도의 함수는 매크로로 정의하기도 쉽고, 또 치환되더라도 컴파일할 소스 코드의 크기가 크게 증가하지 않습니다. 


#include <stdio.h>


#define SQUARE(x) x*x

int square(int x);


int main(void)

{

int a;


printf("Input a number(int) : ");

scanf_s("%d", &a);


printf("Square of %d : %d \n", a, SQUARE(a + 3));

printf("Square of %d : %d \n", a, square(a + 3));


return 0;

}


int square(int x)

{

return x*x;


제곱근을 구하는 함수를 두 가지 형태로 제공하고 있습니다. 매크로 함수로 정희하고 있고, 일반 함수로 선언하고 있습니다. 매크로 함수와 일반 함수를 각각 호출하며, 그 결과를 출력하고 있습니다. 결과를 보면 매크로 호출 결과는 우리의 예상이 완전히 빗나갔다는 것을 알 수 있습니다. 매크로로 구현되어 있는 SQUARE는 a+3*a+3 이 되어버립니다. 우선순위의 문제이므로 호출하는 경우나 선언할 경우 괄호를 감싸는 방법이 있습니다. 두 결과 모두 원하던 결과를 얻을 수 있습니다. 일반적으로 함수를 매크로로 정의하는 경우 괄호를 많이 사용하게 됩니다. 


#을 이용한 매크로 함수 전달 인자의 문자열화

매크로에 의한 치환은 문자열 내에서 이루어지지 않는다. 라는 것을 확인하였습니다. C 언어는 문자열 상수를 이어서 선언 할 수 있습니다. 


#include <stdio.h>


int main()

{

char *str = "AAA" "BBB";


puts(str);

puts("EEE" "FFF");


return 0;


두 개의 문자열 상수를 이어서 선언하고 있습니다. #을 통한 매크로 전달 인자의 문자열화

#include <stdio.h>


#define FUN1(x) x

#define FUN2(x) #x


int main()

{

printf("FUN1 : %d \n",FUN1(1000));

printf("FUN2 : %s \n", FUN2(1234 ABCD));


return 0;


FUN1을 호출하고 있따. 전달 시 인자로 정수 1000ㅇㄹ 전달하였기 때문에 그대로 1000이 리턴됩니다. 따라서 출력 형식을 %d로 지정해 주고 있습니다. 두 번째 매크로 함수 FUN2를 호출하고 있습니다. 출력 형식을 %s로 지정해 주고 있습니다.


#include <stdio.h>


// #define ADD(x,y) printf(#x "+" #y "=%d \n", x+y)

#define ADD(x,y) printf("x+y = %d", x+y)


int main()

{

ADD(3, 4);

return 0;

}



치환단계를 살펴보도록 합니다. ""로 두면 문자열로 인식하기 때문에 x + y = 7 이라는 출력 결과과 나옵니다. 반면에 #을 붙이기 되면 #x 는 #3으로 치환되고 #은 문자열의 의미하기 때문에 "3" 과 같은 의미가 됩니다. 그리고 " " " " 처럼 연속된 문자열은 하나의 문자열로 인식 됩니다.


## 을 이용한 토큰의 결합

C 언어에서의 토큰이란 컴파일러가 인식하는 의미를 지니는 문자나 문자열의 최소 단위를 뜻합니다. int val = a + b; 문장을 최소의 단위로 나눠 보면, int, val, =, a, +, b, ; 이렇게 총 7개가 됩니다. 따라서 총 7개의 토큰으로 구성되어 있는 문장이라 할 수 있습니다. 따라서 변수나 함수의 이름부터 시작해서 연산자, 키워드 및 상수들도 모두 토큰의 기본 단위가 됩니다. 토큰에 대해서는 이 정도만 이야기 하고, 다시 본론으로 돌아와서 ##이 토큰을 결합한다는 것이 무슨 뜻인지 이야기 해보도록 하겠습니다. 


#include <stdio.h>

#define CONCAT(a,b) a##b


int main(void)

{

int arr[2] = { 100, 200 };


printf("%d\n", CONCAT(2, 4));

printf("%s\n", CONCAT("Good", "Morning?"));

printf("%d %d\n", CONCAT(arr, [0]), CONCAT(arr, [1]));


return 0;


변수나 함수의 이름을 동적으로 작성하기 위한 용도로 사용된다고 알고 있으면 딥니다. 앞으로 윈도우즈 관련 프로그래밍(win32 API, MFC)를 하다 보면, 그 사용 용도를 느낄 수 있는 기회가 있을 것입니다. 



이미 정의 되어 있는 표준 매크로

C 언어에는 정의하지 않아도 기본적으로 정의되어 있는 매크로가 존재합니다. 이를 가리켜 표준 매크로라 하는데, 이는 프로그래머의 편의를 위해 제공이 되는 것입니다. 

#include <stdio.h>


int main(void)

{

printf("current line: %d\n", __LINE__);

printf("file name: %s\n", __FILE__);

printf("compile time : %s\n", __TIME__);

printf("compile date : %s\n", __DATE__);


return 0;


__FILE__ 현재 소스 코드의 파일명으 나타내는 문자열입니다. __TIME__ 컴파일 시각을 나타내는 문자열 입니다. __DATE__ 컴파일 날짜를 나타내는 문자열 입니다. __LINE__ 처리중인 소스파일의 행 번호를 나타내는 문자열입니다.