C 언어 | 전처리(preprocess) | 매크로 상수 - #define, #undef
##define
지시문을 사용하여 텍스트에 이름을 지정하고 코드에 상수로 배포할 수 있다. 자주 등장하는 의미있는 값이나 연산 등을 자동화하는데 응용할 수 있다.
토큰 열의 전개
이전에 const 형 한정자를 사용하여 변경 불가능한 변수를 만드는 방법을 설명했다. 이것은 개발자에 프로그램 안에서 항상 특정 상수를 나타내는 식별자를 주었다. 그러나 변수는 만드는 것만으로도 메모리 손실이 발생하며, 추가적으로 포인터에 의한 간접 참조되어 CPU의 주소 계산이 발생하게 된다. 많은 C 언어 프로그래머는 이러한 낭비를 없애고, 더 빠르고 똑똑한 프로그램을 만들고 싶다고 생각하기에 이는 큰 문제일 것이다.
그래서 가능한 것이 전처리이다. 전처리는 컴파일 전에 소스를 성형하는 능력을 가지고 있기 때문에, 전처리로 할 수 있는 것은 전처리 수행함으로써, 처리의 효율화를 도모할 수 있다.
전처리 명령은 토큰 열을 전개하는 #define 지시문이 존재한다. #define 지시문에 정의된 식별자를 매크로라고 부르고, 많은 C 언어 프로그래머가 이 기능을 많이 사용하고 있다.
#define 지시문
#define 식별자 토큰열
토큰 열에 지정하는 내용은 어떤 것이라도 상관없다. 프로그램 중에 #define
지시문으로 만든 식별자를 지정하면, 그 위치에 토큰 열이 그대로 전개된다. 토큰 열이 전개될 때는 앞뒤의 공백이 잘린다. #define
지시문으로 만든 토큰 열을 나타내는 식별자는 #define 지시어 다음의 모든 위치에 지정할 수 있다. 극단적으로 다음 프로그램도 문제없다.
#define MAIN int main() { return 0; }
MAIN
이것은 #define
지시문을 사용하여 int main() { return 0; }
라는 토큰 열을 나타내는 MAIN이라는 식별자를 생성한다. 이처럼 #define 지시어는 토큰 열에 이름을 지정할 수 있다. 후에는 프로그램 중에 필요한 위치에 토큰 열의 이름을 지정하면 컴파일시에 전처리 토큰 라인에 옮겨준다. 위에 두줄은 컴파일시에는 int main () {return 0;}
문자열로 대체한다.
이 기능을 잘 이용하면 변수를 사용하지 않고 일관된 정수 처리를 할 수 있다. 실제로 C 언어 프로그래머는 상수를 사용하는 경우 const 형 한정자와 enum을 사용하는 것보다 #define 지시어에 의해서 전처리 할시에 대체를 하도록 하는 것을 많이 이용한다. 함수에 전달하는 논리적 의미가 있는 속성값이나 함수가 반환하는 오류 코드 등은 #define 지시문의 식별자로 구분하는 방법이 일반적이다.
코드1
#include <stdio.h>
#define KITTY "Kitty on your lap"
#define BUFFER 0xFF
int main() {
char str[BUFFER] = KITTY;
printf("%s\n" , str);
return 0;
}
코드1은 리터럴 문자열을 나타내는 KITTY와 0xFF라는 정수 상수를 나타내는 BUF라는 이름을 정의하고 있다. #define 지시문에 정의된 토큰 열 이름은 프로그램 중 어느 곳에서도 지정할 수 있다. 이 이름들은 컴파일 전에 전처리에서 대체되어, C 언어의 구문에는 관여하지 않는다. 물론 확장된 토큰 열이 구문적으로 올바른지는 개발자의 책임이다.
헤더 파일 등은 이러한 상수의 이름을 붙일 수 있는 유효한 수단으로 동시에, 다른 한편으로는 어느 한곳에 상수의 이름을 붙이는 처리해야 할 문제도 발생한다. 전처리기 지시문에는 가시성이라는 개념이 존재하지 않기 때문에, 항상 #define 지시문에서 정한 이름이 공개되어 버린다. const 형 한정자 등이라면 로컬 변수를 사용하여 지역성을 제공됐지만, #define 지시어를 사용하는 경우는 어떻게 해결해야 할까?
이러한 문제는 #undef 지시문을 사용하여 해결할 수 있다. #undef 지시문은 #define 지시문에서 정의 된 이름을 삭제한다. 이에 따라 정의 해제된 이름은 그 이하 줄에서는 사용할 수 없다.
#undef 전처리 명령
#undef 식별자
식별자는 정의 해제를 보장하는 이름을 지정한다. #undef 지시문은 반드시 정의된 이름을 지정할 필요가 없다. 정의되지 않은 이름을 지정해도, 해당 식별자의 정의되지 않은 상태를 보장하는 효력이 있기 때문이다.
코드2
#include <stdio.h>
int main() {
#define PI 3.14159265358979323846
printf("%f\n" , PI); /*OK*/
#undef PI
/*printf("%f\n" , PI); /*error*/
return 0;
}
코드2는 원주율을 나타내는 상수 PI를 정의하고 있다. main()
함수의 시작 부분에서 PI를 printf()에서 사용하고 있지만, 그 후에 #undef 지시문에 의해 정의 해제를 하고 있다. #undef 지시문의 행 이후에 PI라는 이름은 정의되지 않은 상태가 보장되기 때문에, 주석 처리된 printf()
함수를 컴파일하면 오류가 발생하는 것을 확인할 수 있다.
헤더 파일 등으로 어느 한곳에서 사용하는 상수는 헤더 파일의 끝에서 정의 해제하면 이름 충돌을 피할 수 있다.