C\\C++

[C/C++] 선행처리기(전처리)와 매크로

아몬드통 2022. 12. 26. 12:02

c(c++)의 소스 파일을 실행파일을 만들때 기본적인 과정은 아래와 같다.

  • 1.소스파일 -> 2.컴파일 -> 3.링크 - > 4.실행파일
  • 다만 컴파일에는 전처리라는 과정이 합쳐져 있다. 이 전처리를 풀어서 다시 나열해보면
  • 1.소스파일 -> 2.전처리 -> 3.컴파일 -> 4.링크 - > 5.실행파일

 

그럼 전처리가 하는 역할은 무엇일까?

  • c언어를 한번이라도 해봤다면 이미 전처리 문법을 사용했다고 볼 수 있다.
  • #include <stdio.h> 바로 이 문장이 전처리지시 문법이기 때문이다.
  • #include 가 전처리를 전처리기(선행처리기) 에게 알려주는 문법이기 때문이다.

 

전처리와 컴파일은 어떤 것이 다른가?

  • 전처리는 단어대로 어떤 일의 "전"에 하는 처리이고 그 어떤것은 "컴파일"이다.
  • 이때 100%는 아니지만 "A를 B로 바꿔라" 라는 치환의 의미를 가진 전처리들이 많다.
  • 우리는 작성한 .c/c++ 파일을 컴파일하면 나오는 파일은 보통 오브젝트파일이라고 부른다.
  • 하지만 전처리 후 파일은 처음과 마찬가지로 소스파일이다. 다만 전처리를 마친 소스파일일 뿐이다. 

 

전처리의 대표적인 방식은 Object-like macro, Function-like macro 가 있다.

Object-like macro(매크로 상수)

  • Object란 "그 자체로 완전한 의미를 갖는 대상이나 사물" 이라는 의미를 가진다.
  • 다시 말해서 위에서 예시로 들었던 치환의 의미 "A를 B로 바꿔라" 이 말을 그대로 따른다고 보면 된다.
  • 정의한 매크로는 보통 아래의 PI처럼 그 자체로 의미가 있는 상수이다.
 #define PI 3.14

 

Function-like macro(매크로 함수)

  • 동작방식이 마치 함수와 유사한 매크로라서 Function-like macro이다.
  • 아래 코드 처럼 괄호를 꼭 사용해야하고 의미는 SQUARE(X)에서X를 X*X의 위치로 바꿔라 라는 의미다.
#define SQUARE(X) X*X

SQUARE(123) //123*123
  • 위의 예제처럼 SQUARE(123) 은 전처리 후 123*123 으로 바뀐다. 이런 변환 과정을 매크로 확장이라고 한다.

 

잘못된 매크로 함수 정의

#define SQUARE(X) X*X

SQUARE(3+2) //25? 가 아니고 11이 출력된다.
  • 위처럼 SQUARE(3+2) 를 하면 3과2를 더해서 5*5=25 가 나와야할 것 같지만 매크로는 치환을 할 뿐이므로 5*5가 아니라 3+2*3+2 가 되어서 실제로는 11의 결과가 나온다.
  • 이런 문제를 예방하려면 매크로 몸체(X*X)에 괄호를 마구마구 쳐주면 된다.
//#define SQUARE(X) X*X
#define SQUARE(X) (X)*(X)

SQUARE(3+2) //이제는 제대로 25가 출력된다.
  • 위의 예제처럼 괄호를 추가해주면 (3+2)*(3+2) 이 되기 때문에 원하는 결과 25를 얻을 수 있다.

 

매크로를 여려줄에 걸쳐서 정의

#define SQUARE(X) \
(X)*(X) \
+ (X)
  • 위처럼 \ 를 사용하면 여러줄에 걸쳐서 매크로를 정의할 수 있다.

 

매크로에서 매크로 사용하기

  • C와 C++은 기본적으로는 절차지향 언어기 때문에 아래 처럼 먼저 선언된 매크로를 다음 매크로에서 사용이 가능하다.
#define ONE 1
#define TWO 2
#define SQUARE(X) ((X)*(ONE)) + ((X)*(TWO))

 

매크로 함수의 장점

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

 

1. 일반 함수보다 실행이 빠른 이유

  • 일반함수를 호출하면 아래와 같은 명령이 동반된다.
    1. 함수 호출을 위한 스택 메모리 할당
    2. 해당 함수의 실행위치로 이동 + 매개변수 인자 전달
    3. return문에 의한 값 반환
  • 이렇게 일반함수는 호출할때 3가지의 명령이 동반 되기 때문에 함수를 반복호출한다면 실행속도의 저하로 이어진다.(물론 요즘 컴퓨터는 이 정도의 속도저하를 신경쓸 필요는 없기는 하다..)
  • 매크로함수는 몸체 부분이 함수의 호출문장을 대신하기 때문에 위의 일반 함수호출의 123 실행을 하지 않는다.
  • 글의 처음 부분에서 얘기했던 전처리기가 매크로부분을 몸체로 치환하기 때문에 컴파일시에는 함수를 호출하는 것이 아니라 코드를 읽어서 실행만 하면 되기 때문에 일반함수의 호출의 단계가 필요 없다.

 

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

  • 자료형에 따라서 따로 정의할 필요가 없다는 편리하다.
  • (내 생각)다만 다시 생각해보면 어떤 자료형이 들어와서 어떤 오류를 출력할지 모른다는 문제도 존재하므로 2번째 장점은 사실 장점과 단점이 모두 존재한다고 생각한다.(테스트 해보니 컴파일러에서 잡기 전에 이미 ide가 잡아준다.)

 

매크로 함수의 단점

1. 정의하기가 정말로 힘들다.

  • 간단한 두 수의 곱셈,  두 수의 덧셈 정도의 매크로는 정의하기 좋지만 조금만 조건이 추가되어도 그에 따른 괄호 추가, 길어지는 매크로 몸체 부분, 논리적으로 맞는지의 판단 등 차라리 일반 함수로 만드는 편이 좋다는 생각이 들때도 있었다.

 

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

1 #include <stdio.h>
2 #define DIFF_ABS(X, Y) ((X)>(Y) ? (x)-(y) : (y)-(y))
3
4 #int main(int argc, char* argv[]){
5	printf("두 값의 차: %d \n", DIFF_ABS(5, 7));
6	printf("두 값의 차: %g \n", DIFF_ABS(1.8, -1.4));
7	return 0;
8 }
  • 위 코드는 를 실행시키면 5행에 문제가 있다고 나온다. 5행은 크게 문제가 없는데 무엇이 문제일까?
  • 2행의 #define으로 선언한 XY의 대소문자를 틀리게 한 부분이 문제이다.
  • 이렇게 매크로를 잘못 정의하면 에러메세지는 전처리 이전의 소스파일 기준이 아니라, 전처리 이후의 소스파일 기준으로 출력이 된다. 따라서 일반적인 에러메세지보다 이해하기 힘들다는 단점이 있다.

 

언제 사용해야할까?

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

위의 2조건 딱 맞아 떨어질때는 꼭 사용해봐야겠다.

간단한 크기의 함수이면서, 호출 빈도수가 굉장히 높을때!