ProgrammingInObjectiveC:Chapter 10

From 흡혈양파의 번역工房
Jump to navigation Jump to search
10장
변수와 데이터 형에 대하여

10장 :: 변수와 데이터 형에 대하여

이 장에서는 변수의 범위, 객체의 초기화 메서드, 데이터 형에 대해 더 상세히 다룬다. 객체의 초기화 메서드는 특히 더 자세히 살펴본다.

7장 「클래스에 대해서」에서 인스턴스 변수의 범위와 정적 변수, 지역 변수를 간략하게 설명했다. 여기서는 정적 변수에 대해 더 상세히 설명하고,전역 변수와 외부 변수를 소개한다. 또한 Objective-C 컴파일러의 지시어를 사용하여 인스턴스 변수의 범위를 더 정확히 조작할 수 있는데 이런 지시어들도 이 장에서 다룰것이다.

'열거(enumerated)' 데이터 형을 이용하면 특정 값의 목록을 저장하는 데만 사용하는 데이터 형의 이름을 정의할 수 있다. Objective-C 의 typedef 문은 원래 있던 데이터 형이나, 파생된 데이터 형에 자신만의 이름을 부여한다. 이 장의 마지막에 이르면 Objective-C 컴파일러가 표현식을 평가할 때 데이터 형을 변환하면서 따르는 과정을 정확한 단계로 나눠 더 상세히 설명할 것이다.


클래스 초기화하기

초기화 패턴은 이미 살펴보았다. 다음과 같은 코드를 작성해 객체의 새 인스턴스를 생성하고 초기화한다.

Fraction *myFract = [[Fraction alloc] init];


두 메서드가 호출된 뒤, 보통 다음과같이 새 객체에 값을대입한다.

[myFract setTo: 1 over: 3];


객체를 초기화하고 나서 초기값을 설정하는데, 보통 한 메서드에서 이 두 작업을 동시에 수행한다. 예를 들어, initWith:: 메서드를 정의하여 분수를 초기화하고(이름이 붙지 않은) 두 인수를 제공해 분자 분모의 값을 설정할 수 있다.

많은 메서드와 인스턴스 변수가 담긴 클래스는 대개 초기화 메서드도 여러개다. 예컨대 Foundation 프레임워크의 NSArray 클래스는 초기화 메서드가 여섯개다.

initWithArray:
initWithArray:copyItems:
initWithContentsOfFile:
initWithContentsOfURL:
initWithObjects:
initWithObjects:count:


다음과 같은 코드로 배열을 생성하고 초기화할 수 있다.

myArray = [[ NSArray alloc] initWithArray: myOtherArray];


클래스에서 초기화 메서드의 이름은 보통 init... 으로 시작한다. 위에서 보듯이 NSArray 의 초기화 메서드도 이 규칙을 따른다. 초기화 메서드를 작성할 때는 다음 두가지 원칙을 따라야 한다.

만일 클래스가 초기화 메서드를 하나 이상 갖는다면, 그 가운데 하나는 '지정된 초기화 메서드'여야 하고, 다른 메서드는 모두 이 초기화 메서드를 사용해야 한다. 지정된 초기화 메서드는 대개 가장 복잡한 초기화 메서드, 즉 가장 많은 인수를 받는 메서드다. 지정된 초기화 메서드를 만들면 주요 초기화 코드를 단일한 메서드에 집중시킬 수 있다. 여러분의 클래스에 대한 서브클래스를 만드는 사람은 모두 지정된 초기화 메서드를 재정의해야 하며, 그렇게 함으로써 새 인스턴스를 적절히 초기화할 수 있다.

상속받은 인스턴스 변수를 적절히 초기화해 주어야 한다. 가장 쉽게는 먼저 부모의 지정된 초기화 메서드(보통 init이다)를 호출해 주는 방법이 있다. 그 후 새로 정의한 인스턴스 변수를 초기화해 주면 된다.

이러한 내용을 바탕으로 보건대, Fraction 클래스의 초기화 메서드 initWith:: 는 다음과 비슷한 형태일 것이다.

-(Fraction *) initWith: (int) n: (int) d
{
    self = [super init];

    if (self)
        [self setTo: n over: d];

    return self;
}


이 메서드는 부모의 초기화 메서드인 NSObject 의 init 메서드를 먼저 호출한다(NSObject 가 Fraction 의 부모임을 기억하라). 초기화 메서드는 메모리 상의 객체를 바꾸거나 이동시킬 권한이 있기 때문에, 반환 값을 seif 에 할당해 주어야 한다.

super 를 초기화한 후에 (그리고 반환 값이 0 이 아니어서 초기화가 성공한 경우) setTo:over: 메서드를 사용하여 Fraction 의 분자와 분모를 설정해 준다. 다른 초기화 메서드와 마찬가지로, 초기화된 객체를 반환해 주어야한다.


프로그램 10.1 은 initWith:: 초기화 메서드를 테스트한다.


프로그램 10.1


#import "Fraction.h"

int main (int argc, char *argv[])
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    Fraction *a, *b;

    a = [[Fraction alloc] initWith: 1: 3];
    b = [[Fraction alloc] initWith: 3: 7];

    [a print];
    [b print];

    [a release];
    [b release];

    [pool drain];
    return 0;
}

프로그램 10.1 의 출력결과


1/3
3/7


실행이 시작될 때 프로그램은 모든 클래스에 initialize 메서드를 보낸다. 만일 어떤 클래스와 그에 관련된 서브클래스가 있다면 부모 클래스가 먼저 이 메시지를 받는다. 이 메시지는 클래스마다 단 한번 호출되고 클래스로 보내는 다른 어떤 메시지보다 먼저 보내진다. 이 메서드를 보내는 목적은 초기화가 필요한 클래스를 이 시점에서 처리하려는 것이다. 예를 들어, 이때 클래스의 정적 변수들을 초기화할 수 있다.


범위 다시살펴보기

프로그램에서 변수의 범위를 조절하는 방법은 여러 가지다. 함수 내부와 외부에 선언된 일반 변수는 물론 인스턴스 변수까지 모두 범위를 조절할 수 있다. 이제부터는 '모듈' 이라는 용어를 사용하여 한 소스파일에 포함된 메서드나 함수정의에 대해 언급할 것이다.


인스턴스 변수의 범위롤 조절하는 지시어

이제 인스턴스변수의 범위가 클래스에 정의된 인스턴스 메서드에 의해 제한된다는 것을 안다. 따라서 어느 인스턴스 메서드든 특별한 방법을 사용하지 않고 인스턴스변수에 이름으로 직접 접근할 수 있다.

또한, 서브클래스를 통해 상속받은 인스턴스 변수가 있음도 알 것이다. 서브클래스에 정의된 메서드도 상속받은 인스턴스 변수에 이름으로 직접 접근할 수 있다. 다시 한 번 말하지만, 특별히 다른 무언가를 해주지 않아도된다.


인터페이스 부분에서 인스턴스 변수를 선언할 때 선언 앞에 네 가지 지시어를 붙임으로써 범위를 더 상세히 설정할 수 있다. 네 가지 지시어를 살펴보자.

  • @protected - 어떤 클래스에서 인스턴스 변수가 정의되었을 때, 그 클래스와 그 서브클래스에 정의된 메서드는 이 인스턴스 변수에 바로 접근할 수 있다. 이것이 기본값이다 .
  • @private - 클래스에 정의된 메서드는 인스턴스 변수에 바로 접근할 수 있지만, 서브클래스의 메서드는 바로 접근할 수 없다.
  • @public - 인스턴스 변수가 정의된 클래스와 그 밖의 클래스 그리고 모듈에 정의된 메서드라면, 인스턴스 변수에 바로 접근할 수 있다(어디서든 인스턴스 변수에 접근할 수 있다) .
  • @package - 64비트 이미지의 경우, 그 클래스를 구현하는 이미지 안에서는 어디든 인스턴스 변수에 접근할 수 있다.


만약 Printer 클래스에 정의된 pageCount 와 tonerLevel 을 private 변수로 만들고 Printer 클래스에 있는 메서드만 접근할 수 있게 하려면 어떻게 해야 할까? 다음과 같이 인터페이스 부분을 작성하면 된다.

@interface Printer: NSObject
{
    @private
        int pageCount;
        int tonerLevel;
    @protected
        // 다른 인스턴스 변수
}
    ...
@end


Printer 의 서브클래스를 만들면, private 로 만든 두 인스턴스 변수에는 접근할 수없다.

이 특별한 지시어들은 '스위치' 처럼 동작한다. 이 지시어 다음에 나오는 모든 변수는(변수선언이 끝났음을 의미하는중괄호 '}' 가 나오기 전까지는) 이후 다른 지시어가 등장하지 않는 한,이 지시어가 나타내는 범위를 갖는다. 이전 예제에서 @protected 지시어는 중괄호 전까지 등장한 인스턴스 변수가 Printer 클래스와 그의 서브클래스에서만 접근할 수 있도록 설정한다.

@public 지시어는 포인터 연산자(->)를 써서 인스턴스 변수를 다른 메서드나 함수에서 접근하도록 만든다. 이 포인터 연산자는 13장 「하부 C 언어 기능」에서 다룬다. 인스턴스 변수를 public 으로 만들면 데이터 캡슐화(클래스가 자신의 인스턴스 변수를 숨기는 것)를 하지 못하기 때문에, 좋은 프로그래밍 습관으로 여겨지지 않는다.


외부변수

만일 다음 명령문을 프로그램 초반에(메서드와 클래스 정의 그리고 함수 바깥에서) 작성했다면 이 값은 그 모듈 어디서든 참조할수 있다.

int gMoveNumber = 0;


이 경우에 gMoveNumber 가 '전역(global)' 변수로 정의되었다고 말한다. 명명규칙(convention)을 따르면 보통 전역 변수의 첫 글자로 g가 쓰인다. 이를 통해 프로그램 코드를 읽는 사람은 변수의 범위를 알게 된다.

사실, 변수 gMoveNumber 를 이렇게 정의하면 다른 파일에서도 이 값에 접근할 수 있다. 특히 앞의 명령문은 변수 gMoveNumber 를 global 변수만이 아닌 '외부' global 변수로 정의한다.

'외부' 변수는 그 값을 다른 메서드나 함수에서 접근하고 또 바꿀 수도 있는 변수다. 외부 변수에 접근하고 싶은 모듈에서 일반적인 선언 방식과 동일하게 변수를 선언한 다음 그 앞에 extern 키워드를추가한다. 이것은 시스템에게 보내는 일종의 신호다. 이렇게 하면 시스템은 다른파일에서 전역으로 정의된 변수에 접근해야 한다는 걸 알게 된다. 다음은 gMoveNumber 를 외부 변수로 선언하는 방법을 다룬 예제다.

extern int gMoveNumber;


이 선언이 나오는 모듈은 이제 gMoveNumber 에 접근하고 값을 수정할 수 있다. 다른 모듈도 파일에 비슷한 방식으로 extern 을 선언하여 gMoveNumber 의 값에 접근할 수 있다.

외부 변수를 다룰 때는 한 가지 중요한 규칙을따라야 한다. 즉, 변수는 소스파일의 어딘가에 정의되어 있어야만 한다. 다시 말해서,다음과 같이 변수를 메서드나 함수 바깥에 extern 키워드를 붙이지 않고 선언해야 한다는 얘기다.

int gMoveNumber;


여기서, 초기값은 이전에 했던 것처럼 선택적으로 할당해줄 수 있다. 이제 외부변수를 정의하는 두 번째 방법을 보자. 바로 함수 바깥 어디서든 extern 키워드를 앞에 붙이고 다음과 같이 명시적으로 초기값을 할당해 주는 것이다.

extern int gMoveNumber = 0;


그러나 이 방식은 별로 선호되지 않는 데다가, 컴파일러 역시 extern 변수를 선언함과 동시에 값을 할당했다고 경고를 표시할 것이다. 그 까닭은, extern 을 사용하면 변수를 '선언' 할 뿐 '정의' 하는 것이 아니기 때문이다. 선언은 할당될 변수를 저장할 공간을 만들지 않지만, 정의에서는 저장 공간을 만든다는 점을 기억하자. 이 예제는 선언을 (초기값을 할당함으로써) 정의로 강제로 취급하여 이 규칙을 위반한다.

외부 변수를 다룰 때, 여러 곳에서 extern 으로 변수를 선언할 수는 있지만 정의는 딱 한번 해야 한다.

작은 프로그램 예제를 통해 어떻게 외부 변수를 사용하는지 살펴보자'. foo 라는 클래스를 정의하고 main.m 파일에 다음 코드를 입력했다고 하자.

#import "Foo.h"

int gGlobalVar = 5;

int main (int argc, char *argc[])
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    Foo *myFoo = [[Foo alloc] init];
    NSLog (@"%i ", gGlobalVar);

    [myFoo setgGlobalVar: 100]
    NSLog (@"%i", gGlobalVar);
    [myFoo release];
    [pool drain];
    return 0;
}


이 프로그램에서 전역변수 gGlovalVar 를 정의하자. 이제 적절하게 extern 을 선언하는 메서드 혹은 함수라면 어디서든 이 변수 값에 접근 가능해졌다. Foo 의 메서드인 setgGlovalVar: 가 다음과 같다고 해보자.

-(void) setgGlobalVar: (int) val
{
    extern int gGlobalVar;
    gGlobalVar = val;
}


이 프로그램은다음과 같은 결과가나을 것이다.

5
100


이 결과로 setgGlovalVar: 메서드가 외부 변수 gGlovalVar의 값에 접근하고 수정하는 것이 가능함을 확인할 수 있다.

gGlovalVar 에 접근해야 하는 메서드가 많은 경우, extern 선언을 파일 앞부분에 한 번만 해주면 더 편리하게 사용할 수 있다. 그러나 이 변수에 접근해야 하는 메서드가 단 하나이거나 적은수라면, 그 메서드 각각에 extern 선언을 해주는 편이 더 좋다. 그러면 프로그램이 좀더 정리되고 특정 변수를 실제로 사용하는 함수만 따로 분리해 주는 효과가 있다. 변수에 접근하는 코드가 담긴 파일 내에서 변수가 정의되었다면, extern을 일일이 선언할 필요는 없다.


정적변수

사실 방금 본 예제는 데이터 캡슐화와 좋은 객체지향 프로그래밍 기법에 어긋난다. 그러나 다른 메서드 호출에서 값을 공유하는 변수를 다뤄야 할 때가 있다. 비록 gGlovalVar는 Foo 클래스의 인스턴스 변수가 되기에는 합당하지 않겠지만, Foo 클래스에 정의된 게터와 세터 메서드를 제한하여 이 변수를 '감추는' 방법이 좀 더 나을수 있다.

앞서 메서드 외부에서 정의된변수는 전역 변수만이 아니라 외부변수도 된다고 이미 말했다. 그러나 전역 변수이면서도 외부 변수는 되지 않기를 원하는 경우가 많다. 다시 말하면, 특정 모듈(파일)에서는 지역 변수이면서 전역으로 변수를 정의하고 싶을 때가 있다는 이야기다. 만일, 특정 클래스 정의에 포함된 메서드를 제외하고는 특정 변수에 접근할 필요가 없다면, 이런 식으로 변수를 정의해야 합당하다. 파일 내에 특정 클래스를 구현히는 부분이 있다면 파일 안에에 변수를 정적으로 정의하여 이를 달성할 수 있다.

만일 메서드 (혹은 함수) 바깥에서 다음 명령문을 사용하면, 이 정의가 나오는 파일 안에, 명령문 다음에 등장하는 모든 지점에서는 gGlovalVar 의 값을 접근할 수 있다. 그러나 다른 파일에 포함된 메서드와 함수에서는 접근할 수 없게 된다.

static int gGlobalVar = 0;


클래스 메서드는 인스턴스 변수에 접근할 수 없음을 기억하자(여기서도 역시 마찬가지다). 그러나 클래스 메서드가 변수에 접근하고값을 설정해야 할 때도 있다. 간단한 예로 생성한 객체의 개수를 세는 클래스 할당 메서드를 생각해 보자. 클래스의 구현 파일 내에 정적 변수를 설정하여 이 작업을 수행할 수 있다. 할당 메서드는 인스턴스 변수에 접근할 수 없으므로 이 정적 변수에 직접 접근하여 값을 설정한다. 클래스의 사용자는 이 변수에 대해서 무언가 알고 있을 필요가 없다. 그 이유는 구현 파일에 정적 변수로 정의되어서, 변수의 범위가 그 파일로 제한되기 때문이다. 따라서, 사용자는 이 변수에 직접 접근할 필요가 없고, 데이터 캡슐화 개념을 위반하지도 않는다. 만일 클래스 바깥에서 접근할 필요가 있다면 변수의 값을 받아오는 메서드를 작성하면 된다.

프로그램 10.2 는 Fraction 클래스 정의를 확장하여 새 메서드를 두 개 추가하였다 allocF 클래스 메서드는 새로운 Fraction 을 생성하고 생성된 Fraction의 숫자를 추적한다. 그리고 count 메서드는 그 숫자를 반환한다. 두 메서드 모두 클래스 메서드임에 주의하자. count 메서드는 인스턴스 메서드로 만들어도 되지만, 생성된 인스턴스의 개수를 묻는 메시지를 인스턴스 보다는 클래스에 보내는 편이 더 합당하다.

다음은 Fraction.h 헤더파일에 추가할 새 클래스 메서드를 선언한 것이다.

+(Fraction *) allocF;
+(int) count;


상속받은 alloc 메서드를 재정의하는 대신 할당 메서드를 새로 정의하였다. 새로 정의한 메서드에서 alloc 메서드를 상속받아 활용할 것이다. Fraction.m 구현 파일에 다음 코드를 추가하자.

static int gCounter;

@implementation Fraction

+(Fraction *) allocF
{
    extern int gCounter;
    ++gCounter;

    return [Fraction alloc];
}

+(int) count
{
    extern int gCounter;

    return gCounter;
}

// Fraction 클래스의 다른 메서드
    ...
@end


objc2_notice_01
alloc 은 물리적인 메모리 할당을 다루는 메서드이기 때문에, 이를 재정의하는 것은 좋지 않은 프로그래밍 습관이다. 이 단계에서는 작업에 관여하면 안된다.


gCounter 를 정적 변수로 선언할 경우, 구현부에 정의된 메서드 내에서라면 어디서든 접근 가능한 반면, 파일 바깥에서는 접근할 수 없다. allocF 메서드는 그저 gCounter 변수의 값을 증가시킨 후, alloc 메서드를 사용하여 새 Fraction 을 만들고 결과를 반환한다. count 메서드는 카운터의 값을 반환해주어 사용자가 직접 값에 접근하지 않도록 해준다.

gCounter 가 동일한 파일에 정의되어 있기 때문에 두 메서드에서 extern 을 선언해 줄 필요는 없다. 선언하더라도 메서드를 읽는 사람이 메서드 바깥에서 변수가 정의되었다는 걸 좀더 빨리 이해할 뿐이다. 마찬가지로 변수 이름의 맨 앞에 있는 g 도 코드를 읽는 사람을 배려한 것이다. 이런 이유로 extern 선언을 넣지 않는 프로그래머들이 많다.


프로그램 10.2는 새 메서드를 테스트한다.


프로그램 10.2


#import "Fraction.h"

int main (int argc, char *argv[])
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    Fraction *a, *b, *c;

    NSLog (@"Fractions allocated: %i", [Fraction count]);

    a = [[Fraction allocF] init];
    b = [[Fraction allocF] init];
    c = [[Fraction allocF] init];

    NSLog (@"Fractions allocated: %i", [[Fraction count]);
    [a release];
    [b release];
    [c release];
    [pool drain];
    return 0;
}

프로그램 10.2 의 출력결과


Fractions allocated: 0
Fractions allocated: 3


프로그램이 실행되면, gCounter의 값은 자동으로 0 으로 설정된다(정적 변수의 값을 0이 아닌 다른값으로 설정하는 것처럼 클래스 전체에 특별한 초기화 작업을 수행하고 싶다면 상속받는 클래스 메서드인 initialize 를 재정의한다). allocF 메서드를 써서 Fraction 객체 세 개를 생성한 뒤(그리고 초기화한 뒤), count 메서드는 gCounter 변수의 값을 가져온다. 결과값은 정확하게 3이다. 비록 이 프로그램에서는 필요 없지만, 만일 카운터를 리셋하거나 특정 값으로 설정하고 싶다면 클래스에 세터 메서드를 추가할 수도 있다.


저장 클래스 식별자

앞에서 extern, static 과 같이, 변수 이름 앞에 쓰는 저장 클래스 식별자를 접했다. 여기서는 컴파일러에게 변수를 쓰는 의도에 대해 정보를 주는 식별자들을 다룰것이다.


auto

이 키워드는 static 과 반대되는 자동 지역 변수를 선언하는 데 쓴다. 함수나 메서드 내에서 변수를 선언하면 기본값이 지정된다. 아마도 이것을 직접 사용하는 경우는 거의 보지 못할 것이다.

auto int index;


위 선언으로 index는 자동 지역 변수가 된다. 이것은 블록(중괄호 안의 명령문들, 메서드, 함수 등)에 들어서면 자동으로 생성되고, 블록을 벗어나면 자동으로 해제된다는 의미다. 블록 안에서는 이 식별자가 기본이기 때문에 다음 두 명령문은 동일하다.

int index;

auto int index;


기본 값이 0인 정적 변수와 달리, 자동 변수는 값을 명시적으로 할당해 주지 않으면 값이 정의되지 않는다.


const

컴파일러는 프로그램에서 값이 변하지 않는 변수에 const 속성을 부여한다. 이 속성은 컴파일러에게 프로그램이 실행되는 동안 특정 변수가 '상수' 값을 갖는다고 알려준다. 만일 const 변수를 초기화 한 뒤 값을 설정하거나, 증가 혹은 감소시키려 하면 컴파일러는 경고 메시지를 보낼 것이다. const 속성을 사용한 예를 보자. 다음코드는 const 변수 pi 를 선언한다.

const double pi = 3.141592654;


이 코드는 컴파일러에게 프로그램이 이 값을 수정하지 않을 것이라고 알려 준다. 물론 const 변수의 값을 수정할 수 없으므로 정의할 때 바로 초기화해 주어야 한다.

또한 이렇게 const 변수로 정의하면, 코드 스스로 문서화되어 코드를 읽는 사람에게 프로그램이 해당 변수의 값을 변경하지 않는다는 것을 알릴 수 있다.


volatile

volatile 은 const 의 반대 역할을 한다. 즉, 컴파일러에게 특정 변수가 값이 바뀔 것이라고 명시적으로 알려 준다. 이런 기능이 생긴 까닭은 무엇일까? 이는 표면상 변수에 중복으로 할당하는 경우나, 값이 변하지 않는 것 같은데도 반복해서 값을 테스트하는 경우에 컴파일러가 최적화 과정에서 무시해 버 리지 않게 하기 위해서다. 이를 설명하는 좋은 예로 I/O 포트를 들 수 있다. I/O 포트의 예를 이해하려면 포인터를 이해해야 한다(13장을보라).

프로그램에서 출력 포트의 주소를 outPort 변수에 저장했다고 하자. 이 포트에 O 와 N 이라는 두 글자를 쓰려고 하면, 다음과 같은 코드를 작성할 것이다.

*outPort = 'O';
*outPort = 'N';


첫 줄은문자 O를 outPort 가 지시한 메모리 주소에 저장하라고 지시한다. 두 번째 줄은문자 N 을 동일한 위치에 저장하라는 지시다. 동일한 메모리 주소에 할당이 두 번 연속되는데 그 사이에 outPort 에는 아무런 수정을 가하지 않으므로 똑똑한 컴파일러는 첫 번째 할당을 제거해 버릴 것이다. 이런 일을 방지하려면 outPort 를 다음과 같이 volatile 로 선언해야 한다.

volatile char *outPort;


열거 데이터 형

Objective-C 에서는 변수에 할당 가능한 값의 범위를 지정할 수 있다. 열거 데이터형은 키워드 emun 을 붙여서 정의한다. 이 키워드 뒤에 바로 열거 데이터 형의 이름이 붙고 그 다음에는 (중괄호 안에) 대입 가능한 값들을 정의하는 식별자 목록이 나온다. 예를들어 다음 명령문으로는 flag 라는 데이터 형을 정의한다.

enum flag { FALSE, true };


이론적으로 이 flag 형은 true 와 FALSE 값만 할당할 수 있다. 안타깝게도 컴파일러는 이 규칙을 위반해도 경고 메시지를 주지 않는다.

enum flag 형 변수를 선언하려면 키워드 enum 과 열거형 이름 flag 를 쓴 다음 그뒤에 변수 목록을 적어 준다. 다음 명령문은 endOfData, matchFound를 flag형 변수가 되도록 정의한다.

enum flag endOfData, matchFound;


이 변수에 대입할 수 있는 값은 (이론적으로) true 와 FALSE 뿐이다. 따라서 다음과 같은 명령문은 유효하다.

endOfData = true;

if ( matchFound == FALSE )
    ...


혹시 열거형 식별자에 특정 정수 값을 연계해서 사용하고 싶을 경우에는, 데이터 형을 정의할 때 해당 식별자에 정수를 할당할 수 있다. 식별자에 특정 정수 값이 할당되었다면 다음에 나오는 열거형 식별자는 그 값에 1 씩 더한 값을 할당받는다.

다음은 열거 데이터 형 direction 을 up, down, left, right 라는 값과 함께 정의한 명령문이다.

enum direction { up, down , left = 10, right };


컴파일러는 목록의 맨 처음인 up 에는 0 을 할당하고 그 다음 등장하는 down 에는 1 을, 명시적으로 값을 할당한 left 에는 10 을 넣는다. 그 다음 right에는 이전의 enum 값에 1을 증가시킨 값 11이 들어간다.

열거 식별자는 동일한 값을 함께 사용할수도 있다. 다음 예를보자.

enum boolean { no = 0, FALSE = 0, yes = 1, true = 1 };


만일 enum boolean 변수에 값으로 no 나 FALSE 를 할당하면 변수 값을 0 으로 설정해 주는 셈이다. 반면 yes 나 true 는 값을 1 로 설정하는 격이다.

열거 데이터 형을 정의한 다른 예를보 자. 다음코드는 enum month 형을 정의하는데, 이 형의 변수에는 매월의 이름을 할당한다.

enum month { january = 1, february , march , april, may , june , july ,
     	   august, september, october, november, december };


Objective-C 컴파일러는 열거 식별자를 정수 상수로 여긴다. 만일 프로그램에 다음 두줄이 들어 있다면 thisMonth 의 값은 february라는 이름이 아니라 2가 된다.

enum month thisMonth;
...
thisMonth = february;



프로그램 10.3


#import <Foundation/Foundation.h>

// 한 달의 날짜 수를 출력한다.
int main (int argc, char *argv[])
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    enum month { january = 1, february, march, april, may, june, 
                july, august, september, october, november,
                december );
                
    enum month amonth;
    int days;

    NSLog (@"Enter month number: ");
    scanf ("%i", &amonth);

    switch (amonth) {
        case january:
        case march:
        case may:
        case july:
        case august:
        case october:
        case december:
            days = 31;
            break;
        case april:
        case june:
        case september:
        case november:
            days = 30;
            break;
        case february;
            days = 28;
            break;
        default:
            NSLog (@"bad month number");
            days = 0;
            break;
        }

    if ( days != 0 )
        NSLog (@"Number of days is %i", days);

    if ( amonth == february )
        NSLog (@"...or 29 if it's a leap year");

    [pool drain];
    return 0;
}

프로그램 10.3 의 출력결과


Enter month number:
5
Number of days is 31

프로그램 10.3 의 출력결과(재실행)


Enter month number:
2
Number of days is 28
...or 29 if it's a leap year


형 변환 연산자를 이용해서 열거 데이터 형 변수에 정수 값을 명시적으로 할당할 수도 있다. 따라서 monthValue 가 값이 6 인 정수 변수였다면, 다음 표현식을 써도 된다.

lastMonth = (enum month) (monthValue - 1);


혹시 형 변환 연산자를 쓰지 않더라도 (안타깝게도) 컴파일러가 이 에 대해 불평하지는 않는다.

열거 데이터 형을 사용할때는 열거형 값이 정수로 처리된다는 사실에 의존하지 말자. 대신, 별개의 데이터 형으로 다루자. 열거 데이터 형을 사용하면 심볼릭 이름에 정수 번호를 부여할 수 있다. 만일 나중에 그 숫자값을 바꿀 필요가 있다면, 열거형이 선언된 곳에서만 값을 바꿔야 한다. 열거 데이터 형의 실제값을 추측해야 한다면,이 데이터 형을 쓰는 이점이 없어지게 된다.

열거 데이터 형을 정의할 때는 몇가지 변형된 형식을 사용할 수도 있다. 데이터 형의 이름을 생략해도 되며, 특정 열거 데이터 형을정의하면서 변수를 동시에 선언해도 된다. 다음 명령문에서는 이 두가지 변형을 모두 보여 준다.

enum { east , west , south , north } direction;


위 코드는 (이름 없는) 열거 데이터 형을 정의하고, 가능한 값으로 east, west, south, north 를 설정한다. 그리고 열거 데이터 형의 변수(direction)을 선언한다.

특정 블록에서 열거 데이터 형을 정의했을 때는 블록 내에서만 정의가 유효하다. 반면, 프로그램의 맨 처음, 그리고 모든 블록 바깥에서 열거 데이터 형을 정의할 경우 해당 파일에서 전역으로 취급된다.

또한 열거 데이터 형은 동일한 범위에서 정의된 변수 이름이나 식별자와 겹치지 않도록 정의해야 한다.


typedef 명령문

Objective-C 는 데이터 형에 다른 이름을 부여하는 기능을 지원한다. typedef 는 명령문으로 이 기능을 쓸 수 있다. 다음 명령문은 Counter 라는 이름을 데이터 형인 int 와 동일하게 만들어 준다.

typedef int Counter;


그러고나서 다음 명령문처럼 Counter 형 변수를 선언할 수 있다.

Counter j, n;


Objective-C 컴파일러는 변수 j 와 n 의 선언을 일반 정수 변수로처 리한다. 이렇게 typedef 를 통해 얻는 주요 장점은 변수 선언에 가독성을 더 높여 준다는 것이다. j 와 n 이 정의된 부분을 보면 프로그램에서 이 변수들이 어떻게 사용될지를 더 분명하게 알게 된다. 전동적인 방식으로 int 형 변수를 선언했다면 이 변수들을 쓰는 목적이 분명히 드러나지 않았을 것이다.

다음 typedef 문을 보자 NumberObject 라는 형을 Number 객체가 되도록 한다.

typedef Number *NumberObject;


그런 다음 다음처럼 NumberObject 변수들을 선언한다.

NumberObject myValue1, myValue2, myResult;


이렇게 해도 다음 코드에서 일반적으로 변수를 선언했을 때와 동일하게 취급된다.

Number *myValue1, *myValue2, *myResult;


이렇듯 typedef 를써서 새로운이름의 데이터 형을 정의하려면 다음 과정을 따라야 한다.

  1. 원하는 형을 선언할때 처럼 명령문을 작성한다.
  2. 선언된 변수 이름이 나타나는 곳에 새로운 데이터 형의 이름을 대신 입력한다.
  3. 위에서 작성한모든 것 앞에 typedef 키워드를 입력한다.


예를 통해서 이 과정을 살펴보자. Direction 이라는 열거 데이터 형을 정의하여 east, west, north, south 의 값을 넣어 주고 변수 이름이 등장할 위치에 Direction 을 대신 집어넣는다. 그런 다음 모든 것 앞에 키워드 typedef 를 넣는다.

typedef enum { east , west , south , north } Direction;


이렇게 typedef 문을 작성하고 나면, 변수를 Direction 형으로 선언할 수 있다.

Direction step1 , step2;


Foundation 프레임워크는 헤더파일 가운데 하나에서 NSComparisonResult 에 대한 다음 typedef 정의를 포함한다.

typedef enum _NSComparisonResult {
    NSOrderedAscending = -1, NSOrderedSame, NSOrderedDescending
};

typedef NSInteger NSComparisonResult;


Foundation 프레임워크에서 비교를 수행하는 메서드들은 이 데이터 형의 값을 반환한다. 예를 들어, 스트링 비교메서드 compare: 의 경우 두 NSString 객체를 비교하여 NSComparisonResult 형의 값을 결과로 반환한다. 이 메서드는 다음과 같이 선언되어 있다.

-(NSComparisonResult) compare: (NSString *) string;


NSString 형 객체 userName 과 savedName 이 동일한지를 테스트해 보려면 다음 코드를 사용한다.

if ( [userName compare: saveName] == NSOrderedSame) {
    // 이름이 일치함
    ...
}


실제로 이 코드는 compare: 의 결과가 0 인지를 확인한다.


데이터 형 변환

4장 「데이터 형과 표현식」 에서 표현식을 평가할 때, 시스템이 이따끔 암묵적으로 형 변환을 수행한다는 사실을 잠깐 언급했다. 그러고서는 float 형, int 형의 데이터를 다루는 경우를 시험해 보았다. float 과 int 로 부동소수점 연산을 할 때, 정수형 데이터가 부동소수점 데이터로 자동 변환되는 것을 보았다.

또한 형 변환 연산자를 써서 명시적으로 변환하는 형태도 보았다. 다음 코드를 보자.

average = (float) total / n;


total 과 n 이 모두 정수 변수일 때, total 의 값은 연산이 수행되기 전에 float 형으로 변환되어 나눗셈이 부동소수점으로 연산된다.


변환규칙

Objective-C 컴파일러는 각기 다른 데이터 형으로 구성된 표현식을 평가할 때 매우 엄격한 규칙을 따른다.

다음은 표현식에서 두개의 피연산자를 평가하여 변환하는 순서를 요약한 것이다.

  1. 만일 두 항 중 하나가 long double 이라면 다른 항과 결과 값 모두 long double 형이 된다.
  2. 만일 두 항 중 하나가 double 이라면 다른 항과 결과값 모두 double 형이 된다.
  3. 만일 두 항 중 하나가 float 면 다른 항과 결과값 모두 float 형이 된다.
  4. 만일 두 항 중 하나가 _Bool, char, short int, 비트 필드[1] 혹은 열거 데이터 형이라면, int 형으로 변환된다.
  5. 만일 두 항 중 하나가 long long int 라면 다른 항과 결과 값 모두 long long int 형이 된다.
  6. 만일 두 항 중 하나가 long int 라면 다른 항과 결과값 모두 long int 형이 된다.
  7. 만일 이 단계까지 왔다면, 두 항모두 int 형일 테고, 당연히 결과값도 int 로 나온다.


사실 이것은 표현식에서 형 변환이 진행되는 과정을 간소화한 것이다. unsigned 향을 추가하면 규칙은 훨씬 복잡해진다. 전체 규칙은 부록 B 「Objective-C 2.0 언어요약」에 나와 있다.

어느 단계든 '결과 형' 을 설명하는 부분에 도달하면 변환 과정이 완료된 것이다.

한 예를보면서 이 과정을 어떻게 거치는지 배워 보자. 다음 표현식에서 f 는 float 형 이고, i 는 int 형, l 은 long 형, s 는 short int 형이다.

f * i + l / s


먼저 float 형과 int 형의 연산인 f 와 i 의 곱셈을 해보자. 3단계처럼 f 가 float 형이기 때문에 다른 항(i)도 float 형으로 변환되고, 결과 값도 마찬가지로 float 형이 된다.

다음으로 long int 를 short int 로 나누는, l / s 를 보자. 4단계처럼 short int 는 int 형으로 바뀐다. 그 후 6단계에서, 두 항 가운데 하나(l)가 long int 형이으로 다른 항도 long int 형으로 변환되고 결과 값도 long int 로 나온다. 이 나눗셈은 long int 형으로 결과값을 생성하고 소수점 이하값은 모두 잘려 나간다.

마지막으로, 3단계처럼 두 항중 하나가 float 형이면(f * i의 결과가 float 이다) 나머지 항도 float 로 변환되고 결과 값도 float 임을 알 수 있다. 따라서 l / s 가 수행된후, 연산 결과값은 float 로 변환되고 이는 f * i 의 결과값에 더해진다. 따라서 이 표 현식의 결과값은 float 가 된다.

하지만 언제든 형 변환 연산자를 명시적으로 사용하여 형 변환을 할 수 있음을 기억하자.

따라서 l / s의 결과에서 소수점을 보존하고 싶다면, 형 변환 연산자를 써서 두 항중 하나를 float 형으로 변환한다. 그러면 부동소수점 연산을 할 수 있다.

f * i + (float)l /  s


이 표현식 에서는 나눗셈 연산자보다 형 변환 연산자의 우선순위가 높기 때문에, l 은 나눗셈을 하기 전에 float 형으로 변환된다. 두 항 중 하나가 float형이기 때문에 다른하나(s)도 자동으로 float형이 될 테고, 결과값도 마찬가지다.


부호 확장

부호가 있는 int 와 short int 가 더 큰 정수형으로 변환된다고 해보자. 이때 부호는 왼쪽으로 확장된다. 예컨대 값이 -5인 short int는 long int 로 변환되더라도 -5 값을 갖는다. unsigned int 의 경우, 더 큰 정수형으로 변환되면 부호를 확장하지 않는다.

몇몇 기기(현재 매킨토시 컴퓨터 에서 사용하는 인텔 프로세서나 아이폰, 아이팟 터치에서 쓰는 ARM 프로세서)에서는 문자를 양의 부호로 처리한다. 이것은 문자를 정수로 변환할 때, 부호 확장이 발생한다는 얘기다. 문자가 표준 ASCII 문자 세트에 속할 때는 부호가 확장되어도 아무런 문제가 없다. 그러나 문자값이 표준 문자 세트에 속하지 않으면, 정수로 변환될 때 부호가 확장될 수도 있다. 예를 들어, 맥에서 문자 상수 '\377'은 부호가 있는 8비트로 처리하면 음수 값이 되기 때문에, -1 이라는 값으로 변환된다.

Objective-C 언어에서는 문자변수를 unsigned 로 선언할 수 있다는 점을 기억하자. unsigned 로 선언하면 이 문제를 피할 수 있다. 즉, unsigned char 변수는 정수로 변환될 때 부호를 확장하지 않는다는 얘기다. 값이 언제나 0보다 크거나 같다. 일반적인 8비트 문자의 경우 signed 문자 변수가 -128 에서 +127 까지의 값을 갖는다. 반면, unsigned 문자 변수의 값은 0에서 255까지이다.

문자 변수에서 부호가 확장되도록 강제하고 싶을 때는 signed char 형으로 문자 변수를 선언하면 된다. 이렇게 하면 문자값이 정수로 변환될 때, 문자 확장을 하지 않는것이 기본값인 시스템에서도 문자확장을 하도록 강제할 수 있다.

15장 「숫자, 스트링, 컬렉션」 에서 멀티바이트 유니코드 문자를 다루는 방법을 배울 것이다. 문자 세트에 수백만 글자가 있으므로 이에 속한문자로 구성된 스트링을 다룰 때는 이 방법을 더 선호하게 된다.


연습문제

1. 8장 「상속」에서 본 Recrangle 클래스를 사용하여 다음과 같이 선언하고 초기화 메서드를 추가하라.

-(Rectangle *) initWithWidth: (int) w andHeight: (int) h;


2. 연습문제 1에서 개발한 메서드를 Recrangle 클래스의 지정된 초기화 메서드로 설정하고 8장의 Square, Rectangle 클래스 정의를사용한다고 해보자. 이제 다음과 같이 선언하여 초기화 메서드를 Square 클래스에 추가하라.

-(Square *) initWithSide: (int) side;


3. Fraction 클래스의 add: 메서드에 카운터를 추가하여 이 메서드가 호출된 횟수를 세라. 이 카운터 의 값을 어떻게 가져올 수 있을까?


4. typedef 문과 열거 데이터 형을 사용 함으로써 Sunday, Monday, Tuesday, Wednesday, ThursWay, Friday, Saturday 를 가능한 값으로 갖는 Day 라는 형을 정의하라.


5. typedef 를 써서 FractionObj 라는 형을 정의하라. 그 결과, 다음 명령문을 사용할 수 있어야 한다.

FractionObj f1 = [[Fraction alloc] init],
       	             f2 = [[Fraction alloc] init];


6. 다음 정의를 보라.

float f = 1.00;
short int i = 100;
long int = l = 500L;
double d = 15.00;


이 정의와, 표현식 에서의 형 변환을 다룬 일곱 단계에 따라 다음 표현식의 값 과 형을 알아내라.

f + i
l / d
i / l + f
l * i
f / 2
i / (d + f)
l / (i * 2.0)
l + i / (double) l


7. 여러분 컴퓨터에서 부호가 있는 char 변수에 부호확장이 가능한지 확인할 수 있는 프로그램을 작성하라.


Notes

  1. 13장에서 비트 필드에 대해 짧게 셜명한다.