C 언어 | 전처리(preprocess) | 조건부 컴파일 - defined

조건부 컴파일은 전처리으로 코드를 컴파일의 대상으로 할 것인지를 선택하는 방법이다. 예를 들면 디버깅 할 때 컴파일하려는 코드와 릴리스 시에 컴파일하려는 코드를 전처리 지시문으로 분기할 수 있다.

컴파일을 제어하기

C 언어는 많은 시스템에서 사용되는 국제적인 프로그래밍 언어이며, 전문 프로그래머들 사이에서는 표준어와 같은 존재이다. 그 만큼 많은 시스템에서 사용되는 언어라면, 당연히 프로그래머는 이식성이 높은 코드를 만들려고 노력하는 것이다.

하지만 컴파일시의 상황에 따라 꼭 다시 써야 할 부분이 생긴다. OS 버전과 개발 환경이 다르면 약간의 사양의 차이가 프로그램의 동작에 영향을 미칠 수 있고, 코드를 부분적으로 OS에 특화시키고 싶은 경우도 있을 것이다.

이러한 문제는 소스를 다시 작성하는 것 보다도, 컴파일해야 소스를 분기시키는 컴파일 과정을 제어하는 조건부 컴파일라는 기술을 사용하여 효율적으로 해결할 수 있다. 이는 컴파일 전에 소스를 변경하는 전처리로 할 수 있다.

컴파일 부분적인 통제는 #if 지시문 및 #endif 지시문의 조합으로 구성된다. #if 지시문은 #endif이 대응되어야 한다.

#if 지시문과 #endif 지시문

#if 상수 표현식
    컴파일 코드
#endif

상수 표현식은 정수형 상수 표현식이다. 이 상수 표현식이 거짓이면 대상 코드는 컴파일되지 않는다. 이 기능을 이용하면 디버깅시 임시 코드 등을 작성할 수 있습니다. 릴리스 버전을 컴파일하는 경우는 상수 식을 0으로 하면 된다.

상수 식에는 #define 지시문에 정의된 이름을 사용할 수 있지만, 변수와 sizeof 연산자, 형 캐스팅은 사용할 수 없다. 다른 기본 연산자는 사용할 수 있다.

코드1

#include <stdio.h>
#define DEBUG_MODE 1

int main() {
#if DEBUG_MODE
  printf("Debug mode\n");
#endif
  return 0;
}

코드1에는 main() 함수에서 #if 지시문을 사용하고 있다. 이 프로그램은 상수 DEBUG_MODE가 참이면 printf()를 컴파일하고, 거짓이면 컴파일을 실시하지 않는 것을 나타낸다. DEBUG_MODE은 1이므로 printf() 함수가 컴파일되지만, 이를 0으로 변경하고 다시 컴파일하면 #if에서 #endif까지의 텍스트가 컴파일 대상에서 배제되는 것을 확인할 수 있다.

C 언어 구문인 if-else 마찬가지로 #if 지시문이 거짓인 경우에 컴파일하는 코드를 지정하려면 #else 지시문를 사용한다. #else 지시문은 기본적으로 if-else의 관계와 같아서 이해하기 쉬울 것이다.

#else 지시문

#if 상수 표현식
    참일 , 컴파일 코드
#else
    거짓을 , 컴파일 코드
#endif

이 경우, 상수식이 0이 아니라면 #if 후에 텍스트가, 0이면 #else부터 #endif까지의 텍스트를 컴파일된다. #else 지시문은 #if와 함께 사용되며, 반드시 #endif 지시문 앞에 작성된다. #else 지시문은 #if ~ #endif 사이에 최대 하나만 지정할 수 있다. 덧붙이면, #if 지시문는 중첩될 수 있다.

#if N
  #if M
   ...
 #else
   ...
 #endif
#else
 ...
#endif

이 구문을 사용하여 컴파일할 국가나 개발 환경, 시스템 등에 따라 대상 코드를 선택할 수 있다. 다만, 필요 이상으로 복잡하게 해서는 안된다. 너무 복잡하다면 프로그램에 맡기는 것이 무난하다.

코드2

#include <stdio.h>
#define EN 1
#define  KO 2

#define LANG KO

int main() {
#if LANG == EN
 printf("Kitty on your lap\n");
#else 
 #if LANG == KO
    printf("당신의 무릎 위에 고양이\n");
 #endif
#endif
  return 0;
}

이 프로그램은 국가 코드를 나타내는 EN과 KO 상수를 정의하고 #if 등으로 비교에 이용하기 위한 상수 LANG을 정의하고 있다. 프로그램은 LANG이 EN이면 영어로 KO이면 한국어로 문자열을 표시한다. #else 지시문 후에 추가적인 #if 지시문을 사용하여 중첩 구조로 되어있는 곳에도 주목하자.

코드2와 같이 #if 지시문을 중첩하는 경우는 전처리 지시문의 구문상의 문제로 #else 지시문 후에 지정하는 경우에서도 줄 바꿈을 해야 하고, 추가적으로 #endif 지시문에 대응시켜야 하기에 코드가 복잡해 진다. 그래서 #else 지시문과 #if 지시문을 조합한 #elif 지시문이 준비되어 있다. #elif 지시어는 다음과 같이 사용할 수 있다.

#elif 지시문

#if 상수 식
    컴파일 코드
#elif 상수 식
    컴파일 코드
#elif 상수 식
...
#endif

#elif는 #else와 #if이 일체가 된 효력이 있고, #if를 #else 후에 중첩하는 경우와 동일한 결과를 얻을 수 있기 때문에, 단계적으로 #if 지시문에 의해 상수를 평가하고 싶은 경우에 유효한 수단이다. #elif는 상수 표현식이 참이면 그 이후의 텍스트를 컴파일 대상으로 한다. #else와 마찬가지로 #if ~ #endif 사이에 지정할 수 있고 #else와는 달리 여러개를 지정할 수 있다.

코드3

#include <stdio.h>
#define EN 1
#define KO 2

#define LANG EN

int main() {
#if LANG == EN
 printf("Kitty on your lap\n");
#elif LANG == KO
 printf("당신의 무릎 위에 고양이\n");
#endif
 return 0;
}

코드3는 코드2를 #elif 지시어로 표현한 것이다. 동작은 동일하지만 #if 지시문을 중첩하는 것보다 소스가 짧아지기 때문에 가독성이 향상된다.

이름 정의에 의한 제어

사실 #define으로 정의하는 이름은 반드시 어떠한 토큰 열에 전개되는 것은 아니다. 이름만 정의하여 토큰 열을 생략할 수 있는 것이다. 예를 들어 다음과 같은 정의가 허용된다.

#define DEBUG

상수 DEBUG는 토큰 열을 가지고 있지 않지만 이름은 정의되어 있다. 이와 같은 이름의 정의는 코드에 영향을 미치는 것은 아니지만 전처리 레벨의 처리에는 의미가 있다.

예를 들어, 코드1과 같이 디버그 및 릴리스에서는 다른 소스를 컴파일하고 싶다면, 상수 DEBUG 값을 평가하는 것보다 DEBUG라는 이름이 정의되어 있는지 여부를 평가하는 것이 스마트하다고 생각할 수 있다. 그러면 #define을 주석 처리하거나 #undef를 사용하여 정의 해제하는 것으로 릴리스 버전의 컴파일을 할 수 있다.

이름의 정의를 평가하려면 defined 전처리기 연산자를 사용한다. 이 전처리 연산자는 #if 지시문 또는 #elif 지시문의 상수 표현식으로만 사용할 수 있다.

defined 전처리 연산자

defined (식별자)
defined 식별자

defined 연산자는 정의를 평가하는 식별자를 ()로 묶거나, defined 후에 공백을 두고 지정한다. 지정된 식별자가 #define에 의해 정의되어 있는 경우는 true를, 정의되지 않았다면 false를 반환한다.

코드4

#include <stdio.h>
#define DEBUG

int main() {
#if defined(DEBUG)
  printf("Debug mode\n");
#else
 printf("Release mode\n");
#endif
  return 0;
}

코드4는 DEBUG라는 이름이 정의되어 있으면 디버그 모드용의 코드를, 그렇지 않으면 릴리스 코드를 컴파일하는 프로그램의 예이다. #if 지시문으로 defined 연산자를 사용하여 식별자를 평가하고 있다.

그러나, 일반적으로 defined 연산자가 사용되는 것은 아니다. 대부분 C 언어 프로그래머는 단순하게 작성할 수 있는 #ifdef 지시문 및 #ifndef 지시문을 사용하는 것을 선호하고 있기 때문이다. 이들은 #if defined …와 같은 형태를 생략한 지시문이다.

#ifdef 지시문

#ifdef 식별자

#ifndef 지시문

#ifndef 식별자

이 두개의 지시문은 #if defined (식별자)#if !defined (식별자)와 같은 코드와 같다고 생각할 수 있다. #ifdef 지시문은 식별자가 정의되어 있으면, 그 후에 코드를 컴파일하다. 반대로 #ifndef는 식별자가 정의되어 있지 않으면 다음의 코드를 컴파일한다. 이러한 지시문은 #if 지시문과 마찬가지로 #endif에 대응해야 한다.

코드5

#include <stdio.h>
#define DEBUG

int main() {
#ifdef DEBUG
 printf("Debug mode\n");
#else
 printf("Release mode\n");
#endif
  return 0;
}

코드5는 코드4를 #ifdef 지시문을 사용하여 수정한 것이다. 결과는 동일하지만 #if defined (...) 대신에 #ifdef 지시문을 사용하고 있다. 많은 C 언어 프로그래머는 이와 같은 작성을 선호한다.

컴파일 오류

#if 지시문을 사용하여 상수와 이름을 조사한 결과, 컴파일을 하려면 불충분한 상태라고 하면, 개발자는 어떻게 소스를 작성해야 하나? 충분한 정보가 설정되어 있지 않은 경우의 처리 방법 중 하나는 기본 동작하는 것을 미리 정하고 그것을 설정하는 방법도 있지만, 그것을 선호하지 않은 경우도 있다.

이러한 경우 #error 지시문을 사용하여 컴파일을 강제로 중단시키는 방법이 있다.

#error 지시문

#error 출력 에러 문자열

전처리는 #error가 평가되면 컴파일을 중지하고 오류 문자를 출력한다. 컴파일에 필요한 개발자의 의사로 이름이 정의되어 있지 않거나, 필요한 헤더 파일이 포함되어 있지 않은 경우에 이러한 오류를 출력하는 방법도 있다.

코드6

#include <stdio.h>
/*#define BUFFERSIZE 0xFF*/

int main() {
#ifdef BUFFERSIZE
  int iArray[BUFFERSIZE];
#else
  #error BUFFERSIZE 상수가 정의 되어 있지 않는다.
#endif
 return 0;
}

코드6은 프로그램이 할당되는 배열의 크기 BUFFERSIZE 상수가 정의되어 있는지 여부를 #ifdef 지시문을 이용하여 조사하고 있다. BUFFERSIZE가 정의되어 있으면, 이 값을 사용하여 배열을 초기화되지만 그렇지 않으면 배열을 초기화할 수 없기 때문에 오류를 발생시킨다. 에러 문자열이 어떻게 표시되는지는 컴파일 환경에 의존하는 문제이다.