ProgrammingInObjectiveC:Chapter 12: Difference between revisions
Onionmixer (talk | contribs) (OC2 12장 :: 전처리기 페이지 추가) |
Onionmixer (talk | contribs) (띄어쓰기 수정) |
||
(One intermediate revision by the same user not shown) | |||
Line 64: | Line 64: | ||
<syntaxhighlight lang="objc"> | <syntaxhighlight lang="objc"> | ||
-(double) area | |||
{ | |||
return PI * radius * radius; | |||
} | |||
-(double) circumference | |||
{ | |||
return 2.0 * PI * radius; | |||
} | |||
</syntaxhighlight> | </syntaxhighlight> | ||
Line 595: | Line 604: | ||
혹은 그냥 다음 | 혹은 그냥 다음 명령문으로도 충분하다. | ||
<syntaxhighlight lang="objc"> | <syntaxhighlight lang="objc"> |
Latest revision as of 04:27, 3 August 2013
- 12장
- 전처리기
12장 :: 전처리기
전처리기는 프로그램을 개발하고, 읽고, 수정하고, 다른 시스템으로 포팅하는 작업을 좀더 쉽게 해준다. 전처리기를 쓰면 Objective-C 를 특별한 프로그래밍 용도 혹은 자신만의 프로그래밍 스타일에 맞도록 편리하게 사용할 수 있다.
전처리기는 Objective-C 컴파일 과정에서 프로그램 코드에 산재한 특별한 명령문을 인식한다. 그 이름이 암시하듯, 전처리기는 이 명령문들을 Objective-C 프로그램을 분석하기 전에 처리한다. 전처리 명령문을 만들기 위해서는 샵 기호(#)를 줄의 맨 앞에 붙여야 한다. 앞으로 보겠지만, 전처리 명령문은 일반적인 Objective-C 명령문과 약간 문법이 다르다. 먼저 #define 문을 살펴보자.
#define 명령문
#define 문의 주 용도는 프로그램 상수에 심벌 명을 부여하는 것이다. 다음 전처리 명령문은 TRUE 라는 이름이 값 1 과 동일하도록 정의한다.
#define TRUE 1
이제 상수 1 을 쓸 자리에 TRUE 라는 이름을 대신 사용할 수 있다. 이 이름이 나타나는 위치마다 전처리기는 정의된 값 1 로 자동 대치해 준다. 예를 들어, 정의된 이름 TRUE 를 사용하는 다음 Objective-C 명령문이 있다고 하자.
gameOver = TRUE;
이 명령문은 TRUE의 값을 gameOver 에 대입한다. TRUE 에 정의한 실제 값이 무엇인지에는 신경 쓸 필요가 없다. 그러나 여기서는 1 이라고 정의해 놓았으므로 gameOver 에 1이 대입될 것이다. 다음 전처리문은 FALSE 라는 이름을 값 0 이라고 정의한다.
#define FALSE 0
이렇게 정의하고 나면, 이후에 FALSE 를 0 과 동일하게 쓸 수 있다. 다음 명령문은 gameOver 에 FALSE 를 대입한다.
gameOver = FALSE;
다음 명령문은 gameOver의 값과 FALSE에 정의된 값을 비교한다.
if ( gameOver == FALSE )
정의된 이름은 변수가 아니다. 따라서 대치하는 결과값이 변수가 아닌 한, 값을 새로 대입해줄 수 없다. 프로그램에서 정의한 이름이 사용되면, 전처리기는 #define 문의 오른쪽에 나오는 것으로 대치해준다. 문서 편집기에서 검색하고 변경하는 것과 유사하다. 이런 식으로 생각하면 전처리기는 정의한 이름이 나타날 때마다 연결된 텍스트로 대치해 주는 것이다.
이 #define 명령문은 문법이 독특하다. TRUE에 값 1을 대 입할 때 등호가 사용되지 않는다. 게다가 세미콜론도 명령문 뒤에 나타나지 않는다. 왜 이런 독특한 명령문이 사용되는지 곧 이해하게 될 것이다.
일반적으로 #define 문은 코드의 맨 앞부분에 #import 문이나 #include 문 다음에 있다. 이 순서가 반드시 지켜져야 하는 것은 아니다. 프로그램 코드 어디서든 #define 을 사용할 수 있다. 그러나 이름은 프로그램에서 사용하기 전에 정의되어 있어야 한다. 정의된 이름은 변수처럼 동작하지 않는다. 따라서, 지역 정의 같은 것은 존재하지 않는다. 이름이 정의된 뒤에는 프로그램 어디서든 사용할수 있다. 대부분의 프로그래머들은 헤더파일에 집어넣어,하나 이상의 소스파일에서 사용할 수 있도록 한다.
정의된 이름을 사용하는 다른 예를 보자. Circle 객체의 넓이와 원주를 계산하는 메서드 두 개가 필요하다고 하자. 두 메서드 모두 상수 π 를 사용해야 하는데, 기억하기 쉬운 상수가 아니다. 따라서, 프로그램을 시작할 즈음에 이 상수값을 정의해서 각 메서드에서 필요할 때마다 이 값을 사용하도록 하면 좋을 것이다.
프로그램에 다음코드를 넣자.
#define PI 3.141592654
Circle 클래스의 메서드에서 이 값을 다음과 같이 사용할 수 있다(여기서 Circle 클래스가 radius 라는 인스턴스 변수를 보유한다고 가정한다).
-(double) area
{
return PI * radius * radius;
}
-(double) circumference
{
return 2.0 * PI * radius;
}
상수를 심벌 명에 할당하면 특정 상수 값을 사용할 때마다 기억할 필요가 없다. 그 뿐 아니라 상수 값을 바꿔야 한다면(예를 들어 잘못된 값을 사용하고 있었다면), 프로그램의 단 한 곳, #define 명령문 에서만 값을 변경해 주면 된다. 이 방법을 사용하지 않으면 프로그램 전체를 검색하여 상수가 사용된 곳마다 값을 변경해 주어야 한다.
지금껏 정의한 define들(TRUE , FALSE, pl)을 보자. 모두 대문자로 쓰여 있다. 이를 통해 변수와 정의된 값을 시각적으로 구분하는 것이다. 어떤 프로그래머들은 define 이름에 대문자만 사용하여 이름으로 변수, 객체, 클래스 이름과 define 이름을 쉽게 구별할 수 있도록 한다. 또 자주 사용되는 이름 규칙으로는 define 이름을 k 로 시작하는 것이 있다. 이 경우, 그 뒤에 나오는 글자를 모두 대문자로 쓰지 않는다. kMaximumValues 와 kSignificantDigit는 이 이름 규칙을 따르는 define 이름이다.
상수값에 define 이름을 사용하면 프로그램을 더 쉽게 확장할 수 있다. 예를들어, 배열이 어떻게 동작하는지 학습하면, 할당하고 싶은 배열의 크기를 하드코딩하지 않고 다음과 같이 값으로 정의할 수 있다.
#define MAXIMUM_DATA_VALUES 1000
이후 배열을 생성할 때 배열 크기에 이 정의한 값을 사용하고, 이 값을 기준으로 배열의 유효한 인덱스에 접근할 수 있다.
또한, 프로그램이 배열 크기가 사용되는 곳에 MAXIMUM_DATA_VALUES 를 사용할경우, 이후에 배열 크기를 바꿔야할 때 프로그램 앞의 define 을 선언하는 부분만 수정하면 된다.
define 의 고급형태
간단한 상수값 외에 다른 것들에서도 define 이름을 정의할 수 있다. 표현식을 포함해도 되고, 거의 모든 것을 포함해도 된다.
다음은 2.0 과 3.141592654 를 곱한 결과를 TWO_PI 라는 이름으로 정의한다.
#define TWO_PI 2.0 * 3.141592654
이제 이 정의된 이름을 표현식 2.0 * 3.141592654 가 유효한 곳에서는 언제나 사용할 수 있다. 따라서 이전 예제에서 circumference 메서드의 return 명령문을 다음과 같이 고칠 수 있다.
return TWO_PI * radius;
정의된 이름이 Objective-C 프로그램에서 나을 때마다, #define 문에서 정의된 이름의 오른쪽에 있는 글자로 대치된다. 따라서 전처리기가 앞의 return 문에서 TWO_PI 를 접할 때, 이 이름을 #define 문에서 설정해 준 내용으로 대치한다. 따라서 전처리기는 문자 그대로 프로그램에서 정의된 이름인 TWO_PI 가 나타날 때마다 이를 2.0.3.141592654 로 대치한다.
전처리기가 텍스트를 글자 그대로 대치하기 때문에, 보통 #define 문은 세미콜론으로 끝내지 않는다. 세미콜론을 추가하면 정의된 이름이 나타날 때마다 세미콜론까지 대치되기 때문이다. 만일 PI를 다음과 같이 정의했다고 해보자.
#define PI 3.141592654;
그리고 다음과 같은 명령문을 작성했다면 어떻게 될까?
return 2.0 * PI * r;
전처리기는 정의된 이름인 PI가 나타날 때마다 3.141592654; 로 대치한다. 전처리기가 이 명령문을 대치하고나서 컴파일러는 다음 명령문을 보게 될것이다.
return 2.0 * 3. 141592654; * r;
이 명령문은 구문 오류를 낸다. 정말 세미콜론을 사용해야 하는 경우가 아니라면 마지막에 세미콜론을 추가하지 말자.
결과 표현식이 사용되는 곳에서 유효하기만 하면, 전처리기 정의 자체는 유효한 Objective-C 표현식일 필요가 없다. 예를 들어 다음과 같이 정의했다고 하자.
#define AND &&
#define OR ||
그리고 다음과 같은 표현문들을 작성했다고 하자.
if ( x > 0 AND x < 10 )
...
if ( Y == 0 OR Y == value )
...
심지어 동일성을 테스트하기 위해 #define 을 포함할 수도 있다.
#define EQUALS ==
이제 다음과 같은 명령문을 작성할 수 있다.
if ( Y EQUALS 0 OR Y EQUALS value )
...
드디어 동일성을 비교할 때 실수로 대입 연산자를 사용할 가능성을 없앨 수 있다.
비록 이 예제들에서 #define 문이 지닌 강력한 면이 보이지만, 프로그래밍 언어 하부의 문법을 이런 식으로 재정의하는 것은좋지 않은 프로그래밍 습관이다. 또한 다른사람이 당신의 코드를 이해하기 더 어려울 것이다.
흥미롭게도 정의한 값 자체를 다른 define 에서 참조할 수 있다. 다음 두 #define 문은 모두 유효하다.
#define PI 3.141592654
#define TWO_PI 2.0 * PI
TWO_PI 는 앞에 정의된 이름 PI 를 사용하므로 3.141592654 를 다시 쓰지 않아도 된다. define 의 정의를 다음과 같이 바꿔도 유효하다.
#define TWO_PI 2.0 * PI
#define PI 3.141592654
여기서 규칙은, 정의된 이름이 프로그램에서 사용될 때 모든 것이 정의되어 있다면 define 에서 다른 define 을 사용할 수 있다는 것이다.
- define 을 잘 사용하면 프로그램에서 주석을 달 필요가 줄어든다. 다음 명령문을 살펴보자.
if ( year % 4 == 0 && year % 100 != 0 || year % 400 == 0 )
...
이 표현식은 변수의 해가 윤년인지 확인한다. 다음 #define 문과 뒤따라 나오는 if 문을 살펴보자.
#define IS_LEAP_YEAR year % 4 == 0 && year % 100 != 0 \
|| year % 400 == 0
...
if ( IS_LEAP_YEAR)
...
대개 전처리기는 define 정의가 단 한줄이라고 가정한다. 만일 두 번째 줄이 필요하다면 첫 줄 맨 마지막을 백슬래시 문자(\)로 끝내야한다. 이 문자는 전처리기에게 define이 계속 이어짐을 나타낼 뿐 다른 의미는 없다. 두 줄 이상인 경우도 마찬가지다. 뒤에 이어지는 줄이 있다면 그 줄은 백슬래시 문자로 끝나야 한다.
여기서 if 문은 그 앞의 경우보다 훨씬 이해하기 쉽다. 명령문 자체가 자신을 설명하고 있기 때문에 주석이 필요 없다. 물론, 이 정의는 year 변수가 윤년인지 아닌지만 테스트한다. 변수 year뿐 아니라 어느 해든 윤년인지 확인할 수 있게 만든다면 좋을 것이다. 사실, 인수를 하나 이상 받는 define 을 작성할 수 있다. 다음 주제는 바로 이 내용이다.
다음과 같이 IS_LEAP_YEAR 가 y 라는 인수를 받도록 정의할 수 있다.
#define IS_LEAP_YEAR(y) y % 4 == 0 && y % 100 != 0\
|| y % 400 == 0
메서드 정의와 달리, 여기서는 인수 y 의 형을 지정해 주지 않는다. 그 이유는 함수를 호출하는 것이 아니라 글자를 대치하는 일을 할 뿐이기 때문이다. 인수를 받는 이름을 정의할 때는 정의된 이름과 인수 목록 왼쪽 괄호 사이에 빈칸을 넣으면 안된다.
앞의 정의를 사용하여 다음과 같은 명령문을 작성할 수 있다.
if ( IS_LEAP_YEAR (year) )
...
이 명령문은 year 의 값이 윤년인지 테스트한다. nextYear 의 값이 윤년인지 테스트하고 싶다면, 다음과 같이 작성한다.
if ( IS_LEAP_YEAR (nextYear) )
...
이 명령문에서 if 문 안의 IS_LEAP_YEAR 는 define 정의에서 y 가 나타나는 곳 마다 nextYear 로 대치된다. 따라서 컴파일러는 if 문을 다음처럼 보게 될 것이다.
if ( nextYear % 4 == 0 && nextYear % 100 != 0 || nextYear % 400 == 0 )
...
이 정의들을 보통 매크로라고 부른다. 이 용어는 보통 인수를 하나 이상받는 정의에 사용된다.
SQUARE 라는 이 매크로는 인수를 제곱한다.
#define SOUARE(x) x * x
비록 SQUARE 매크로의 정의가 분명하지만, 매크로를 정의할 때 발생할 수 있는 흥미로운 함정에 빠지지 않도록 주의해야 한다. 위에서 언급했던 것처럼 다음 명령문은 v2 의 값을 y 에 할당한다.
y = SOUARE (v);
다음 명령문의 경우,어떤일이 발생할지 생각해 보라.
y = SOUARE (v + 1);
이 명령문은 예상과 달리 (v + 1)(2제곱)을 y 에 대입하지 않는다. 전처리기가 매크로에 인수의 텍스트 대치 작업을 수행하기 때문에, 위 표현식은 다음과 같이 평가된다.
y = v + 1 * v + 1;
당연히 이 문장은 예상했던 결과값을 생성해 주지 않는다. 이 문제를 제대로 처리하려면 SQUARE 매크로 정의에 괄호를 사용해야 한다.
#define SOUARE(x) ( (x) * (x) )
이 정의가 좀 이상해 보일지도 모른다. SQUARE 매크로에 주어진 값이 매크로안의 x 를 글자 그대로 대치함을 기억하도록 하자.
다음 첫 번째 명령문에 SQUARE 의 새 매크로 정의를 적용한다고 해보자. 그 다음과같이 정상적으로 평가된다.
y = SQUARE (v + 1);
y = ( (v + 1) * (v + 1) );
다음 매크로를 써서 Fraction 클래스에서 새 분수를 쉽게 생성해낼 수 있다. 이 매크로를 작성한 후에는 그 다음과 같이 표현식을 작성할 수 있다.
#define MakeFract(x, y) ([[Fraction alloc] initWith: x over: y)))
myFract = MakeFract (1, 3); // 분수 1/3을 만든다.
심지어 다음 표현식으로 분수 n1 / d1 과 n2 / d2 를 더할 수도 있다.
sum = [MakeFract (n1, d1) add: MakeFract (n2, d2)];
매크로를 정의할 때 조건 연산자를 써도 매우 유용하다. 다음은 두 값의 최댓값을 알려 주는 MAX 라는 매크로 정의다. 이 매크로를 쓰면 다음과 같은 명령문을 작성할 수 있다.
#define MAX(a,b) ( ((a) > (b)) ? (a) : (b) )
limit = MAX (x + y, minValue);
이 표현식은 x + y 와 minValue 가운데 큰 값을 limit 에 대입한다. 전체 MAX 정의에 괄호를 씌워 다음과 같은 표현식이 정상적으로 평가되도록 하였다.
MAX (x, y) * 100
괄호로 각 인수를 감싸면 다음 표현식은 정상적으로 평가될 수 있다.
MAX (x & y, z)
& 연산자는 비트 AND 연산자다. 매크로에서 사용되는 > 연산자보다 우선순위가 낮다. 매크로 정의에서 괄호를 쓰지 않았다면 > 연산자가 비트 AND 연산자보다 먼저 평가되어 잘못된 결과를 생성할 것이다.
다음 매크로는 문자가 소문자인지 테스트한다. 이 매크로로 그 다음과 같은 표현식을 작성할 수 있다.
#define IS_LOWER_CASE(x) ( ((x) >= 'a') && ((x) <= 'z') )
if ( IS_LOWER_CASE (c) )
...
심지어 이 매크로를 다른 매크로 정의에 사용하여 소문자를 대문자로 변환할 수도 있다.
#define TO_UPPER(x) ( IS_LOWER_CASE (x) ? (x) - 'a' + 'A' : (x) )
다시 말하지만, 여기서는 표준 ASCII 문자 세트를 다룬다. 2부에서 Foundation 스트링을 배우면 국제(유니코드) 문자 세트에서 대소문자를 어떻게 변환하는지를 보게 될 것이다.
#(샵) 연산자
매크로 정의에서 매개변수 앞에 # 을 붙이면 전처리기는 매크로가 호출될 때 매크로 인수에서 C 스타일 스트링 상수를 생성해 낸다. 예를 들어, 다음과 같이 매크로를 정의했다고 하자. 매크로를 그 다음과 같이 쓴다.
#define str(x) # x
str (testing)
그러면 전처리기는 이를 다음과 같이 확장한다.
"testing"
printf (str (Programming in Objective-C is fun.\n));
따라서 이 printf 호출은 다음과 동일하다.
printf ("Programming in Objective-C is fun. \n");
전처리기는 실제 매크로 인수 주변에 따옴표를 추가한다. 인수에 따옴표나 백슬래시가 있다면 그대로 유지한다. 다음 명령문은 그 다음과 같은 결과를 생성한다.
str ("hello")
"\"hello\"
다음 매크로 정의에 # 연산자의 좀더 실용적인 예가 나와 있다. 이 매크로는 정수 변수의 값을 표시하는 데 사용된다. 만일 count가 값이 100 인 정수 변수라고 해보자.
#define printint(var) printf (# var "= %i\n", var)
printint (count);
이 명령문은 다음과 같이 확장된다.
printf ("count"" = %i\n" , count);
컴파일러는 인접한 스트링 상수 두 개를 한 스트링으로 합친다. 따라서, 인접한 두 스트링을 합치고 나면 위의 명령문은 다음과 같아진다.
printf ("count = %i \ n" , count);
##(더블샵) 연산자
이제부터 알아볼 ## 연산자는 매크로 정의에서 토큰 두 개를 합치는 데 쓰인다. ## 는 매크로의 매개변수 이름 앞이나 뒤에 나온다. 전처 리기는 매크로가 호출될 때 제공되는 매크로의 실제 인수와 ## 뒤에 따라오는 (혹은 앞에 오는) 토큰을 받아 하나의 토큰을 생성한다.
한 예로, 변수가 x1 에서 x100 까지 있다고 하자. 정수 값 1부터 100까지를 인수로 받는 printx 라는 매크로를 만들어 해당하는 x 변수를 표시하자.
#define printx(n) printf ("%i\n", x ## n)
define 의 다음 부분은 ## 앞뒤에 나오는 토큰(여기서는 문자 x와 인수 n)을 사용해서 하나의 토큰을 만들겠다는 의미다.
x ## n
다음 호출을 보자. 이 매크로는그 다음과 같이 확장된다.
printx (20);
printf ("%i\n" , x20);
printx 매크로는 앞에서 정의한 printint 매크로를 사용하여 변수 이름을 가져오고 값을 표시할 수 있다.
#define printx(n) printint(x ## n)
다음 매크로 호출을 보자.
printx (10);
이는 먼저,다음과 같이 확장된다. 그 후 다음과 같이 확장된다.
printint (x10);
printf ("x10" "= %i \n" , x10);
마지막으로 다음과 같아진다.
printf ( "x10 = %i\n" , x10);
#import 명령문
Objective-C 로 프로그래밍한지 꽤 되었다면,자신만의 매크로 세트를 개발하여 각 프로그램에서 사용할 것이다. 이 매크로들을 각 프로그램에 입력하는대신, 전처리기는 다른파일에 있는 정의를 각각 #import 문을 사용하여 프로그램에 포함시킨다. 이 파일들은 (앞에서 사용했지만 직접 만들지 않았던 것들과 유사하게) 일반적으로 .h 로 끝나고 '헤더' 혹은 '인클루드' 파일이라고 부른다.
다양한 단위 변환을 수행하는 프로그램을 작성한다고 해보자. 변환에 사용할 다양한 상수를 위한 #define 문들을 정의하고 싶을 것이다.
#define INCHES_PER_CENTIMETER 0.394
#define CENTIMETERS_PER_INCH (1 / INCHES_PER_CENTIMETER)
#define QUARTS_PER_LITER 1.057
#define LITERS_PER_QUART (1 / QUARTS_PER_LITER)
#define OUNCES_PER_GRAM 0.035
#define GRAMS_PER_OUNCE (1 / OUNCES_PER_GRAM)
...
이 코드를 metric.h 라는 파일에 따로 입력해 두었다고 하자. 어느 프로그램이든 metric.h 에 포함된 정의가 필요하다면 다음 전처리 지시어를 사용하면 된다.
#import "metric.h"
이 명령문은 metric.h에 정의된 #define 문을 참조하기 전에 나타나야 하고, 보통은 소스파일의 맨 처음에 자리 잡는다. 전처리기는 시스템에서 지시한 파일을 찾는다. 그리고 파일의 내용물을 이 #import 명령문이 있는 곳에 복사해 넣는다. 따라서, 이 파일에 포함된 명령문들은 모두 그 지점에 현재 프로그램에 직접 입력해 넣은 것과 동일하게 취급된다.
헤더파일 이름 양쪽의 따옴표는 전처리기에게 지정한 파일을 하나 혹은 그 이상인 파일 디렉터리에서 찾으라는 지시다(보통, 소스파일이 담겨 있는 디렉터리를 먼저 찾는데, 전처리기가 찾는 실제 위치는 Xcode 의 프로젝트 설정에서 수정할 수 있다).
다음과 같이 파일명을 < > 로 감싸도 된다.
#import <Foundation/Foundation.h>
이렇게 하면 전처리기가 지정된 파일을 특별한 '시스템' 혜더 파일 디렉터리에서 찾지만 현재 소스의 디렉터리는 검색하지 않는다. 다시 말하지만, Xcode 의 메뉴에서 '프로젝트', '프로젝트 설정펀집' 을 선택하여 이 디렉터리를 바꿀 수 있다.
이 책에 나오는 이 부분의 예제 프로그램을 컴파일할 때 Foundation.h 혜더파일은, 내 시스템 에서는 다음 디렉터리에서 임포트 되었다. /Developers/SDKs/MacOSX10.5.sdk/System/Library/Frameworks/Foundation.framework/Versions/C/Headers |
실제 프로그램에서 인클루드 파일이 어떻게 사용되는지 예제를 살펴보자. 앞서 본 #define 문 여섯 개를 metric.h 파일에 입력하자. 그리고 프로그램 12.1 을 일반적인 방식으로 입력하고 실행하자.
프로그램 12.1
/* #import 문의 사용 예
주의: 이 프로그램은 metric.h 파일에 필요한 #define이
정의되어 있다고 가정한다. */
#import <Foundation/Foundation.h>
#import "metric.h"
int main (int argc, char *argv[])
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
float liters, gallons;
NSLog (@"*** Liters to Gallons ***");
NSLog (@"Enter the number of liters:");
scanf ("%f", &liters);
gallons = liters * QUARTS_PER_LITER / 4.0;
NSLog (@"%g liters = %g gallons", liters, gallons);
[pool drain];
return 0;
}
프로그램 12.1 의 출력결과
*** Liters to Gallons ***
Enter the number of liters:
55.75
55.75 liters = 14.7319 gallons.
프로그램 12.1은 metric.h 에 포함된 단 하나의 정의된 값(QUARTS_PER_LITES)만 참조하여 보여 주기 때문에 다소 간단하다. 그렇지만 말하고자 하는 바는 잘 전달해 준다. define 이 metric.h 에 입력된 후에는 적절한 #import 문을 사용하는 프로그램이면 어디서든 define 을 사용할 수 있다.
파일 임포트 기능이 지닌 좋은 점 중하나는 정의를 한 곳에 모으면서도 동일한 값을 모든 프로그램이 참조할 수 있다는 것이다. 게다가 인클루드 파일의 값중 잘못된 부분이 하나라도 발견되면 이 값을 사용하는 모든 프로그램을 수정할 필요없이 한 곳만 수정하면 된다. 부정확한 값을 참조했던 프로그램들은 그저 재컴파일만 해주면 된다.
다른 시스템 인클루드 파일은 하부 C 시스템 라이브러리에 저장된 다양한 함수들의 선언을 담고 있다. 예를 들어 limits.h 파일은 다양한 문자와 정수 데이터 형의 크기를 지정하는 값을 포함하는데, 이 값은 시스템에 따라다르다. 예컨대, int 의 최대 크기는 이 파일에 INT_MAX 라는 이름으로 정의되어 있다. unsigned long int 의 최대 크기는 ULONG_MAX 에 저장되어 있고 나머지 데이터 형의 크기도 마찬가지다.
float.h 헤더파일은 부동소수점 데이터 형에 대한 정보를 제공한다. 예를들어, FLT_MAX 는 최대 부동소수를 지정하고, FLT_DIG 는float 형의 소수점 정확도 수를 지정한다.
string.h 파일은 복사, 비교, 연결 같은 문자 스트링 작업을 수행하는 라이브러리 루틴의 선언을 포함한다. 만일 Foundation 스트링 클래스(15장 「숫자, 스트링, 컬렉션」에서 다룬다)만 사용한다면, 프로그램에서 이 루틴들은 전혀 사용할 필요가 없다.
조건컴파일
Objective-C 전처리기는 '조건 컴파일' 이라는 기능을 제공한다. 조건 컴파일은 보통 각기 다른 컴퓨터 시스템 환경에서 돌아가도록 컴파일할 수 있는 하나의 프로그램을 만들 때 사용된다. 또한, 프로그램에서 다양한 명령문을 꼈다 켜는 경우에도 사용된다. 한 예로, 변수의 값을 출력하거나 프로그램 실행의 흐름을 추적하는 디버깅 명령문을 들수 있다.
#ifdef, #endif, #else, #ifndef 문
불행히도, 이따금 프로그램은 시스템에 따른 특정 매개변수에 의존해야 하는 때가 있다. 이 매개변수는 다른 프로세서(예를 들어 powerPC 와 Intel)와 특정 버전의 운영체제(예를 들어 Tiger 와 Leopard)에 따라 다르게 지정되어야 한다.
(가능하면 이런 상황은 최소화해야 하지만)만일 특정 하드웨어나 소프트웨어에 많이 의존하는 큰 프로그램이 있다면, 프로그램을 다른 컴퓨터 시스템으로 옮길 때 값이 바뀌어야하는 define 이 많을것이다.
전처리기의 조건 컴파일 기능을 사용하면 프로그램을 다른 시스템으로 옮겨갈 때마다 이 define의 값을 변경해 줘야 하는 문제를 줄일 수 있고각각 다른 시스템에 따르는 이들 define 값을 프로그램에 포함시킬 수 있다. 간단한 예로, 다음 명령문은 MAC_OS_X 심벌이 정의되어 있는 경우에는 DATADIR 를 '/uxn1/data' 로 정의하고 그 외에는 '\usr\data'로 정의한다.
#ifdef MAC_OS_X
# define DATADIR "/uxn1/data"
#else
# define DATADIR "\usr\data"
#endif
이 코드에서 확인할 수 있듯이, 전처리기 명령문을 시작하는 # 다음에는 원하는만큼 빈칸을 넣어도 된다.
- ifdef, #else, #endif 명령문은 예상대로 동작한다 #ifdef 에 지시한 심벌이(#define 문이나 프로그램이 컴파일될 때 커맨드라인을 통해) 이미 정의되어 있다면 컴파일러는 #else, #elif, #endif 가 나을 때까지만 코드를 처리하고, 그 외의 경우에는 무시한다.
POWER_PC 심벌을 전처리기에 정의하려면 다음 명령문을 사용한다.
#define POWER_PC 1
혹은 그냥 다음 명령문으로도 충분하다.
#define POWER_PC
위 코드에서 알수있듯이 정의된 이름 뒤에 아무런 텍스트가 없어도 #ifdef 테스트를 통과한다. 컴파일러는 프로그램이 컴파일될 때, 컴파일러 명령에 특별한 옵션을 주어 전처리기에 이름을 정의해줄 수도 있다. 다음 커맨드라인을 보자.
localhost # gcc -framework Foundation -0 POWER_PC program.m -
이 커맨드라인은 전처리기에 POWER_PC 라는 이름을 정의하여 program.m 안의 모든 #ifdef POWER_PC 명령문이 참(true)으로 평가되게 한다(커맨드라인에서 프로그램 이름 앞에 -DPOWER_PC 를 입력해야 함에 주의하라). 이 기법을 사용하면 소스코드를 수정하지 않고 이름을 정의할 수 있다.
Xcode의 '프로젝트 설정'에서 Add User Defined Setting 을 선택하여 새로 정의된 이름과 값을 설정해줄 수 있다.
- ifndef 문은 #ifdef 와 유사하게 사용되는데, 지정한 심볼이 정의되지 않았을 때만 따라나오는 코드들을 처리한다.
이미 언급했듯이, 조건 컴파일은 프로그램을 디버깅할 때 유용하다. 증가 결과를 표시하고 실행 흐름을 따라가기 위해 프로그램에서 NSLog 문을 많이 사용하게 될 것이다. 이런 명령문들을 DEBUG 와 같이 특정 이름이 정의되어 있을 때만 조건 컴파일을 할 수 있다. 예를 들어 다음 명령문들을 사용하면 프로그램에 DEBUG 라는 이름이 정의되었을 때만 특정 변수의 값을 표시하도록할 수 있다.
#ifdef DEBUG
NSLog (@"User name = %5, id = %i" , userName , userId);
#endif
프로그램 전반에 이런 디버깅 명령어들이 많이 있다면, 프로그램을 디버그할 때만 DEBUG 를 정의하여 모든 디버깅 명령문을 컴파일할수 있다. 프로그램이 정상 동작하면 DEBUG 를 정의하지 않은채로 재컴파일하면 된다. 이렇게 하면 디버깅 명령문들이 컴파일 되지 않으므로 프로그램 크기도 줄어드는 효과가 있다.
#if 와 #elif 전처리 명령문
- if 전처리 명령문은 조건 컴파일을 조작하는, 한층 일반적인 방법을제공한다. #if 문은 상수 표현식이 0 이 아닌지를 테스트 하는데 사용된다. 만일 표현식의 결과가 0 이 아니라면, 그 후부터 #else 나 #elif, #endif 가 나을 때까지만 코드를 처리하고, 그렇지 않은 경우에는 이 코드들은 무시된다.
이것들을 사용하는 예로, Foundation 의 혜더파일 NSString.h 에 있는 다음 코드를 보자.
#if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5
#define NSMaximumStringLength (INT_MAX-1)
#endif
이 테스트는 정의된 변수 MAC_OS_X_VESION_MIN_REQUIRED 의 값을 정의된 변수 MAC_OS_X_VERSION_10_5 와 비교한다. 만일 전자가 후자보다 작다면, 뒤따르는 #define 이 처리된다. 그 외의 경우에는 뒤따르는 #define 을 그냥 건너뛴다. 아마도 이 코드는 프로그램이 Mac OS X 10.5 이후 버전에서 컴파일되는 경우, 스트링의 최대 길이를 정수 최대 크기 -1 로 설정해 줄 것이다.
다음 특별 연산자를 #if에서 사용할 수 있다.
defined (name)
이 연산자와 #if 문을 함께 사용하면 #ifdef 와 동일한 기능을 한다.
#if defined (DEBUG)
...
#endif
#ifdef DEBUG
...
#endif
NSObjcRuntime.h 파일에 나타나는 다음 명령문은 사용되는 컴파일러에 맞춰 NS_INLINE 를 정의한다(물론 아직 정의되지 않았을 경우에만 그렇다).
#if !defined(NS_INLINE)
#if defined(__GNUC__)
#define NS_INLINE static __inline_attribute_((always_inline))
#elif defined(__MWERKS__) || defined(__cplusplus)
#define NS_INLINE static inline
#elif defined(_MSC_VER)
#define NS_INLINE static __inline
#elif defined(__WIN32__)
#define NS_INLINE static __inline__
#endif
#endif
다음과 같은 형태의 코드시퀀스 에서도 #if 가 자주 사용된다.
#if defined (DEBUG) && DEBUG
...
#endif
이 코드는 DEBUG가 정의되어 있고 그 값이 0 이 아닐 때만, #if 와 #endif 사이의 명령문들을 실행하고자 할 경우 자주 사용된다.
#undef 명령문
때로는 정의한 이름을 취소할 필요가 있다. 이때에는 체ndef 명령문을 사용하면된다. 특정 이름에 대한 정의를 제거하려면 다음과 같은 코드를 작성한다.
#undef name
따라서 다음명령문은 POWER_PC 정의를제거한다.
#undef POWER_PC
그러면 이후에 나오는 #ifdef POWER_PC 나 #if defined (POWER_PC) 명령문은 FALSE 가 된다.
이것으로 전처리기에 대한 이야기는 마치기로 하자. 여기서 다루지 않은 전처리 명령문에 대해서는 부록 B 「Objective-C 2.0 언어 요약」에서 설명한다.
연습문제
1. 컴퓨터에서 시스템 혜더파일 limit.h 와 float.h 의 위치가 어디인지 찾아보라. 이 파일들에 무엇이 들어 있는지 살펴보라. 만일 다른 혜더파일을 담고 있다면 그 파일들도 찾아서 내용을 살펴보라.
2. 두값의 최솟값을 돌려주는 매크로 MIN 을 정의하고 이 매크로 정의를 테스트할 프로그램을 작성하라.
3. 세 값의 최댓값을 돌려주는 매크로 MAX3 를 정의하고 이 매크로 정의를 테스트할 프로그램을 작성하라.
4. 문자가 대문자일 경우, 0 이 아닌 값을 돌려주는 IS_UPPER_CASE 매크로를 작성하라.
5. 문자가 알파벳일 경우, 0 이 아닌 값을 돌려주는 IS_AIPHABETIC 매크로를 작성하라. 이 장에서 정의한 IS_LOWE_CASE 와 연습문제 4번에서 정의한 IS_UPPER_CASE 매크로를 사용하는 매크로를 만들자.
6. 문자가 0 부터 9 까지에 있는 숫자라면, 0 이 아닌 값을 돌려주는 IS_DIGIT 를 정의하라. 이 매크로를, 문자가 특수문자(알파벳이나 숫자가 아닌 문자)일 때 0 이 아닌 값을 돌려주는 매크로인 IS_SPECIAL 를 정의할 때 사용하라. 연습문제 5번에서 개발한 IS_AIPHABETIC 매크로도 사용하라.
7. 인수의 절대값을 계산하는 ABSOLUTE_VALUE 를 작성하라. 다음과 같은 표현식을 정상 평가하는 매크로를 만들라.
ABSOLUTE_VALUE (x + delta)
8. 이 장에 나온 printint 매크로의 정의를 살펴보라.
#define printx(n) printf ("%i\n", x ## n)
다음 코드를 사용하여 x1 부터 x100 까지 변수 100 개의 값을 표시할 수 있을까? 가능하다면 그 이유는 무엇인가?
for ( i = 1; i <= 100; ++i )
printx (i);