[테크월드=정환용 기자] 이번 글에서 다룰 내용은 전처리기의 고급 활용이다. 전처리기의 기능을 최대한 활용할 수 있는 방법을 몇 가지 소개한다. 먼저 함수형 매크로의 잘못된 사용과 일반적으로 실수를 많이 할 수 있는 부분을 설명하고, #과 ## 연산자에 대한 사용방법을 설명한다. 또한, “do { ... } while(0)”의 활용 노하우와 #if와 #ifdef의 활용법을 설명한다.

함수형 매크로의 사용 주의사항
함수형 매크로는 매우 단순하고 사용하기 쉬워 보인다. 하지만 실제 사용에서는 함수형 매크로로 인해 많은 문제가 발생된다. 다음은 몇몇 함수형 매크로의 잘못된 사용 예제와 문제를 해결할 수 있는 방법들이다.

 

함수형 매크로 파라미터에는 괄호가 필수

#define TIMES_TWO(x) x * 2

보기에 매우 간단한 연산을 하는 함수형 매크로다. 예를 들어TIMES_TWO(4)의 결과 값은 4*2를 연산해 8로 값을 생성한다. TIMES_TWO(4 + 5)의 경우 예상되는 연산 값은 18이다. 하지만 예상과 전혀 다른 14로 값을 생성한다. 4+5가 파라미터로 사용될 경우 4+5*2의 연산을 하기 때문이다. 이런 문제의 해결은 파라미터에 괄호를 사용하면 된다.

#define TIMES_TWO(x) (x) * 2

수식에 사용되는 파라미터에 괄호가 있는 경우 (4+5)*2의 연산을 해, 예상과 같은 18의 결과를 생성한다. 함수형 매크로의 수식에는 전체 괄호가 필요하다

#define PLUS1(x) (x) + 1

파라미터에 괄호가 있고 다음과 같은 경우 문제없이 예상 결과를 생성한다. PLUS1(10)의 경우 11의 값을 생성해 출력한다.

printf(“%dn”, PLUS1(10));

그러나 다음의 경우에는 예상과 다른 결과를 보인다. 예상 값은 22이었으나 실제 값은 21이 나온다.

printf(“%dn”, 2 * PLUS1(10));

함수형 매크로에 정의된 수식이 매크로 사용 위치에 그대로 치환되므로 다음과 같은 연산을 한다.

printf(“%dn”, 2 * (10) + 1);

예상과 동일한 연산과 값을 만들기 위해, 수식 전체에 괄호를 추가한다.

#define PLUS1(x) ((x) + 1)
괄호가 추가된 경우 의도와 동일한 값인 22를 출력할 수 있다.

printf(“%dn”, 2 * ((10) + 1));

 

함수형 매크로 입력 파라미터의 잘못된 사용

#define SQUARE(x) ((x) * (x))
printf(“%dn”, SQUARE(++i));

예제 코드의 작성 의도는 i값을 하나 증가시킨 값으로 제곱해 값을 생성하는 것이었으나 실제 생성된 코드를 보면 다음과 같은 코드가 생성된다.

printf(“%dn”, ((++i) * (++i)));

생성된 코드의 문제는 의도와 다르게 i의 값을 증가시킨 값과 또 한번 증가시킨 값을 곱한다. 코드의 의도는 i값을 한 번 증가시킨 후 증가된 값을 제곱하는 것이었다. 문제를 해결하기 위한 방법은 가능한 매크로 사용을 하지 않는 것이다. 최근 대부분의 C/C++ 컴파일러들이 제공하는 인라인 함수 기능을 사용하는 것이다.

 

특별한 매크로 기능
“#”연산자를 사용해 문자열 만들기

“#” 연산자는 함수형 매크로에서 파라미터를 스트링 문자열에 삽입해 변환 할 수 있는 연산자다. 사용하기 쉬우며 다양한 곳에서 활용될 수 있다.

#define NAIVE_STR(x) #x
puts(NAIVE_STR(10)); /* This will print “10”. */

다음의 예는 의도와 다르게 동작될 것이다.

#define NAME Anders
printf(“%s”, NAIVE_STR(NAME)); /* Will print NAME. */

앞의 문자열 출력 예제의 작성 의도는 Anders라는 이름을 출력하는 것이었으나, 의도와 같이 동작되지 않았다. “#” 연산자로 해당 문제를 해결할 수 있다.

#define STR_HELPER(x) #x
#define STR(x) STR_HELPER(x)

앞의 해결 방법은 STR(NAME) 함수형 매크로를 사용하면, 다시 매크로에서 다른 함수형 매크로 STR_HELPER(NAME)를 사용하게 되는 형태며, 결과는NAME 매크로의 값인 Anders를 문자열로 출력하게 되는 구조다.

“##” 연산자를 사용해 파라미터를 연결
“##” 연산자는 전처리기 매크로에서 큰 식별자나 숫자 또는 어떤 문자열이든 조각조각을 이어 붙이는 기능을 한다. 예를 들어 다음과 같은 변수들이 있다고 가정한다.

MinTime, MaxTime, TimeCount.
MinSpeed, MaxSpeed, SpeedCount.

앞의 변수 내용을 참고해 평균 값을 구하는 AVERAGE라는 함수형 매크로를 만들 때 “##” 연산자를 이용하면, 다음과 같이 만들 수 있다.

#define NAIVE_AVERAGE(x)
(((Max##x) - (Min##x)) / (x##Count))

일반적인 방법으로 다음과 같이 함수형 매크로를 사용한다.

NAIVE_AVERAGE(Time);

이 경우 실제 매크로가 동작되는 모습은 다음과 같다. Time이라는 문자가 붙은 변수의 이름을 참조해 평균 값을 계산한다.

return (((MaxTime) - (MinTime)) / (TimeCount));

앞의 언급된 “#”연산자를 사용한 예와 같이, 다음의 문맥에서는 의도한 동작과 다르게 동작할 것 이다.

#define TIME Time
NAIVE_AVERAGE(TIME)

의도와 다르게 다음과 같이 동작한다.

return (((MaxTIME) - (MinTIME)) / (TIMECount));

해결방법은 앞의 STR 예제와 같이 함수형 매크로를 두 단계로 사용하게 하는 것이다.

#define GLUE_HELPER(x, y) x##y
#define GLUE(x, y) GLUE_HELPER(x, y)

앞의 문제 해결 방법을 종합적으로 사용했을 때, 다음과 같은 문제없는 평균 구하기 함수형 매크로가 완성된다.

#define AVERAGE(x)
(((GLUE(Max,x)) - (GLUE(Min,x))) / (GLUE(x,Count)))

 

“do {} while(0)”을 활용해 매크로 만들기
C 언어에서 매크로를 만드는 것은 일반적인 C언어 문법과 크게 다르지 않다. 상수에는 오브젝트형 매크로를 사용하고 특정 코드 구문을 대체하기 위해 함수형 매크로를 사용한다. 하지만 함수형 매크로를 사용하는 경우 세미콜론 사용의 문제가 있을 수 있다. 함수형 매크로 사용에 세미콜론 사용의 문제를 해결할 수 있는 방법은 “do {} while(0)”을 활용하는 것이다.

다음은 a_function(); 함수를 매크로 작성해 사용한 예다.

void test()
{
a_function(); /* The semicolon is not part of A_MACRO(); the macro substitution. */
}

세미콜론은 코드에서 사용됐으므로 다음과 같이 매크로를 정의한다.

#define DO_ONE() a_function(1,2,3)

그러나 함수를 두 번 호출하는 매크로에서는 세미콜론이 문제가 될 수 있다.

#define DO_TWO() first_function(); second_function()

예제와 같이 DO_TWO() 매크로를 세미콜론과 함께 사용하면 다음과 같다.

DO_TWO();

앞의 코드는 다음과 같이 변경돼 수행한다.

first_function(); second_function();

그러나 다음의 구문에서는 문제가 될 수 있다.

if (... test something ...)
DO_TWO();

앞의 예제는 다음과 같이 코드를 변형한다.

if (... test something ...)
first_function();
second_function();

문제는 의도와 다르게 “if” 조건 구문에서 조건을 만족하는 경우 다음의 함수 호출만 하며, 두 번째의 함수는 “if”구문과 관계없이 호출한다. 이런 문제를 해결하기 위해 매크로 전체에 대한 중괄호를 사용하면 어떨까?

#define DO_TWO()
{ first_function(); second_function(); }

앞의 방법 또한 좋은 해결 방법이 되지 못한다. “if” “else” 구문에서 사용될 경우 문제가 있다.

if (... test something ...)
DO_TWO();
else
...

앞의 예제는 다음과 같이 코드가 변형된다.

if (... test something ...)
{ first_function(); second_function(); };
else
...

“if” 구문의 조건이 만족하는 경우 수행돼야 하는 코드에서 c언어 문법 오류를 범하게 된다. 앞의 문제들을 해결할 수 있는 방법으로 “do {} while(0)”을 활용하는 방법이 있다.

#define DO_TWO()
do { first_function(); second_function(); } while(0)

사실 “do {} while(0)” 구문은 반복문이다. “do”에 해당하는 코드를 우선 수행 후 “while”의 조건식에 따라 구문을 반복한다. 반복 코드를 우선 수행 후 조건식에 따라 반복을 결정하기에 “while”의 조건식에 무조건 False로 판단되도록 “0”을 입력하는 것이다. 이런 방법으로 코드 수행을 반복하지않고 1회만 수행하게 된다.

함수를 두 번 호출하게 되는 “if” “else” 구문에서 “do {} while(0)” 사용의 매크로를 사용하면 다음과 같은 코드를 생성한다.

if (... test something ...)
do { first_function(); second_function(); } while(0);
else
...

“do {} while(0)” 구문을 사용한 매크로는 코드 자체가 직관적이지 않으므로 처음 해당 매크로를 보면 조금 이해하기 어려울 수 있다. 주석을 달아 설명하면 이해하기 좋을 것이다.

 

#ifdef보다 #i를 사용
대부분의 애플리케이션은 실제 소스 코드의 일부를 의도적으로 제외시키는 구성을 갖고 있다. 예를 들어 테스트용 코드나, 프로세서 별 필요한 특정 라이브러리 코드 삽입과 제외 등 여러가지 방 법으로 구 성한다. 이런 방 법은 “ if”와 “ifdef” 전처리 명령어를 이용해 코드를 컴파일전 삽입 또는 제외한다.

#ifdefs

#ifdefs를 사용한 예다.

#ifdef MY_COOL_FEATURE
... included if “my cool feature” is used ...
#endif
#ifndef MY_COOL_FEATURE
... excluded if “my cool feature” is used ...
#endif

일반적으로 #defs를 사용하는 애플리케이션은 특별한 구성이나 변수 값 설정 없이도 사용할 수 있다.

#ifs
#ifs 전처리기 명령은 일반적으로 정의된 심볼을 사용한다. 심볼 값의 True와 False를 판별해 코드를 추가 또는 제외한다. True와 False는 1과 0으로 나타낼 수 있다.

#if MY_COOL_FEATURE
... included if “my cool feature” is used ...
#endif
#if !MY_COOL_FEATURE
... excluded if “my cool feature” is used ...
#endif

해당 전처리기 명령은 여러 상태를 구분해 사용할 수 있다.

#if INTERFACE_VERSION == 0
printf(“Hellon”);
#elif INTERFACE_VERSION == 1
print_in_color(“Hellon”, RED);
#elif INTERFACE_VERSION == 2
open_box(“Hellon”);
#else
#error “Unknown INTERFACE_VERSION”
#endif

이런 방법의 전처리기를 사용한 애플리케이션에서는 구성 변수의 값을 지정해야 한다. 예를 들어 “defaults.h” 헤더의 경우, 심볼을 정의하고 값을 지정해 사용한다. 다음은 “defaults. h”의 예제다.

/* defaults.h for the application. */
#include “config.h”
/*
* MY_COOL_FEATURE -- True, if my cool feature
* should be used.
*/
#ifndef MY_COOL_FEATURE
#define MY_COOL_FEATURE 0 /* Off by default. */
#endif

 

#if 와 #ifdef의 차이
두 방식은 거의 동일해 보인다. 실제 애플리케이션에서도 거의 동일한 용도로 사용된다. #ifdefs가 사용하기 쉬워 보이지만, 경험적으로 #ifs가 좀 더 활용하기 좋다. #ifdefs는 철자가 틀려도 컴파일 중 알 수 없지만 #ifs는 철자가 틀리면 정의된 심볼이 아니기에 문제를 식별할 수 있다. #ifdefs는 단지 심볼의 정의 여부만 판단하기 때문이다. 예를 들어 다음과 같은 오류는 확인하기 어렵다.

#ifdef MY_COOL_FUTURE /* Should be “FEATURE”. */
... Do something important ...
#endif

반면 대부분의 컴파일러는 #if 전처리 명령어를 사용할 때 정의되지 않은 심볼을 사용하는
경우 검사할 수 있다. C언어의 표준이며 이런 경우 심볼의 값은 0으로 정의된다. IAR Systems 컴파일러에서는 기본적으로 분석 메시지 Pe193가 출력된다. 애플리케이션에 #ifdefs를 사용할 때, 설정 값이 바뀌면 문제가 된다. 예를 들어 색깔을 정의할 때, 색상의 속성까지 정의돼 사용하기에는 비효율적이다. 반면 #if를 사용하는 경우 정의된 값의 변경이 가능하므로 좀 더 효율적이다.

#ifndef USE_COLORS
... do something ...
#endif

#ifs를 사용하면 다음과 같은 코드를 작성할 수 있다.

#define DON’T_USE_COLOR 0
#define RED 1
#define GREEN 2
#if COLORS == DON’T_USE_COLOR
... do something ...
#elif COLORS == RED
... do something ...
#elif COLORS == GREEN
... do something ...
#endif

#ifdefs를 사용해 상태를 구분 할 수도 있다. 하지만 코드의 가독성이 떨어지며 비효율적이다.

#ifdef DONT_USE_COLORS
#define USE_COLORS 0
#endif
/* Set the default. */
#ifndef USE_COLORS
#define USE_COLORS 1
#endif

 

맺음말
이번에는 다양한 전처리기 명령어들의 효율적인 소스 코드 작성을 위한 활용법과 사용 팁을 전달했다. 언급된 전처리기 명령어들 외 다양한 명령어들과 활용 법들도 있다. 코드를 작성하는 사람마다 주로 사용되는 전처리기 명령어들과 활용 법이 다를 수 있다. 전처리기와 명령어들의 동작을 이해하고 다양한 활용법을 익히면 소스 코드 작성이 훨신 효율적이고 안전한 코드를 만들 수 있다.

 

작성: IAR Sytems 기술지원팀 이현도 과장

회원가입 후 이용바랍니다.
키워드
개의 댓글
0 / 400
댓글 정렬
BEST댓글
BEST 댓글 답글과 추천수를 합산하여 자동으로 노출됩니다.
댓글삭제
삭제한 댓글은 다시 복구할 수 없습니다.
그래도 삭제하시겠습니까?
댓글수정
댓글 수정은 작성 후 1분내에만 가능합니다.
/ 400
내 댓글 모음
저작권자 © 테크월드뉴스 무단전재 및 재배포 금지