앞장에 이어 오프젝트C를 설명한다.
----------------------------------------------------------------------------------------
동적 타이핑, 동적바인딩
설명에 들어가기에 앞서 id형에 대한 이야기를 해보려 한다.
우리에게 어떤 클래스 두개가 있다고하자 (일단 C++문법으로)
이 두개의 클래스는 RPG게임에서 화면상에 배열로 저장된 맵에
그려질 캐릭터 객체이다.
class Swordman
{
int x,y;
image *p;
int hp, mp;
(void)printOut(void)
{
drawImage(x,y);
}
}
class Cleric
{
int x, y;
image *p;
int hp, mp;
(void)printOut(void)
{
drawImage(x,y);
}
}
이것을 객체로 만들면
Swordman *sword = new Swordman;
Cleric *clerics = new Cleric;
이다
위의 그림은 맵인데 총 7 X 5의 크기이다.
배열 array[7][5]에는 무슨 캐릭터가 있는지에 대한 값이 저장되어 있는데
0이면 없고, 1이면 검사, 2면 성직자가 있다고 한다.
일단 여기 배열에 다음과 같이 값이 저장되어있다고 하자
여기에 난 for문을 사용하여 모든 맵 타일에 캐릭터들을 그리려 한다.
그렇다면 내가 생각할 수 있는 제일 편리한 방법은 이런 방법일 것이다.
for(i = 0 ; i < 7 ; i++)
{
for( j = 0 ; j < 5 ; j++)
array[i][j]->printOut;
}
이런식으로 되야 가독성도 높고 코드를 읽는 사람이 편하게 읽을 수 있다.
하지만 캐릭터 클래스가 2개가 있으므로
배열에 타입이 다른 클래스를 저장한다는건 불가능하다.
왜냐하면 애초부터
Swordman *array[7][5] 라고 배열을 선언해놓고 array에 값을 넣을때
array[3][3] = clerics;
성직자 객체를 저장할수가 없다.
타입이 다르기 때문이다. 하지만 Objective C에는
ID타입이 있는데 이것은 C++에 void타입과 비슷하다.
id array[7][5]로 선언하면
array[3][3] = clerics;
array[2][2] = sword;
로 다른타입의 객체를 저장할수가 있다.
이렇게 되면
for(i = 0 ; i < 7 ; i++)
{
for( j = 0 ; j < 5 ; j++)
array[i][j]->printOut;
}
이런 주문도 가능하다는 것이다. ID 설명은 여기까지..
그럼 동적타이핑과 동적바인딩에 대한 설명을 하겠다.
동적 타이핑
동적 타이핑을 이해하려면 오히려 정적 타이핑개념을 이해하면 된다.
id는 모든 데이터형을 저장할 수 있다. 그렇다면 id 만 쓰면 되지 어렵게 여러가지 데이터형을 쓸까?
답은 메모리 때문이다. 메모리를 절약하기위해 특정 테이터형을 명시하는 것이다. 이것을 정적 타이핑이라고 한다. 이와 반대되는 개념이 동적 타이핑이다. 즉 특정 객체의 데이터형을 런타임시에 파악하는 것을 이야기한다.
동적 바인딩
동적바인딩은 객체에 호출되는 실제 메서드를 알아내는 시기를 프로그램 실행 중으로 미룬다.
메소드 판단을 런타임에 한다는 점에서는 다형성과 같지만 동적 바인딩은 id 와 관련된 개념이다.
다음을 보자
id dataValue;
Fraction *f1; // Fraction : 분수 클래스
Complex *c1; // Complex : 복소수 클래스
dataValue = f1;
[dataValue print];
dataValue =c1;
[dataValue print];
dataValue print 에서 프로그램은 컴파일시에 Fraction의 Print를 할지 complex의 print를 할지 알수 없다. 런타임시에 객체의 클래스가 무엇인지 알아낸다.
Cat *cat = [[Cat alloc]init];
[cat sing] // 컴파일시에 경고
id cat = [[Cat alloc]init];
[cat sing] // 컴파일시에 경고 안함. 런타임시에 경고
-------------------------------------------------------------------------------------------
카테고리
아까 언급했던 캐릭터 클래스 중에 Swordman 클래스를 예로 들어보자
아까는 C++로 표기했지만 이번에는 ObjectiveC로 쓰겠다
@interface Swordman:NSObject
{
int x, y;
image *p;
int hp,mp;
}
-(void)printOut(void)
@implementation Swordman
-(void)printOut(void)
{
[mFrame drawImage: p];
}
기존 구현은 이렇게 되어 있는데..
만약에 내가 소드맨을 그리는 것 말고 소드맨의 상태이상을 조종하는 메소드들을 추가하고 싶다면?
"그러면 그냥 클래스 안에 메소드를 써넣으면 되잖아!"
물론 그말이 맞다. 근데 이 클래스에 구현될 메소드 들을 여러 프로그래머랑 같이 작업하는
것이라서 함부로 건드릴수 없다면?
이런 상황에서 ObjectiveC는 카테고리를 쓰길 권장한다.
방법은 다음과 같다. 기존 구현부분은 냅두고 추가로
@interface Swordman (statusControl)
-(void)Bleeding(void);
-(void)Healing(void);
-(void)Poisened(void);
-(void)Paralize(void);
@implementation Swordman (statusControl)
-(void)Bleeding(void)
{
//구현부분
}
.
.
이렇게 따로 추가해주면
statusControl이라는 카테고리로 기존 Swordman클래스에 저 메소드 들이 추가가 된다
하지만 남용하면 위험하니 충분히 다른 사용 예를 검토하고 쓰길 바란다.
카테고리는 여기까지..
프로토콜
프로토콜에 대해 설명하겠다.
프로토콜(protocol).. 사전식 풀이로는, 규범, 규칙이다.
프로토콜은 여러사람들이 한가지 프로젝트를 할 때 유용한 개념이다.
위에서 우리는 ID타입에 대한 개념을 봤었다.
ID타입을 통해 printOut으로 캐릭터를 출력했었는데,
만약 사람들이 각자 다른 직업의 클래스를 만든다고 하자
소드맨,클레릭,엘프,드워프
근데 각자 클래스마다 출력하는 메소드가 다르다면?
소드맨.printOut
클레릭.printChar
엘프.printElf
드워프.printDwarf
이러면 아무리 유연한 ID타입형 배열에 저 객체들을 집어 넣어도
printOut을 하면 소드맨만 출력되고 나머지는 메소드명이 틀렸다고 에러가 뜰 것이다.
물론 프로그래머들 끼리 모여서 메소드명을 통일하기로 정한 후에 하면 저런 문제는
발생할 가능성이 적어지겠지만, 그래도 누가 실수안할거라고 확신할수 있을까?
이런 상황에서 프로토콜은 클래스에 어떤 지켜야할 "규정"을 정해줌으로서
저런 상황의 발생을 줄일 수 있도록 해준다.
프로그래머들 끼리 캐릭터출력하는 것을 printOut으로 정했다고 하자.
이 규정을 프로그램에서 프로토콜로 표현하면
@protocol printProtocol
- (void) printOut (void)
@end
이다.
이제 각자 클래스를 만들때
뒤에 <프로토콜명>을 명시하면
@interface Swordman:NSObject <printProtocol>
클래스에 -(void) printOut (void) 메소드가 구현이 되어 있지 않으면
컴파일러가 오류로 처리하고, printOut을 클래스에 구현할 것을 명령한다.
인수와 메소드 이름, 리턴 타입도 전부 동일해야만 한다.
@interface Dwarf:NSObject <printProtocol>
{
int x, y;
image *p;
int hp,mp;
}
-(void)printDwarf(void)
@implementation Swordman
-(void)printDwarf(void)
{
[mFrame drawImage: p];
}
//프로토콜에 규정된 printOut 메소드가 없으므로 컴파일러는 오류로 처리한다.
//printDwarf를 규정되어 있는 printOut으로 바꿔야 그제서야 컴파일러가 넘어간다.
-----------------------------------------------------------------------------------------
Foundation Framework
이제 나오는 것들은 Objective C에서 제공해주는 프레임워크들에 대한 내용이다.
Foundation 프레임워크는 WinAPI와 비슷하다고 보면 된다. 프로그래밍에서 쓰이는
유용한 메소드들을 모아놓은 것인데
메모리 관련 메소드
숫자 관리 메소드
파일관리 메소드
기타 등이 있다.
대표적인 것들만 다루고, 더 궁금한게 있다면 검색해보길 권한다.
그만큼 내용도 많고, 찾아서 쓰기만 하면 되는 것들이기 때문에 많이 다루지는 않겠다.
NSNumber
정수,실수,복소수, 심지어 문자도 가리지 않고 저장이 가능하다.
관련 메소드는
initWithChar : 문자형 객체로 초기화한다.
initWithInteger : 정수형 객체로 초기화한다.
initWithFloat : 실수형 객체로 초기화 한다.
사용 예로 들자면
NSNumber *memory;
memory = [NSNumber initWithInteger];
initWithInteger 메소드를 호출하면
NSNumber는 알아서 메모리 공간을 정수형으로 할당해서 memory에 주소를 넘겨준다.
아예 처음부터 값을 넣어줄수도 있다.
memory = [NSNumber numberWithInteger : 10];
아예 처음부터 10으로 초기화 해버린 것이다.
NSString
문자열을 다루는 객체이다.
예를 들어보자
NSString *string = [NSString stringWithString : @"hello];
위의 내용은 문자열공간을 할당하여 hello를 저장하여 주소를 string에 넘겨주었다.
다음 내용도 거의 동일하다.
NSString *string = @"hello";
하지만 NSString 객체는 문자열을 수정할 수가 없다.
C++에서도
char *string = "hello";
string[2] = 'k';
라고 하면 문자열 내부의 값을 바꾸려고 하면
[value is Constant. Can't change] 에러메시지가 뜬다.
값이 상수라서 변경을 할수 없다는 것이다.
NSString은 상수문자열을 저장하는 객체이다.
수정이 가능한 문자열 객체를 원한다면
NSMutableString 객체를 사용하길 바란다.
정리하면, 문자열에는 수정불가능한 문자열과, 가능한 문자열이 있다는 것이다.
기억해두자.
NSArray
이것은 배열을 관리해주는 객체이다.
일반 C의 배열과 다른점은 C에서
int array[10]; 이라고 했을시에 array에는 int형 값만 넣을수 있다는 것인데
NSArray는 무엇을 넣든 상관이 없다.
NSNumber처럼 배열을 관리해주는 관련 메소드가 많다
(상당히 많기 때문에 다른곳에서 레퍼런스를 찾아 읽어보길 바란다.
그리고 나도 NSArray는 거의 사용해보지 않았다. 아직도 내겐 int array[]가 익숙하다)
NSAutoReleasePool
앞장에서 Objective C 첫예제를 기억할 것이다.
int main()
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSLog(@"hello world");
[pool drain];
}
pool 변수에 NSAutoreleasePool객체를 할당받아서 초기화하고 주소를 저장하였다.
대체 pool 객체가 뭘까?
사실 이 pool객체는 메모리 관리를 좀더 쉽게 해주기 위해서 있는 것이다.
pool객체에는 우리가 alloc 메소드로 만들어내는 '객체들의 참조하는 갯수'들이 저장이 된다.
근데 이게 왜 필요할까?
한번 예를 들어보겠다.
void main()
{
int *a = new int;
int *b = a;
delete a;
}
이 상황을 그림으로 표현하면
이렇게 될 것이다. 근데 이 상황에서
delete a;를 해버리면
a가 참조하고 있던 int형객체를 메모리에서 지우게 되는데
a는 더이상 참조하지 않으니까 별문제 없지만
b는 존재하지도 않는 int객체를 참조하고 있기 때문에
문제가 발생하게 된다.
"난 저런 실수 안해"
물론 변수 달랑 두개가지고 참조하는 걸로 실수할 사람은 별로 없을 것이다. 문제는
한개의 객체를 참조하는 변수가 10개이상이라면..
저 10개중 한놈이라도 delete를 호출하여 메모리를 해제하게 될 시 나머지 변수들은 매우 곤란한 상황에 처하게 된다.
만약 저 int형객체를 참조하는 횟수를 알고 있다면 참조하고 있는 놈들 중 한놈만 남기고
전부 0으로 초기화 하면 되긴 하지만, 횟수를 알기는 힘들기 때문에 관리가 어렵다.
그래서 Objective C에서는 NSAutoreleasePool이라는 "내가 대신 관리해줄게" 클래스가 있다.
이 클래스는 NSObject클래스로부터 '상속'되었다.
(NSObject에서 상속되었다는 사실은 매우 중요하다. 아래에서 설명하겠다)
기억해둘것
앞장에서 언급안한것 같아 여기서 설명한다
C와 Objective C에서는 메모리 할당 메소드와 해제 메소드 이름이 다르다.
C는
int *a = (int*)malloc(sizeof(int));
free(a);
C++에서는
int *a = new int;
delete a;
Objective C에서는
int *a = [[int alloc]init];
[a dealloc];
이다.
실제로 free = delete = dealloc 다 물리적으로 같은 일을한다.
RAM에 잡아두었던 메모리를 해제하는 것이다.
하지만 Obective C에서는
dealloc 대신 release라는 메소드를 더 많이 쓴다.
release역시 dealloc과 비~슷한 일을 한다.
하지만 차이가 있다.
어떻게 차이가 있을까 보자
int main()
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
Swordman *sword = [[Swordman alloc]init];
Swordman *sword2;
Swordman *sword3;
NSLog = (@"Swordman count = %i",[sword retainCount]);
[sword2 addObject: sword];
NSLog = (@"Swordman count after addObject = %i",[sword retainCount]);
[sword2 release];
NSLog = (@"Swordman count after release = %i", [sword retainCount]);
[sword release];
NSLog(@"Dealloc has executed");
[pool drain];
}
결과
Swordman count = 1
Swordman count after addObject = 2
Swordman count after release = 1
Dealloc has executed
sword 변수에는 alloc으로 메모리를 할당해서 객체를 생성하였다.
그리고 메모리에 만들어진 Swordman 객체를 참조하는 것은 sword 하나밖에 없기 때문에
참조횟수가 1이 나오는 것이다.(retainCount는 참조횟수를 뜻한다)
근데 sword2에 addObject를 통해 sword 주소를 sword2에 복사하였다.
그래서 다음 NSLog에서 참조횟수가 2가 된 것이다.
그림으로 표현하자면
이렇게 될 것이다. 두 객체변수가 메모리에 잡혀있는 Swordman 객체 하나를 참조하고 있다.
근데 여기서
[sword2 release]를 하면
release 메소드는
sword2를 0으로 만들고
참조횟수를 1 감소시킨다.
이 사실은 매우 중요하다.
dealloc은 메모리를 '정말' 지우는 반면에
release 참조횟수가 2이상이면 메모리를 지우지 않고, 참조횟수만 1 줄인것이다.
만약 참조횟수가 1일때 release 메소드가 실행된다면
release 메소드는 자동으로 dealloc을 호출하여 물리적으로 메모리를 해제할 것이다.
이것은 매우 중요한 개념이다. 이해가 안간다면 다시 읽기를 권한다.
한가지 더-----------------------------------------------------------
궁금하지 않은가? 어떻게 release메소드는 참조횟수를 알고있는 걸까?
힌트는 NSAutoreleasePool이 NSObject에서 상속되었다는 것이다.
.
.
.
.
.
눈치챈 사람도 있을 것이다.
NSObject 내부에 객체들의 참조횟수를 저장하는 공간이 있는것이다.
NSAutoreleasePool은 NSObject와 상속관계이기 떄문에 그 공간에 있는 참조횟수를
확인할 수 있다.
그리고 자주봐서알겠지만 Objective C에 사용자 정의 클래스들은 거의
@interface Swordman: NSObject
NSObject의 상속 클래스이다.
그림으로 표현하자면
(미안하다.. 못그려서)
----------------------------------------------------------------
pool에 의한 참조갯수에 대한 이야기를 하고 있었다.
하지만 아직도 한가지 의문이 남는다.
[pool drain]
이건뭘까?
이걸 설명하기 전에 autorelease 라는 메소드에 대해서 배워보자.
제일 좋은 설명방법은 역시 예를 드는 것이다.
int main()
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
Swordman *sword = [[Swordman alloc]init];
NSLog(@"Swordman count after alloc = %i", [sword retainCount]);
[sword autorelease];
[pool drain];
NSLog(@"Swordman count after drain = %i", [sword retainCount]);
}
결과
Swordman count after alloc = 1
Swordman count after drain = 0
아까 그 예제인데 뭔가 좀 틀리다.
[sword autorelease]가 추가되었다
이 말은 sword변수가 참조하는 Swordman객체 메모리를
pool에서 관리해달라고 요청하는 것이다.
이렇게 되면 [pool drain]을 실행하면
autorelease로 등록되어 있는 sword는 자동으로 dealloc된다.
마치며
아직도 설명하지 못한 것들이 있는데, 아카이빙, 객체복사 등이 있는데 그것들은 직접 찾아서 이해하길 바란다. 내가 다루고 싶긴해도 아직 나 역시 그부분에 대한 이해가 완벽하지 않기 때문에 괜히 어설프게 설명했다가 읽는사람들이 피해를 볼 수 있기 때문이다.
(물론 여태껏 내가 설명했던 내용이 완벽하다는 건 아니다)
아무튼 Objective C와 C++의 차이점에 대해선 여기까지 다루겠다.
다음 내용으로는 표준 그래픽 라이브러리인 OpenGL ES 2.0을 다루도록 하겠다.
혹시 틀리거나 이상한 부분이 있으면 덧글로 알려주시면 감사하겠습니다.