Skip to content

68. 조건부 컴파일과 파일 포함


1. 키워드

  • 조건부 컴파일


2. 조건부 컴파일과 파일 포함 사용하기

  • 조건부 컴파일은 정해진 조건에 따라 소스 코드의 컴파일 여부를 제어하는 전처리기이다.


1] 플랫폼과 OS 구분

  • 조건부 컴파일은 플랫폼이나 OS에 특화된 코드를 구분할 때 사용한다.
  • 예를 들어 Windows API로 작성된 코드는 리눅스에서 컴파일할 수 없으므로 조건부 컴파일을 사용하여 WIN32 매크로가 있을 때만 컴파일되도록 만든다.

2] 디버깅 및 테스트

  • 최종 제품에는 디버깅을 위한 코드나 테스트 코드가 들어가면 안 되므로 조건부 컴파일을 사용하여 코드를 분리한다.


  • #include도 전처리기이며 헤더 파일이나 소스 파일을 현재 소스 코드에 포함하여 함께 컴파일하는 기능이다.
  • 보통 특정한 기능을 이용하기 위해 헤더 파일을 포함할 때 사용하며 조건부 컴파일과 연계하여 플랫폼과 OS에 특화된 헤더 파일을 포함한다.


3. 조건부 컴파일 사용하기

  • 조건부 컴파일은 #ifdef#endif 지시자를 사용하여 정의한다.
  • #ifdef에 매크로를 지정하면 해당 매크로가 정의되어 있을 때만 코드를 컴파일한다.


#ifdef 매크로
코드
#endif
#include <stdio.h>

#define DEBUG // DEBUG 매크로 정의

int main()
{
#ifdef DEBUG // DEBUG 매크로가 정의되어 있다면 #ifdef, #endif 사이의 코드를 컴파일
    printf("Debug: %s %s %s %d\n", __DATE__, __TIME__, __FILE__, __LINE__);
#endif

    return 0;
}


  • 먼저 #define으로 DEBUG라는 매크로를 정의한다.
  • 이때 조건부 컴파일에 사용할 매크로는 매크로 이름만 정의해도 된다.
  • 그리고 다음과 같이 조건부 컴파일을 할 코드를 #ifdef, #endif로 묶어준다.
  • 여기서 #ifdef DEBUGDEBUG 매크로가 정의되어 있다면 #ifdef, #endif 사이의 코드를 컴파일한다.


#ifdef DEBUG // DEBUG 매크로가 정의되어 있다면 #ifdef, #endif 사이의 코드를 컴파일
    printf("Debug: %s %s %s %d\n", __DATE__, __TIME__, __FILE__, __LINE__);
#endif


  • printf 안에서 사용한 __DATE__, __TIME__, __FILE__, __LINE__은 컴파일러에서 제공하는 매크로이며 디버그 코드를 작성할 때 유용하다.


1] __DATE__

  • 컴파일한 날짜(실행 시점의 현재 날짜가 아님)

2] __TIME__

  • 컴파일한 시간(실행 시점의 현재 시간이 아님)

3] __FILE__

  • __FILE__ 매크로가 사용된 헤더, 소스 파일

4] __LINE__

  • __LINE__ 매크로가 사용된 줄 번호


  • #ifdef, #endif가 전처리기 과정을 거치면 코드는 다음과 같이 바뀐다.


001


4. 값 또는 식으로 조건부 컴파일하기

  • 이번에는 #if로 값 또는 식을 판별하여 조건부 컴파일을 해보자.


#if 값 또는 식
코드
#endif
#include <stdio.h>

#define DEBUG_LEVEL 2 // 2를 DEBUG_LEVEL로 정의

int main()
{
#if DEBUG_LEVEL >= 2 // DEBUG_LEVEL이 2보다 크거나 같으면 #if, #endif 사이의 코드를 컴파일
    printf("Debug Level 2\n");
#endif

#if 1 // 조건이 항상 참이므로 #if, #endif 사이의 코드를 컴파일
    printf("1\n");
#endif

#if 0 // 조건이 항상 거짓이므로 #if, #endif 사이의 코드를 컴파일하지 않음
    printf("0\n");
#endif

    return 0;
}

// Debug Level 2
// 1


  • 이번에는 2DEBUG_LEVEL 매크로로 정의했다.
  • 그리고 #if DEBUG_LEVEL >= 2와 같이 정의했으므로 DEBUG_LEVEL2보다 크거나 같을 때 코드를 컴파일한다.


#if DEBUG_LEVEL >= 2 // DEBUG_LEVEL이 2보다 크거나 같으면 #if, #endif 사이의 코드를 컴파일
    printf("Debug Level 2\n");
#endif


  • #if에는 값을 그대로 지정할 수도 있다.
  • 여기서 1을 지정하면 조건이 항상 참이므로 코드를 컴파일하고, 0을 지정하면 조건이 항상 거짓이므로 코드를 컴파일하지 않는다.


#if 1 // 조건이 항상 참이므로 #if, #endif 사이의 코드를 컴파일
    printf("1\n");
#endif

#if 0 // 조건이 항상 거짓이므로 #if, #endif 사이의 코드를 컴파일하지 않음
    printf("0\n");
#endif


  • #if, #endif는 전처리기 과정을 거치면 코드는 다음과 같이 바뀐다.


002


  • #ifdefined와 조합하면 복잡한 조건도 만들 수 있다.


#if defined 매크로
코드
#endif
#include <stdio.h>

#define DEBUG // DEBUG 매크로 정의
#define TEST  // TEST 매크로 정의

int main()
{
// DEBUG 또는 TEST가 정의되어 있으면서 VERSION_10이 정의되어 있지 않을 때
#if (defined DEBUG || defined TEST) && !defined(VERSION_10)
    printf("Debug\n");
#endif

    return 0;
}

// Debug


  • #if defined에는 논리 연산자를 사용할 수 있다.
  • 여기서는 DEBUG 또는(||) TEST가 정의되어 있으면서(&&) VERSION_10이 정의되어 있지 않을 때(!) 코드를 컴파일한다.


#if (defined DEBUG || defined TEST) && !defined(VERSION_10)
    printf("Debug\n");
#endif


  • defined 뒤의 ()(괄호)는 생략해도 된다.
  • 그리고 논리 연산의 순서를 명확하게 나타내기 위해 defined와 논리 연산자를 모두 ()로 묶는다.


조건부 컴파일과 디버그 코드

  • #if는 다음과 같이 DEBUG 매크로를 정의해서 사용한다.
  • #define DEBUG 1과 같이 코드에서 10을 지정하여 디버그 코드의 사용 여부를 제어하거나, 컴파일 옵션에서 DEBUG 매크로를 설정하여 디버그 코드를 제어한다.


gcc main.c -DDEBUG
gcc main.c -DDEBUG_LEVEL=2
#define DEBUG 1 // 코드에서 디버그 코드 제어

#if DEBUG
printf("Debug message\n");
#endif

#ifdef DEBUG // 코드에 DEBUG 매크로를 정의하거나 컴파일 옵션에서 DEBUG 매크로 설정
printf("Debug message\n");
#endif

// 코드에서 2를 DEBUG_LEVEL 정의하거나 컴파일 옵션에서 DEBUG_LEVEL에 2를 설정
#ifdef DEBUG_LEVEL >= 2
printf("Debug Level 2\n");
#endif


  • 이번에는 #elif#else를 사용해 보자.


#ifdef 매크로
코드
#elif defined 매크로
코드
#else
코드
#endif

#if 조건식
코드
#elif 조건식
코드
#else
코드
#endif
#include <stdio.h>

#define USB // USB 매크로 정의

int main()
{
#ifdef PS2 // PS2가 정의되어 있을 때 코드를 컴파일
    printf("PS2\n");
#elif defined USB // PS2가 정의되어 있지 않고, USB가 정의되어 있을 때 코드를 컴파일
    printf("USB\n");
#else             // PS2와 USB가 정의되어 있지 않을 때 코드를 컴파일
    printf("지원하지 않는 장치입니다.\n");
#endif

    return 0;
}


  • #elif를 사용하여 PS2가 정의되어 있지 않고, USB가 정의되어 있을 때 코드를 컴파일하고, #else를 사용하여 PS2USB 둘 다 정의되어 있지 않을 때 코드를 컴파일하도록 만들었다.
  • 여기서는 USB만 정의되어 있으므로 #elif defined USB만 만족한다.


#ifdef PS2 // PS2가 정의되어 있을 때 코드를 컴파일
    printf("PS2\n");
#elif defined USB // PS2가 정의되어 있지 않고, USB가 정의되어 있을 때 코드를 컴파일
    printf("USB\n");
#else             // PS2와 USB가 정의되어 있지 않을 때 코드를 컴파일
    printf("지원하지 않는 장치입니다.\n");
#endif


  • #elif로 매크로를 판별할 때는 defined와 함께 사용해야 한다.
  • #ifdef, #elif, #else가 전처리기 과정을 거치면 코드는 다음과 같이 바뀐다.


003


  • #ifndef는 매크로가 정의되어 있지 않을 때 코드를 컴파일한다.


#ifndef 매크로
코드
#endif
#include <stdio.h>

#define NDEBUG // NDEBUG 매크로 정의

int main()
{
#ifndef DEBUG // DEBUG가 정의되어 있지 않을 때 코드를 컴파일
    printf("main function\n");
#endif

    return 0;
}

// main function


  • 여기서는 NDEBUG만 정의되어 있고 DEBUG는 정의되어 있지 않다.
  • 따라서 #ifndef DEBUG를 만족하므로 코드를 컴파일한다.
  • #ifndef#elif, #else와 함께 사용할 수 있다.


5. 파일 포함하기

  • #include 앞에 #이 쓰인 이유는 #include도 전처리기이기 때문이다.
  • #은 전처리기를 호출하는 문법이다.
  • #include 지시자는 헤더 파일(.h)과 소스 파일(.c)를 포함한다.


#include <파일>
#include "파일"


  • #include에 헤더 파일을 지정할 때 <>(홑화살괄호)를 사용하기도 하고, ""(큰따옴표)를 사용하기도 한다.
  • <>""의 차이는 다음과 같다.


1] <>

  • 보통 C 표준 라이브러리의 헤더 파일을 포함할 때 사용한다.
  • 또한, 컴파일 옵션에서 지정한 헤더 파일 경로를 기준으로 파일을 포함한다.

2] ""

  • 현재 소스 파일을 기준으로 헤더 파일을 포함하고, 헤더 파일을 찾지 못 할 경우 컴파일 옵션에서 지정한 헤더 파일 경로를 따른다.
  • 예를 들어, "message.h"는 현재 소스 파일과 같은 경로에 있는 message.h 파일을 포함한다.
  • 그리고 "inc/message.h"는 현재 소스 파일 경로에서 inc 디렉터리 아래의 message.h 파일을 포함한다.
  • 마지막으로 "../message.h"는 현재 소스 파일 경로의 상위 디렉터리에 있는 message.h 파일을 포함한다.


  • 헤더 파일을 만들 때는 확장자명을 .h로 지정하면 된다.
  • 이번에는 작업 디렉터리에 message.h 헤더 파일을 생성하여 추가한다.
  • 헤더 파일이 추가되었으면 message.h 파일에 다음과 같이 입력한다.


#if defined EN
#define HELLO_MESSAGE "Hello"
#elif defined KO
#define HELLO_MESSAGE "안녕하세요"
#elif defined FR
#define HELLO_MESSAGE "Bonjour"
#endif


  • 이제 다음 내용을 입력한 뒤 실행해 보자.


#include <stdio.h>

#define KO           // KO 매크로 정의
#include "message.h" // message.h 헤더 파일 포함

int main()
{
    printf("%s\n", HELLO_MESSAGE); // message.h에 정의한 HELLO_MESSAGE의 값이 출력됨
                                   // KO 매크로가 정의되어 있으므로 HELLO_MESSAGE는 "안녕하세요"가 됨

    return 0;
}

// 안녕하세요


  • #include를 사용하여 message.h 헤더 파일을 포함했다.


#define KO           // KO 매크로 정의
#include "message.h" // message.h 헤더 파일 포함


  • 현재 소스 코드에는 HELLO_MESSAGE 매크로가 없지만 message.h 헤더 파일 안에 HELLO_MESSAGE 매크로가 정의되어 있으므로 다음과 같이 매크로의 값을 출력할 수 있다.
  • 여기서 #include 위에 #define으로 KO를 정의했으므로 message.h에도 KO가 적용되어 #elif KO 부분의 #define HELLO_MESSAGE "안녕하세요"가 컴파일된다.
  • 이때 #include 아래에 KO를 정의하면 적용되지 않으므로 주의한다.


printf("%s\n", HELLO_MESSAGE); // message.h에 정의한 HELLO_MESSAGE의 값이 출력됨
                               // KO 매크로가 정의되어 있으므로 HELLO_MESSAGE는 "안녕하세요"가 됨


  • 다른 언어를 사용하고 싶다면 #include "message.h" 위에 KO 대신 EN이나 FR을 정의하면 된다.
  • 지금까지 C 표준 함수의 보안 경고로 인한 컴파일 에러를 방지하기 위해 #include <stdio.h> 위에 #define _CRT_SECURE_NO_WARNINGS처럼 정의해 준 것도 같은 원리이다.
  • 즉, _CRT_SECURE_NO_WARNINGS가 정의되어 있으면 Visual Studio에서 제공하는 함수 대신 C 표준 함수를 선택해서 사용한다.
  • 이처럼 헤더 파일을 포함하면 헤더 파일 안에 정의된 매크로나 선언된 함수 등을 사용할 수 있다.


004


  • #include#ifdef, #if 등과 조합하여 특정 조건에서만 헤더 파일을 포함할 수 있다.


#define DEBUG

#ifdef DEBUG // DEBUG가 정의되어 있다면 debug_message.h 헤더 파일 포함
#include "debug_message.h"
#else // 아니면 message.h 헤더 파일 포함
#include "message.h"
#endif


  • 지금까지 조건부 컴파일과 파일 포함에 대해 배웠는데 코드에 매크로가 섞이다 보니 알아보기가 쉽지 않았다.
  • 실제로 C로 작성된 프로그램의 소스 코드를 보면 매크로와 조건부 컴파일을 상당히 많이 사용한다.
  • 특히 멀티 플랫폼을 지원하는 프로그램은 매크로와 조건부 컴파일을 복잡하게 사용한다.

References