본문 바로가기

별걸다하는 IT/프로그래밍언어

[C,C++] #if, #ifdef, #elif, #else, #endif 전처리기 지시어 알아보기. #if와 #ifdef 차이점이 무엇일까. 조건부 컴파일 매크로

반응형

[C,C++프로그래밍 완전정복 목차]

안녕하세요~ㅎㅎ 

오늘 알아볼 전처리기 지시어는 #if, #ifdef, #else, #endif 입니다.

if 조건문과 #if 비교를 통해 #if 역할 알아보기 

#if ~#else ~#endif는 조건문 if~else 로직과 비슷하게 보이죠??? 보이는 것처럼 의미도 둘이 유사합니다.

하지만 차이가 있어요.ㅎㅎ 가장 먼저 보이는 가시적 차이는 지시어의 경우 #endif 이렇게 닫아주는 지시어가 있습니다. 일반 if문의 경우 괄호로 블락을 구분하지만 전처리기는 괄호를 사용하지 않기 때문이죠 ㅎㅎ

의미적 차이로는 일반 if 조건문이 FALSE일 경우, 실행이 되지 않을 뿐 컴파일은 된다면, #if가 0이라면 컴파일 자체가 되지 않습니다. 

#include <stdio.h>
int main()
{
	if (0)
	{ //괄호로 범위 지정 
		printf("실행되지는 않지만 컴파일은 됨\n");
	}

#if 0
	printf("컴파일 자체가 안됨\n"); 
#endif
	return 0;
}

전처리기 특성을 이용한거죠. 전처리기는 실제 컴파일이 수행되기 전에 실행되어 소스에 치환되는데 #if 가 0일 경우에는 소스에 삽입되지 않아요. 즉 #if는 if와 다르게 조건에 따라 소스코드를 삽입하거나 삭제하기 위해 사용되는 지시자입니다. 

 

#if는 0이 아닐 경우에 실행이 되어요. 

#include <stdio.h>
#define NUM -3
int main(void)
{
#if NUM
    printf("if: NUM is %d\n", NUM);
#else
    printf("else: NUM is %d\n", NUM);
#endif
}

#if값이 -3으로 음수인데도 #else가 아닌 #if를 탄 것을 확인할 수 있습니다.

#if vs #ifdef  두 지시어의 차이는 무엇인가

그런데 지시자 중 #if가 있고 #ifdef가 있어요 

소스코드를 보면 #if ~ #else ~ #endif 이렇게 쓰인 경우도 있고, #ifdef ~ #else ~ #endif 이렇게 쓰인 경우도 있는데 이 두 지시어의 차이는 무엇일까요??

 

#ifdef의 def는 define의 약자입니다. 즉 #if가 '만약~라면' 이라는 뜻이라면 #ifdef는 '만약 ~가 정의되어 있다면'을 의미해요.  만약 '#ifdef A' 하면 A가 TRUE이던 FALSE이건 #ifdef는 상관하지 않습니다. #ifdef는 오로지 A가 사전에 정의되었느냐 안되었느냐만 확인하는 거예요. 반면 '#if A'는 A에 들어있는 값이 중요하게 작용합니다. 

#include <stdio.h>
#define A 0
int main()
{
#ifdef A
	printf("A is defined\n");
#endif
//-----------------
#if A
	print("A is True\n");
#else
	printf("A is False\n");
#endif
	return 0;
}

그래서 이 코드를 실행시켜보면 A가 0임에도 불구하고 #ifdef #endif 사이에 있는 코드가 컴파일되어 "A is defined"문자열이 출력돼요.

결과창

#if 조건을 분기하는 #elif

조건을 계속 분기할 수 있는 else if와 같은 역할이 지시자에도 있습니다. #elif예요.

#ifdef는 정의되어 있냐 정의되어 있지 않냐 이분법적으로만 나누기 때문에 #elif를 사용할 수 없어요.

하지만 #if는 #elif를 사용할 수 있습니다.

#include <stdio.h>
#define NUM 2
int main(void)
{
#if NUM==1
    printf("NUM is 1\n");
#elif NUM==2
    printf("NUM is 2\n");
#elif NUM==3
    printf("NUM is 3\n");
#else
    printf("NUM is %d\n", NUM);
#endif
	return 0;
}

확인을 위한 간단한 소스를 짜봤어요 ㅎㅎ

#elif NUM==2를 잘 탄 것을 확인할 수 있습니다. 

대표적으로 사용되는 예시

뭐 개념은 조건문 if와 유사하기 때문에 쉬워요. 하지만 어떤 이론이건 현업에서 어떻게 활용되는지 알아야 배움이 와닿겠죠? ㅎㅎ 자주 쓰는 대표적 상황들은 간단하게 예로 들어볼게요.

 

#ifdef __cplusplus

덧셈 뺄셈 기능을 C++로 만들었다고 합시다.

//헤더파일 중 일부
 int add(int a, int b); //예시기 때문에 매우 단순하게 표현
 int minus(int a, int b);

그런데 이 기능을 C 프로젝트에도 갖다 쓰고 싶어요. 

즉 어떤 기능을 만들었으면 이 기능은 C컴파일러에서도, C++컴파일러에서도 호환이 가능하도록 짜는게 좋겠죠?

그런데 C와 C++을 혼합해서 사용할 경우 문제가 발생할 수 있는데 얘네들 linking 방식이 다르기 때문이예요. 이를 해결하기 위해 'extern'을 씁니다. 정확히는 C++에서 선언한 전역변수나 함수를 C언어에서 사용하고 싶을 때 사용하는 키워드가 extern "C" 입니다. 반대는 그냥 extern. (name mangling이라던가 깊은 얘기는 extern "C"포스팅에서 자세히 알아보아요) 

//헤더파일 중 일부
 int add(int a, int b);  // C++ 형식으로 링킹됨 
 
 extern "C" {
   int minus(int a, int b);  //C언어 형식으로 링킹됨 
 }

이 extern "C"없이 C++컴파일러로 컴파일 하면 C++형식으로 링킹되기 때문에 C에서 모듈을 호출해서 쓰면 추후 문제가 발생할 수 있어요. 그래서 C링킹방식을 써! 라는 의미로 extern "C'를 붙여 컴파일 한 후 갖다쓰는 겁니다.

근데 이 extern "C"는 C++ 컴파일러에서만 지원이 됩니다. 그래서 C컴파일러로 컴파일하면 에러가 나요 ㅎㅎ 

//헤더파일 중 일부
#ifdef __cplusplus  //c++일 경우에만 extern "C" {} 가 적용됨
extern "C" {   
#endif
 int add(int a, int b);
 int minus(int a, int b);
#ifdef __cplusplus
}
#endif

이를 해결하기 위해 #ifdef를 사용합니다. #ifdef __cplusplus는 C++일 경우에만 범위에 있는 소스를 컴파일 하라는 의미예요. ㅎㅎ (짧게 설명하려 했는데 길어졌네요;;)

 

#ifdef __DEBUG__, #ifdef __TSET__, #ifdef __KERNEL__

#ifdef __DEBUG__라던가 #ifdef __TEST__라던가 #ifdef __KERNEL__ 이런 코드를 보셨을 수도 있는데,,, 이런게 다 뭐냐~

보통 회사에서는 개발 서버, 테스트서버, 운영서버가 따로 있죠.(서비스 하고 있는걸 개발하기 위해 내릴 순 없으니까요. 그럼 서비스중인게 다 먹통되겠죠? )

암튼 대게 소스를 동일하게 맞춰놓는데 개발서버에서만 동작해야 하는 소스들이 있을거예요. 또 커널에서만 동작해야 하는 소스라던가, 디버그 모드에서만 동작해야 하는 소스라던가 이런 상황이 개발하다보면 등장하기 마련이죠.

#ifdef __TEST__  //개발서버에서는 동작하고 운영서버에서는 동작하지 않게 하기 
  test();
#endif 

이럴 때 컴파일 기본 옵션으로 개발서버에는 __TEST__를 정의해놓고 운영서버에는 __REAL__을 정의해놓은 후, make를 돌리면 실수로 개발에 있는 소스들이 운영으로 들어가는 것을 방지할 수 있어요. 한 서버에 여러 사람이 개발을 진행할 경우 이는 더더욱 필수적입니다. A라는 사람과 C라는 사람이 하나의 소스를 동시에 수정했는데, A가 실수로 반영을 해버려도 둘 다 #ifdef __TEST__를 걸고 개발을 하고 있었다면 운영에서 컴파일이 안먹히니 큰 문제를 막을 수 있는거죠~.

또, 디버그나 커널 또한 마찬가지의 개념입니다. ㅎㅎ 디버그때만 필요한 코드들이 운영에 찍힐 필요는 없겠죠?? 그리고 커널에서만 동작해야하는 코드가 유저모드에서 동작해버리면 큰일날거예요. (참고로 그래서 리눅스 커널 소스를 보면 #ifdef __KERNEL__를 흔히 볼 수 있어요.)

 

#if MODE #else #endif

(간단하게 테스트 할 경우) 내가 테스트 하고 싶은 두 가지 상황이 있다고 합시다. 

왔다갔다 테스트를 좀 하고 싶은데, 테스트할 때마다 매번 소스를 일일이 부분부분 수정해주려면 매~~우 귀찮겠죠.

이럴 경우 #if #else #endif를 써서 두 가지 상황을 모두 코딩해준 후 매크로 정의 한 줄 부분만 변경해주면 원하는 모드로 쉽게 change할 수 있습니다. 

#define MODE 1  --A테스트 환경
//#define MODE 0  --B테스트 환경
void test()
{
어쩌구저쩌구 소스 블라블라 기존소스들..
#if MODE  --A테스트 환경
   MODE가 1일 경우는 여기 소스 블라블라
#else     --B테스트 환경
   MODE가 0일 경우 여기 소스 블라블라
#endif
어쩌구저쩌구 소스 블라블라
#if MODE  --A테스트 환경
   MODE가 1일 경우는 여기 소스 블라블라
#else     --B테스트 환경
   MODE가 0일 경우 여기 소스 블라블라
#endif
어쩌구저쩌구 소스 블라블라
}

또 매크로를 소스안에 선언하기 보다 #if MODE #else로 코드를 짜준 뒤, 컴파일러 옵션에 MODE값을 매크로로 지정해 컴파일 해주면 소스는 건드릴 필요 없이 두 가지 모드로 테스트가 가능합니다. 

gcc test.c -o test -DMODE=1  

 

#if 1 / #if 0 

음 잘 사용되는 상황을 하나 더 예시들어보자면, 현재 사용되는 소스는 아닌데 나중에 원복할 것 같아서, 또는 다시 사용해야 할 일이 있어 남겨둬야 될 것 같은 소스들 있잖아요. 섣불리 지울 수 없는 소스들... 그렇다고 if를 쓰자니 기존에 있는 소스인지 헷갈리겠죠? 컴파일 되면 쓸데없이 용량만 잡아먹고요. 

일시적으로 사용되는 소스, 특정 기간 동안 이벤트로 삽입된 소스, 일정 기간동안 테스트를 위한 소스, 누군가가 요청해서 일주일간 풀어주는 거, 추후 활용을 위해 남겨놔야 할 경우 등등..

이런 상황에서 #if 1, #if 0을 종종 사용합니다.

 

오늘은 여기까지입니다. 정성들여 작성해보았어요.

도움이 되셨다면, 좋아요/댓글/광고보답으로 마음을 표현해주시면 어떨까요? 

정보공유에 힘이 됩니다. :) 다음 포스팅에서 또 봐요!

반응형