왜 이 짓을 하냐고??
MinGW를 깔 때 Objective-C 컴파일러도 함께 설치해서 libobjc 가 깔려있는 상태지만,
심심풀이삼아 Objective-C 코딩을 해보려면 NSObject 없이는 솔직히 아무것도 안되거덩.
하지만 그거 하자고 GNUStep 을 또 설치하기는 그렇잖아. 태국 홍수로 하드값이 비싸져 용량 늘리기도 힘든데...
게다가 GNUStep의 우중충한 GUI는 별로 관심없으니 base만 깔아보자는 이야기.
맨 밑에 Pre-requisites 항목에는 libffi, libxml, libtiff 가 필요하다고 나와있다.
libtiff야 GUI에 쓰일테니 무시. libxml은 gnustep-base 구성시 --disable-xml 옵션으로 빼버릴 수 있으니 libffi만 설치하면 된다.
* Objective-C 는 id 라고 부르는 자료형이 있다. 이 물건은 여러모로 void* 와 유사하지만 반드시 객체에 대해서만 사용된다. Objective-C 에서는 C++이나 자바와 달리 객체의 메소드를 호출할 때 그 자료형을 몰라도 관계없다. 단지 메소드만 있으면 된다. 이를 객체에 대한 메시지 전달이라고 한다.
* idTypeTest 프로젝트를 만들고 생성자에서 사용한 예제에서 Fraction.h, Fraction.m 을 그대로 가지고 온다.
* Complex.h
#import <Foundation/NSObject.h>
@interface Complex: NSObject {
double real;
double imaginary;
}
-(Complex*) initWithReal: (double) r andImaginary: (double) i;
-(void) setReal: (double) r;
-(void) setImaginary: (double) i;
-(void) setReal: (double) r andImaginary: (double) i;
-(double) real;
-(double) imaginary;
-(void) print;
@end
* Complex.m
#import "Complex.h"
#import <stdio.h>
@implementation Complex
-(Complex*) initWithReal: (double) r andImaginary: (double) i {
self = [super init];
if (self) {
[self setReal: r andImaginary: i];
}
return self;
}
-(void) setReal: (double) r {
real = r;
}
-(void) setImaginary: (double) i {
imaginary = i;
}
-(void) setReal: (double) r andImaginary: (double) i {
* 이러한 동적 형 연결은 어떤 함수를 호출하기 위해 그 객체의 형을 알 필요가 없다는 큰 장점이 있다. 객체가 메시지에 반응한다면, 그 메소드를 실행될 것이다. 자바에서는 .intValue() 라는 함수를 호출할 경우 먼저 캐스팅을 하고 그 뒤에 메소드를 호출해야 하지만, Objective-C 에서는 이런 지저분한 캐스팅문제로 머리아플 필요가 없다.
상속 (Inheritance)
* 원문의 예제는 Rectangle 객체를 만들게 되어있는데, MinGW에서는 wingdi.h 에 정의된 Rectangle 함수와 충돌이 나므로 MyRectangle 로 변경하였다.
* Inheritance 프로젝트를 만들고 다음과 같이 각 유니트를 코딩한다.
* Rectangle.h
#import <Foundation/NSObject.h>
@interface MyRectangle: NSObject {
int width;
int height;
}
-(MyRectangle*) initWithWidth: (int) w height: (int) h;
-(void) setWidth: (int) w;
-(void) setHeight: (int) h;
-(void) setWidth: (int) w height: (int) h;
-(int) width;
-(int) height;
-(void) print;
@end
* Rectangle.m
#import "Rectangle.h"
#import <stdio.h>
@implementation MyRectangle
-(MyRectangle*) initWithWidth: (int) w height: (int) h {
* Objective-C 의 상속은 자바와 유사하다. 부모 클래스를 확장할 때는 (상속도 단일상속만 가능) 부모의 메소드를 단순히 구현부에 재정의 하는 것으로 오버라이딩 할 수 있다. C++ 에서의 가상테이블은 잊어버리자.
* 한가지만 짚고 넘어가자. 만약 정사각형의 생성자 호출 부분에서 MySquare *sq = [[MySquare alloc] initWithWidth: 10 height: 15] 와 같이 부모의 생성자를 호출하면 어떻게 될까. 답은 컴파일 에러가 발생하게 된다. 왜냐하면 MyRectangle 클래스 생성자가 반환하는 값은 MyRectangle* 이지 MySqare* 는 아니기 때문이다. id 객체는 이럴 때 써먹을 수 있다. 만약 하위 클래스에서 상위 클래스의 생성자를 사용하려면 단순히 리턴값을 MyRectangle* 이 아닌 id 로 바꾸기만 하면 된다.
동적 자료형 (Dynamic types)
* 동적 자료형에서 사용할 수 있는 Objective-C 의 메소드들을 살펴보자.
-(BOOL) isKindOfClass: classObj
이 객체가 clssObj 의 멤버 또는 하위클래스인지 여부
-(BOOL) isMemberOfClass: classObj
이 객체가 classObj 의 멤버인지 여부
-(BOOL) isRespondsToSelector: selector
이 객체가 selector 라는 이름의 메소드를 가지고 있는지
+(BOOL) instancesRespondToSelector: selector
이 클래스에서 생성된 객체가 지정된 selector에 반응하는지
-(id) performSelector: selector;
객체의 지정된 셀렉터를 실행
* NSObject 에서 상속된 모든 객체는 클래스 객체를 반환하는 class 라는 메소드를 가진다. 이 방식은 자바의 getClass() 메소드와 아주 유사하다. 위에 나열된 메소드들은 이 클래스 객체를 사용한다.
* Objective-C 에서 셀렉터(selector)는 메시지를 표현하기 위해 사용된다. 셀렉터를 구현하는 문법은 아래를 참고하자.
printf( "square instance responds to setSize: method\n" );
}
// 메모리 해제.
[rec release];
[sq release];
return 0;
}
* 출력 결과는 다음과 같다.
square is member of square class
square is kind of square class
square is kind of rectangle class
square is kind of object class
square responds to setSize: method
square class responds to alloc method
square instance responds to setSize: method
카테고리 (Categories)
* 메소드를 클래스에 추가할 때 일반적으로는 확장을 하게 되지만, 이 방법이 통하지 않는 경우가 있다. 특히 소스코드가 없는 경우가 그럴 것이다. 카테고리를 사용하면 기존에 존재하는 클래스를 확장하지 않고 원하는 함수를 추가할 수 있다. 루비가 이와 비슷한 기능을 제공한다.
* Categories 프로젝트를 만들고 앞의 예에서 컴파일된 Fraction.o 파일과 Fraction.h 파일을 동일한 위치에 복사한다.
* 이대로 빌드하면 [Linker error] undefined reference to `__objc_class_name_Fraction' 라는 에러가 발생한다. Fraction.o 파일이 링크되지 않았기 때문이다. Dev-C++ 의 프로젝트 옵션의 링커에 Fraction.o 를 추가해도 이 항목이 LINKLIB 쪽으로 가기 때문에 제대로 되지 않는다. 이 버그는 차차 고쳐보도록 하고, 일단 빌드를 위해 생성되는 Makefile.win 을 열고 LINKOBJ 항목에 Fraction.o 를 직접 추가한다.
......
LINKOBJ = Fraction.o FractionMath.o main.o $(RES)
......
* 이제 make 를 사용해 빌드해보자.
C:\GNUstep\bin\make.exe -f Makefile.win
* 출력결과는
1/3 * 2/5 = 2/15
* Fraction.m 소스코드 없이도 Fraction 클래스에 새로운 메소드를 추가할 수 있다.
* 이 마술의 비밀은 바로 2개의 @implementation 과 @interface 에 있다. @interface Fraction (Math) 그리고 @implementation Fraction (Math)
* 동일한 이름으로는 오직 한개의 카테고리만을 만들 수 있다. 카테고리를 추가하려면 이전과 다른 유일한 이름으로 만들어야 한다.
* 카테고리에서는 인스턴스 변수를 추가할 수 없다.
* 카테고리는 private 메소드를 만들 때 유용하다. Objective-C 는 자바와 달리 private/protected/public 메소드에 대한 구분이 명확하지 않지만, 감추고 싶은 메소드를 만들 때에는 카테고리를 사용할 수 있다. 예를 들어 클래스의 private 메소드들을 클래스 헤더파일이 아닌 소스파일에 정의하는 것이다. 다음 예를 보자.
* MyClass.h
#import <Foundation/NSObject.h>
@interface MyClass: NSObject
-(void) publicMethod;
@end
* MyClass.m
#import "MyClass.h"
#import <stdio.h>
@implementation MyClass
-(void) publicMethod {
printf( "Public method\n" );
}
@end
// private method
@interface MyClass (Private)
-(void) privateMethod;
@end
@implementation MyClass (Private)
-(void) privateMethod {
printf( "Private method\n" );
}
@end
* main.m
#import "MyClass.h"
int main ( int argc, const char *argv[] ) {
MyClass *obj = [[MyClass alloc] init];
// 컴파일 되는 부분.
[obj publicMethod];
// 컴파일 안되는 부분
//[obj privateMethod];
[obj release];
return 0;
}
* 결과
Public method
포징 (Posing)
* 포징은 카테고리와 유사하지만 조금 더 꼬여있다. 포즈를 이용하면 클래스를 확장한 서브클래스를 수퍼클래스와 전역적으로 바꿔칠 수 있다. NSArray를 확장한 NSArrayChild를 만들었다고 할 때, NSArrayChild를 NSArray로 포징하면 코드 내의 NSArray는 자동으로 NSArrayChild로 변경되게 된다.
* Posing 프로젝트를 만들고 이전에 만든 Fraction.h, Fraction.m 파일을 옮겨온다. 그리고 다음 유니트들을 추가하자.
* 프로토콜을 구현한 메소드들은 헤더의 메소드리스트에 없어도 관계없다. 위의 예제에 보이듯 Complex.h 에는 "-(void) print" 에 대한 정의가 없다. 하지만 구현부에는 존재해야 한다.
* Objective-C 에서 특이한 점은 형을 지정하는 방식이다. 자바나 C++ 에서는 Printing *someVar = (Printing*) frac; 과 같이 명시적으로 형을 표현해야 하지만, Objective-C 는 id형과 프로토콜을 사용해 id <Printing> var = frac; 처럼 표현한다. 이를 통해 여러개의 프로토콜이 필요한 자료형을 하나의 변수를 사용해 동적으로 명시할 수 있다. 위 코드의 id <Printing, NSCopying> var = frac; 이 좋은 예이다.
* 객체의 상속을 테스트하기위해 @selector를 사용했던 것과 마찬가지로, 인터페이스가 일치하는지 살피기 위해서는 @protocol을 사용한다. [object conformsToProtocol: @protocol(SompProtocol)] 은 객체가 프로토콜을 따르는지 여부를 BOOL형으로 반환하는 함수이다. 클래스의 경우에도 마찬가지로 [SomeClass conformsToProtocol: @protocol(SomeProtocol)] 처럼 쓸 수 있다.
메모리 관리
* 지금까지는 Objective-C 의 메모리관리에 대해 별다른 설명을 하지 않았다. 물론 단순히 dealloc 하면 메모리는 해제되겠지만, 만약 다른 객체가 이 객체를 참조하고 있다면 어떻게 해야할까. 이제부터는 이런 상황에 대한 주의와 함께 Foundation 프레임워크에서 클래스를 만들 때 어떻게 메모리를 관리하는지에 대한 설명을 시작하려 한다.
* 당연한 이야기지만, 위의 예제들은 모두 적절히 메모리가 관리되었다.
Retain과 Release
* NSObject에서 상속받은 모든 객체는 retain과 release라는 두개의 메소드를 가진다. 모든 객체는 내부의 카운터를 통해 자신이 얼마나 참조되었는지를 추적하고 있다. 만약 3번의 참조되고 있다면 자기 자신을 dealloc 하면 안될것이다. 물론 참조갯수가 0이라면 메모리에서 해제되어도 된다. [object retain] 은 이 참조횟수, 즉 레퍼런스카운트를 하나 증가시키고 [object release]는 하나 감소시킨다. 만약 [object release] 에 의해 카운터가 0이 되면 dealloc 이 호출된다.
* ReleaseTest 프로젝트르 시작하고 idTypeTest에 사용된 Fraction.h/m 파일을 복사한다.
* retain 호출은 카운터를 증가시키고 release 호출은 감소시킨다. [obj retainCount] 는 레퍼런스 카운트를 정수값으로 반환한다. 이 값이 0이 되면 객체는 dealloc 메소드를 호출하고 자기자신의 메모리를 해제한다. 마지막줄의 "Deallocing fraction" 은 그것을 나타낸다.
Dealloc
* 객체가 다른 객체를 포함할 경우, 자기 자신을 제거하기 전에 포함한 객체도 메모리에서 제거해야 한다. Objective-C 의 또다른 장점은 nil 에도 메시지를 전달할 수 있기 때문에 객체 제거시 귀찮은 에러검사를 생략할 수 있다는 것이다.
* DeallocTest 프로젝트를 만들고 다음과같이 코딩하자.
* AddressCard.h
#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
@interface AddressCard: NSObject {
NSString *first;
NSString *last;
NSString *email;
}
-(AddressCard*) initWithFirst: (NSString*) f
last: (NSString*) l
email: (NSString*) e;
-(NSString*) first;
-(NSString*) last;
-(NSString*) email;
-(void) setFirst: (NSString*) f;
-(void) setLast: (NSString*) l;
-(void) setEmail: (NSString*) e;
-(void) setFirst: (NSString*) f
last: (NSString*) l
email: (NSString*) e;
-(void) setFirst: (NSString*) f last: (NSString*) l;
-(void) print;
@end
* AddressCard.m
#import "AddressCard.h"
#import <stdio.h>
@implementation AddressCard
-(AddressCard*) initWithFirst: (NSString*) f
last: (NSString*) l
email: (NSString*) e {
self = [super init];
if (self) {
[self setFirst: f last: l email: e];
}
return self;
}
-(NSString*) first {
return first;
}
-(NSString*) last {
return last;
}
-(NSString*) email {
return email;
}
-(void) setFirst: (NSString*) f {
[f retain];
[first release];
first = f;
}
-(void) setLast: (NSString*) l {
[l retain];
[last release];
last = l;
}
-(void) setEmail: (NSString*) e {
[e retain];
[email release];
email = e;
}
-(void) setFirst: (NSString*) f
last: (NSString*) l
email: (NSString*) e {
[self setFirst: f];
[self setLast: l];
[self setEmail: e];
}
-(void) setFirst: (NSString*) f last: (NSString*) l {
2010-11-26 18:26:07.890 DeallocTest[4036] autorelease called without pool for object (b58c90) of class GSAutoreleasedMemory in thread <NSThread: 0x3dea0>
2010-11-26 18:26:07.890 DeallocTest[4036] autorelease called without pool for object (b58f18) of class GSAutoreleasedMemory in thread <NSThread: 0x3dea0>
2010-11-26 18:26:07.890 DeallocTest[4036] autorelease called without pool for object (bc3e40) of class GSAutoreleasedMemory in thread <NSThread: 0x3dea0>
Tom Jones <tom@jones.com>
밑에도 나오지만 맥의 코코아에서는 Autorelease pool 이 기본설정된단다. 결과가 원문과 다르게 NSLog로 도배된 이유는, NSString 의 cString 메소드를 호출할 때 autorelease 가 호출되지만 지금 학습중인 환경에서는 Autorelease pool 을 쓰고 있지 않기 때문이다. 따라서 다음장에서 살펴볼 NSAutolreleasePool 을 사용하면 이 로그가 사라지게 된다.
main.m 에서 #import <Foundation/NSAutoreleasePool.h> 를 상단에 추가하고 NSString 객체 생성 전에 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 해주면 저 로그가 사라진다. 이 pool 은 가장 마지막에 [pool release] 해주어야 한다.
Retain count: 1
Tom Jones <tom@jones.com>
* AddressCard.m 는 멤버변수 해제를 위해 dealloc 메소드를 작성하는 방법에 대해 보여주고 있다.
* 각각의 set 메소드에서 수행하는 세 코드는 그 순서가 매우 중요하다. 좀 이상한 예이지만 set 함수에 자기 자신의 인자를 넘기는 경우를 상상해보자. 만약 release 를 먼저하고 그 다음에 retain 을 한다면 자기 자신은 메모리에서 사라져버리게 된다. 때문에 항상 1) retain 2) release 3) 값설정 의 순서를 지켜야 한다.
* C문자열은 유니코드를 지원하지 않기 때문에, 문자열 변수를 초기화할 때 일반적으로는 C문자열을 사용하지 않는다. NSAutoreleasePool을 사용하는 다음 예제에서 좀 더 적절한 방법에 대해 이야기 할 것이다.
* 이 방법은 멤버 변수 메모리를 관리하는 여러방법 중 하나일 뿐이다. 또 다른 방법으로는 set 메소드 안에서 값을 복사하는 방법이 있다.
Autorelease Pool
* NSString 을 비롯한 Foundation 프레임워크를 사용하려면 좀 더 유연한 메모리관리가 필요하다. 이 시스템은 Autorelease pool 을 사용한다.
* 맥에서 코코아 어플리케이션을 개발할 때는 이 Autorelease pool 이 자동으로 설정된다.
* 또 한가지는 str3만 release 했는데도 이 프로그램에서 메모리관리는 완벽히 이루어지고 있다는 것이다. 첫번째 상수 문자열은 자동으로 Autorelease pool 에 추가되었고, 다른 문자열이 사용한 stringWithString 메소드는 NSString 문자열을 만들면서 자동으로 Autorelease pool에 이 문자열을 추가하기 때문이다.
* 적절한 메모리 관리를 위해 이 점을 잊지 말자. [NSString stringWithString: @"String"] 같은 편의함수들은 Autorelease pool 을 사용하는 반면 [[NSString alloc] init] 처럼 직접 메모리를 할당해서 만든 함수들은 Autorelease pool 을 사용하지 않는다는 점이다.
* Objective-C 에서는 두가지 메모리관리 방법을 사용한다. 1) retain 과 release 또는 2) retain 과 release/autorelease
* 각 retain 에는 반드시 한 개의 release 또는 autorelease 가 있어야 한다.
* 예제를 통해 이 내용을 살펴보자.
* AutoPool2 프로젝트를 생성하고 idTypeTest의 Fraction.h, Fraction.m 을 복사해온다.
* Fraction.h 에 다음 내용을 추가한다.
......
+(Fraction*) fractionWithNumerator: (int) n denominator: (int) d;
......
* Fraction.m 에 구현부를 만들어준다.
......
+(Fraction*) fractionWithNumerator: (int) n denominator: (int) d {
Fraction *ret = [[Fraction alloc] initWithNumerator: n denominator: d];
* 이 예제에서 새로 추가한 메소드는 클래스 레벨 메소드이다. 객체가 생성된 후 autorelease 가 호출되었으며, main 함수 내에서는 이 객체의 release 를 호출하지 않는다.
* 객체 해제가 동작하는 이유를 살펴보자. 모든 retain 은 하나의 release 또는 autorelease 와 쌍을 이루어야 한다. 이 예제에서 retain 카운트는 1이고 한번의 autorelease 를 호출하였다. 1-1=0 이라는 이야기. 이 Autorelease pool 이 release 되면, autorelease 를 호출한 모든 객체는 그 호출 횟수만큼 [obj release] 가 실행된다.
* 주석에 설명해두었듯이, 해당라인의 주석을 지우고 실행해보면 segment fault 가 발생한다. autorelease 가 이미 객체에서 호출되었기 때문에 release 를 호출하고 Autorelease pool 에서 다시 release 를 호출하게 되므로 이미 nil 이 된 객체의 dealloc을 호출하는 상황이 되는 것이다. 수식으로 보면 1(생성) - 1(release) -1(autorelease) = -1 이 되는 것이다.
* Autorelease pool 은 많은 양의 임시객체를 만들어내는 데 사용할 수 있다. 이 때는 먼저 풀을 만든 뒤 많은 임시객체를 생성하고 마지막에 풀을 제거하는 것이다. 당연한 이야기지만 한번에 한개 이상의 Autorelease pool 만드는 것도 얼마든지 가능하다.
Foundation 프레임워크 클래스
* Foundation 프레임워크는 C++ 의 Standard Template Library 와 유사하다. Objective-C는 진짜 동적 자료형을 다루기 때문에 C++의 템플릿처럼 복잡하게 코딩할 필요가 없다. 이 프레임워크에는 자료구조, 네트워킹, 쓰레드를 비롯한 여러가지가 포함되어있다.
원문에 쓰인 mutable 이 C++ 예약어이므로 marr 로 바꾸어주었다. 수정한 Dev-C++ 이 *.c 와 *.m 을 구분하지 않기 때문에...
* 출력 결과
---static array
Me
Myself
I
---mutable array
One
Two
Me
Myself
I
Three
---sorted mutable array
I
Me
Myself
One
Three
Two
* Foundation 프레임워크에는 NSArray 와 NSMutableArray 두 종류의 배열이 있다. 이 물건은 Foundation 프레임워크에서 가장 데이터에 중점을 둔 물건들이다. 이름에서 보이듯이, Mutable 은 크기가 변할 수 있지만 NSArray는 그렇지 않다. 다시 말해 NSArray는 한번 만들어진 후에는 그 길이를 바꿀 수 없다.
* 배열은 생성자에서 Obj, Obj, Obj, ... nil 의 형태로 초기화시킬 수도 있다. 여기서 nil은 끝을 의미한다.
* 정렬 부분은 셀렉터를 사용해 객체를 정렬하는 방법을 보여준다. 여기서 셀렉터는 array 객체에 정렬할 때 NSString의 대소문자 구분없는 비교함수를 사용하라고 알리고 있다.
* print 함수에서는 description 메소드를 사용했다. 자바의 toString과 비슷한 이 메소드는 객체를 NSString 으로 표현해 반환한다.
* NSEnumerator 는 자바의 enumerator 시스템과 유사하다. while ( obj = [array objectEnumerator] )이 작동하는 원리는 obj가 가장 마지막 객체에 들어가면 nil이 반환되기 때문이다. 보통 C에서는 nil은 0이고, 곧 false이다. 보다 정확하게 하려면 ( ( obj = [array objectEnumerator] ) != nil ) 이 나을지도 모르겠다.
설치 후 뜨는 MSys의 셸이 복사/붙이기가 잘 안되는 문제가 있으니 DKW를 셸로 사용하자. GNUStep 의 디렉토리에 DKW 실행파일과 INI를 복사하고 이름을 각각 GNUStep.exe, GNUStep.ini 로 바꾼다. GNUStep.ini 의 exec 항목을 다음과 같이 바꿔준다.
exec=C:\GNUstep\bin\sh.exe --login -i
한글 입출력을 원활히 하기 위해 .inputrc 수정
set output-meta on
set convert-meta off
환경설정, 프롬프트 및 ls 색 변경, 침침한 셸 색상 변경을 위해 .profile 파일 추가.
PS1="[\u@\h \W]\\$ "
alias ls="ls --color=auto --show-control-chars"
BGCOLOR=black
FGCOLOR=#AFAFAF
/GNUStep/System/Library/Makefiles/GNUstep.sh
이제 익숙한 셸 환경이 보인다. 우헤~~
다음과 같이 source.m 파일을 만들고
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
int
main (void)
{
NSAutoreleasePool *pool;
pool = [NSAutoreleasePool new];
[NSApplication sharedApplication];
NSRunAlertPanel (@"Test", @"Hello from the GNUstep AppKit",
nil, nil, nil);
return 0;
}
GNUMakefile 을 다음과 같이 만들어준다.
include $(GNUSTEP_MAKEFILES)/common.make
APP_NAME = PanelTest <---------- 실행파일 이름
PanelTest_OBJC_FILES = source.m <--- 소스파일 이름
include $(GNUSTEP_MAKEFILES)/application.make
콘솔창에서 make를 때리면 빌드. 햐~~ 맥에서의 어플과 마찬가지로 *.app를 가지는 디렉토리가 떨어진다.
컴파일이 다 끝났으면 실행.
$ openapp ./PanelTest.app
NS객체를 쓰지 않는 다음과 같은 hello.m 은 GCC명령 한방으로 컴파일되니 함 돌려보세.
#import <stdio.h>
int main (int argc, const char *argv[]) {
printf( "Hello world\n");
return 0;
}
gcc -o hello.exe hello.m
이 위키의 다음 페이지에서는 Dev-C++ 을 사용한 통합환경 꾸미기에 대해서도 설명되어있다.