C 선행처리기와 매크로

프로그래밍/C 2019.05.26 댓글 Plorence

선행처리기

선행처리란 컴파일 이전의 처리를 의미합니다.

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

전처리기라고 부르기도 합니다.

#define, enum이 이에 해당합니다.

 

컴파일의 순서는 소스파일 >선행처리기 > 선행처리 거친 소스파일 > 컴파일러 > 오브젝트 파일 > 링커 > 실행파일(exe) 순입니다.

#define을 예시로 들었을때

 

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

의미는 "stdio.h 파일의 내용을 이곳에 가져다 놓으세요"

선행처리 명령문,#define

형식이 두 가지가 있습니다.

 

#define Object-like macro 형식

#define one 1

위 코드의 의미는 "one을 1이라고 치환하라!"라는 의미입니다.

선행처리는 컴파일 이전에 하기 때문에, 아무런 문제도 없습니다.

 

#define을 지시자, one을 매크로, 1을 매크로 몸체(또는 대체 리스트)라고 합니다.

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

 

결국 one이라는 이름의 매크로는 그 자체로 상수 1이 된 셈입니다.

이와 같은 매크로를 '오브젝트와 유사한 매크로'라고 하며 간단히 '매크로 상수'라고도 합니다.

오브젝트와 유사 매크로라는 건 one은 그 자체로 1이라는 상수를 의미하기 때문에 오브젝트 유사 매크로라고 합니다.

오브젝트(Object)란 완전한 의미를 갖는 대상이나 사물을 의미합니다.

one은 1이라는 의미 말고는 다른 의미는 없습니다. 그러니까 완전한 의미를 갖는 것입니다.

 

#define Function-like macro 형식

매크로는 매개변수가 존재하는 형식으로도 가능합니다.

동작 방식이 함수랑 유사하여 '함수와 유사한 매크로' 간단히 '매크로 함수'라고 합니다.

#define SQUARE(X) X*X이라는 선행처리 명령문이 선언되었을 때 선행처리기 전과 로 나뉘어서 예시를 만들어 봤습니다.

Function-like macro 형식 동작 예

간단한 예시입니다. 앞서 말한 듯이 매크로 상수와 비슷합니다.

다만 차이점은 매개변수가 있고, 첫 번째 전달 인자가 정수 또는 실수여야만 합니다.

(문자열을 곱할 수는 없잖아요. 된다 해도 의도치 않은 값이 나옵니다)

 

#define의 잘못된 매크로 정의

앞에서 예시로 든

#define SQUARE(X) X*X

을 가지고 한 번 더 예시를 들겠습니다.

 

SQUARE(10+20)이라고 썼습니다. 값을 예측해봅시다. 답은 900?

아닙니다 틀렸습니다.

 

10+20을 먼저 연산을 하고 그 연산 결과를 전달 인자로 넘기는 건 컴파일러가 하는 역할이지 선행처리기가 하는 역할이 아닙니다.

선행처리기가 #define을 처리하는 건 정말 단순 치환입니다.

연산은 안 해요.*(곱하기 연산자)도 컴파일러가 해야 할 역할이지 선행처리기가 하는 역할이 아닙니다.

SQUARE(10+20)

을 호출했을 때 연산은

10+20 * 10+20

이 됩니다. 우선순위는 * / 를 먼저 하고 그다음 + -을 하는 게 맞습니다.

그래서 20 * 10의 연산 결과, 200이 나오고 결국 10 + 200 + 20 = 330이 나옵니다.

 

그럼 어떻게 해결하느냐?

연산의 우선순위를 위해 괄호를 줍니다.

SQUARE((10+20))

이면 수식은 (10+20) * (10+20) = 900이 나옵니다.

근데 SQUARE을 괄호를 쳐줘야 우리가 예측하는 연산 결과가 나옵니다. 괄호를 사용하라고 알리는 게 아니라면 의도치 않은 연산 결과가 나올 겁니다.

결국 안정적이지 못하다는 겁니다.

 

그럼 어떻게 하느냐?

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

로 선언해줍시다.

SQUARE(2+3)이라고 호출됐을 때, 선행처리기는 단순 치환만 해주므로 수식은

(2+3) * (2+3)

으로 됩니다.

우리가 생각하는 연산 결과랑 같습니다.

결국 선언할 때 괄호를 마구마구 쳐줍시다.

 

그런데도 문제가 한 가지 더 생겼습니다.

int num = 120 / SQUARE(2);

을 했을 때 수식은 120 / (2) * (2)입니다.

개발자 입장에서는 2*2를 하고 120 / 4 해서 num에 30이 들어가길 원하는 상태입니다.

하지만, 이렇게 했을 시에 똑같은 우선순위일 때 진행방향은 왼쪽에서 오른쪽입니다.

결국 (120 / 2) * 2라는 것입니다. (원래는 괄호 안쳐도 되는데 좀 더 이해하기 쉽게 하기 위해 쳐줬습니다.)

 

그러면 무조건 SQUARE의 치환 값부터 연산을 해주려면 어떻게 해야 하는가?

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

해주면 됩니다.

우선 매크로 몸체 전체를 괄호로 묶어준 뒤 X와 같은 전달 인자 하나하나에 한 번 더 괄호로 묶어줘야 합니다.

그러면 매크로 몸체가 ( (X) * (X) ) 일 때 연산 결과를 봅시다.

int num = 120 / SQUARE(2); --> 120 / ( (2) * (2) )

가 됩니다.

그래서 가장 먼저 2*2부터 하고 120 / (4)가 최종 수식이 되고 연산 결과는 의도한 대로 30입니다.

 

잘못된 매크로를 선언하지 않기 위해 하는 약속

  • 매크로 몸체 전체에 괄호로 묶어줍니다.

  • 전달 인자 하나하나 괄호로 묶어줍니다. EX) (X)

매크로는 원칙적으로 한 줄에 정의하는 게 원칙입니다.

두 줄 이상 정의하려면 \(백슬래쉬 (엔터 위에 원화 키 누르면 됨))를 사용하면 됩니다.

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

그리고 매크로 정의 시, 먼저 정의된 매크로도 사용 가능합니다.

#include <stdio.h>
/*
목표: 먼저 A*A를 하고 그 연산결과로 A+A해주는 매크로를 만들어야한다.
*/
#define SQUARE(X) ( (X) * (X) )
#define ADD(A) ( ( SQUARE(A) )+( SQUARE(A) ) ) //먼저 A*A를 한다음, 연산결과의 A+A을 한다.
int main(void) {
    int num = 10;
    printf("%d", ADD(10)); //200
}

C는 절차 지향 언어라는 걸 기억해야 합니다.

위에서 아래로 코드를 실행하기 때문에, 먼저 정의된 매크로는 다음 정의할 때 사용이 가능합니다.

우선 ADD(10) 했을 때 수식을 확인해봅시다. 상당히 짜증날 정도로 괄호가 많습니다.

( ( ( (A) * (A) ) ) + ( ( (A) * (A) ) ) ) 복잡 한건 아닌데 괄호가 너무 많아 보기 힘듭니다.

파란색, 초록색, 주황색, 빨간색 순으로 연산이 진행됩니다.

 

그러면 이러한 것들을 왜 쓰는지 알아야 합니다.

매크로 함수의 장점

  • 일반 함수에 비해 속도가 빠르다

  • 자료형에 따라서 별도로 선언을 안 해도 된다.

일반 함수에 비해 속도가 빠른 이유는 호출된 함수를 위한 스택 영역에 변수 할당, 실행 위치의 이동과 매개변수로의 인자 전달, return 문에 의한 값 반환 등등 호출해야 할 것이 많습니다.

비해 매크로는 단순 치환이므로 호출할 건 없고 연산만 진행하면 됩니다.

매크로 함수의 단점

  • 정의하기가 복잡하다 (까다롭다 & 어렵다)

  • 디버깅하기 쉽지 않다 (문제 발견의 어려움?)

예를 들어 두 값의 차에 대한 절댓값을 매크로로 정의하려면 상당히 복잡해지고 길어집니다.

 

매크로를 정의할 때도 대소문자 구분이 필요함

#define ADD(X,Y) x+y

라는 매크로를 정의했다고 가정해봅시다. 컴파일을 하면 에러가 납니다.

x, y가 선언된 적 없는 식별자라고 뜹니다. 분명히 앞에서 X, Y라고 써놨는데도 불구하고요.

그 이유는 변수의 대소문자 구분 (int X, int x는 각각 다른 변수임)을 하는 것처럼 매크로도 마찬가지입니다.

대소문자 구분을 하기 때문에 전달 인자로 x, y를 안 줬습니다.

사용하지 않으면 에러 출력은 선행 처리 이후의 문제이기 때문에 문제를 잡는 게 힘들지도 모릅니다.

매크로 함수는 언제 사용해야 하는가?

  • 작은 크기의 함수 (간단)

  • 호출의 빈도수가 높은 함수 (더하는 함수, 곱하는 함수 등등 많이 사용하는 함수)

한번 알아두면 개발자 입장에서는 편리합니다.

매크로를 매크로 몸체로 치환하니 개발자가 정의한 대로 의미를 가지기 때문입니다.

C++에서는 #define으로 매크로 함수를 정의하는 것보다는 인라인 함수로 정의하는 걸 선호합니다.

 

댓글