ProgrammingInObjectiveC:Chapter 13: Difference between revisions

From 흡혈양파의 번역工房
Jump to navigation Jump to search
(OC2 13장 :: 하부 C 언어기능 페이지 추가)
 
(띄어쓰기 수정)
 
(3 intermediate revisions by the same user not shown)
Line 96: Line 96:


<hr style="color:black;background-color:black;height:3px;">
<hr style="color:black;background-color:black;height:3px;">
프로그램 13.
프로그램 13.1
<hr style="color:black;background-color:black;height:1px;">
<hr style="color:black;background-color:black;height:1px;">
<syntaxhighlight lang="objc">
<syntaxhighlight lang="objc">
Line 844: Line 844:




main 의 첫 명령문은 month, day, year 라는 세 정수 멤버를 갖는 date 구조체를 정의한다. 두 번째 명령문에서 변수 today 가 struct data 형으로 선언된다. 즉, 첫 번째 명령문은 date 구조체가 Objective-C 컴파일러에게 어떻게 보일지를 정의할 뿐이고, 컴퓨터 내부에는 이를 위해 어떤 저장 공간도 예약되지 않는다. 두 번째 명령문은 struα date형 변수가 되도록 선언한다. 이에 따라 구조체 변수 todya의 세 정수 멤버를 저장할 메모리 공간이 예약되는 것이다.
main 의 첫 명령문은 month, day, year 라는 세 정수 멤버를 갖는 date 구조체를 정의한다. 두 번째 명령문에서 변수 today 가 struct date 형으로 선언된다. 즉, 첫 번째 명령문은 date 구조체가 Objective-C 컴파일러에게 어떻게 보일지를 정의할 뿐이고, 컴퓨터 내부에는 이를 위해 어떤 저장 공간도 예약되지 않는다. 두 번째 명령문은 struct data 형 변수가 되도록 선언한다. 이에 따라 구조체 변수 today date 의 세 정수 멤버를 저장할 메모리 공간이 예약되는 것이다.


대입된 다음, 적절한 NSLog 를 호출하여 구조체에 담긴 값을 표시한다. NSLog 함수에 넘기기 전에 today.year 를 나눈 나머지가 먼저 계산되어 그해의 마지막 두자리 09 만 표시되도록 하였다. NSLog 에서 사용된 %.2i 포맷문자는 최소 두문자가 표시되도록 지정하여 연도에 0 이 표시되도록 한다.
대입된 다음, 적절한 NSLog 를 호출하여 구조체에 담긴 값을 표시한다. NSLog 함수에 넘기기 전에 today.year 를 나눈 나머지가 먼저 계산되어 그해의 마지막 두자리 09 만 표시되도록 하였다. NSLog 에서 사용된 %.2i 포맷문자는 최소 두문자가 표시되도록 지정하여 연도에 0 이 표시되도록 한다.
Line 2,238: Line 2,238:


<syntaxhighlight lang="objc">
<syntaxhighlight lang="objc">
int (*fnPtr) (void];
int (*fnPtr) (void);
</syntaxhighlight>
</syntaxhighlight>


Line 2,438: Line 2,438:
'복합 리터럴'은 초기화 목록이 뒤따르는, 괄호 안에 표시된 형 이름이다. 이것은 지정된 형의 이름없는 값을 생성한다. 블록 안에서 정의되면 그 범위는 블록 내로 제한된다. 만일 모든 블록 바깥에서 정의되었다면 전역 범위가 된다. 전역 범위일 경우 초기화 값들은 모두 상수 표현식이어야 한다.
'복합 리터럴'은 초기화 목록이 뒤따르는, 괄호 안에 표시된 형 이름이다. 이것은 지정된 형의 이름없는 값을 생성한다. 블록 안에서 정의되면 그 범위는 블록 내로 제한된다. 만일 모든 블록 바깥에서 정의되었다면 전역 범위가 된다. 전역 범위일 경우 초기화 값들은 모두 상수 표현식이어야 한다.


예를한번보자.
예를한번 보자.


<syntaxhighlight lang="objc">
<syntaxhighlight lang="objc">

Latest revision as of 06:14, 3 August 2013

13장
하부 C 언어기능

13장 :: 하부 C 언어기능

이 장에서 설명하는 Objective-C 기능은 Objective-C 프로그램을 작성하는 데 필수는 아니다. 사실, 이런 기능은 대부분 하부 C 프로그래밍 언어에서 왔다. 함수, 구조체, 포인터, 공용체, 배열은 필요에 따라 배우는 것이 좋다. C 언어 자체는 절차적 언어이기 때문에 C의 일부 기능은 객체지향 프로그램을 배우는 데 방해가 된다. 또한 C 는 Foundation 프레임워크에 구현된 메모리 할당 기법이나 멀티바이트 분자를 포함하는 스트링 작업 같은 몇몇 전략에 훼방을 놓을 수도 있다.


objc2_notice_01
C 언어에도 멀티바이트 문자를 다루는 방법이 있지만, Foundation 프레임워크의 NSString 클래스에서 더 우아한 해결법을 제시한다.


반면 어떤 프로그램은 성능상의 이유로 저수준 접근 방식을 써야 할 수도 있다. 예를 들어, 만일 거대한 데이터 배열을 다루고 있지 않다면 Foundation 의 배열 객체 대신 C 에 포함된 배열 데이터 구조(1 5장 「숫자, 스트링, 컬렉션」 에서 다룬다)를 쓰고 싶을 것이다. 반복되는 작업을 그룹으로 묶거나 프로그램을 모듈화하는 데 함수를 유용하게 사용할 수도 있을 것이다.

이 장의 내용은 한 번 훝어보고 나중에 2부 「Foundation 프레임워크」를 다 읽고서 다시 살펴보자. 아니면 전부 건너뛰고 Foundation 프레임워크를 다루는 2부로 바로 넘어가도 된다. 다른 사람의 코드를 지원해야 하거나, Foundation 프레임워크의 헤더파일을 파고들어 가야할때, 이 장에서 다루는 구조를 보게 될 것이다. 그 때 이 장으로 돌아와서 이해해야 하는 개념을 설명한 부분을 읽으면 된다. NSRange, NSPoint, NSRect 와 같이 Foundation 의 일부 데이터 형은 구조체에 대한 기본적인 이해가 있어야 다룰 수 있다. 이 장에 구조체에 대한 설명이 있다.


배열

Objective-C 에서는 정렬된 데이터 항목의 모음인 '배열'을 정의할 수 있다. 이 절에서는 배열을 정의하고 다루는 방법을 설명한다. 함수, 구조체, 문자열, 포인터와 함께 배열을 사용하는 방법도 배울 것이다.

컴퓨터가 성적의 모음을 읽고, 이 성적들에 어떤 작업을 수행한 다음 오름차순으로 순위를 매기고, 평균을 계산하거나 증가값을 구한다고 하자. 모든 성적을 입력하기 전에는 이런 작업을 수행할 수 없을 것이다.

Objective-C 언어에서 성적 하나의 값이 아니라 전체 성적을 나타내는 grade 라는 변수를 정의할 수 있다. '인덱스' 혹은 '첨자 연산자' 를 사용하여 이 모음안의 개별 성분을 참조한다. 수학에서 첨자 연산자 변수 xi 로 집합에서 i 번째 원소를 표현하듯Objective-C 에서도 다음과 같이 표현한다.

x[i]


따라서 다음 표현식은 grades 라는 변수에서 원소 번호 5 를 나타낸다.

grades[5]


Objective-C 에서 배열 원소는 0번부터 시작한다. 따라서 다음은 배열의 첫 원소를 나타낸다.

grades[0]


또한 배열의 각 원소를, 일반적인 변수가 사용되는 곳이면 어디서든 사용할 수 있다. 예를 들어, 다음 명령문과 같이 배열의 값을 다른 변수에 할당할 수 있다.

9 = grades[50];


이 명령문은 grades[50] 에 들어 있는값을 g 에 대입한다. 다음 명령문은 더 일반적인 형태로, i 가 정수 변수로 선언되었다면 배열 grades 의 원소 번호 i 에 들어 있는 값이 변수 g에 할당된다.

9 = grades[i];


배열 원소를 등호 왼쪽에 놓아서 배열 원소의 값을 지정해줄 수 있다. 다음 명령문을 보자.

grades[100] = 95;


여기서 값 95는 grades 배열의 100번째 원소에 저장되었다.

배열의 첨자 연산자로 사용되는 변수의 값을 바꿔가면서 변수 내 원소에 차례대로 접근할 수 있다. 따라서 다음 for 문은 grades 배열에서 처음부터 100 번째에 이르는 원소(0-99)를 순차적으로 접근하여 각 성적의 값을 sum 에 더한다.

for ( i = 0; i < 100; ++i )
    sum += grades[i];


for 문이 종료되면, 변수 sum 은 grades 배 열의 맨 앞부터 100번째까지 이르는 값의 합을 담는다(반복문이 시작되기 전 sum의 값이 0이었다고 가정한다).

다른 데이터 형의 변수들과 마찬가지로 배열도 사용하기 전에 선언해야만 한다. 변수 선언은 변수에 포함될 원소의 데이터 형 (int나 float 혹은 객체 같은)을 선언하고, 배열에 저장될 원소의 최대 개수를 지정해 주는 일이다.

다음은 fracts가 분수 100 개를 담는 배열이 되도록 정의한다.

Fraction *fracts [100];


인덱스 0부터 99까지 사용하여 이 배 열에 담긴 원소를 참조할 수 있다.

다음 표현식은 Fraction의 add: 메서드를 사용하여 fracts 배열에 등장하는 처음 두 분수를 더하고, 그 결과를 배열의 세번째 위치에 저장한다.

fracts[2] = [fracts[0] add: fracts[1]];


프로그램 13.1은 피보나치 수를 15 개 생성한다. 출력 결과가 어떻게 나을까? 표에 담긴 각 수 사이에는 어떤 관계가 존재하는가?


프로그램 13.1


// 첫 15개의 피보나치 수를 생성하는 프로그램
#import <Foundation/Foundation.h>

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

    Fibonacci[0] = 0; /* 정의에 의함 */
    Fibonacci[1] = 1; /* 마찬가지임 */

    for ( i = 2; i < 15; ++i )
    Fibonacci[i] = Fibonacci[i-2] + Fibonacci[i-1];

    for ( i = 0; i < 15; ++i )
    NSLog (@"%i", Fibonacci[i]);

    [pool drain];
    return 0;
}

프로그램 13. 의 출력결과


0
1
1
2
3
5
8
13
21
34
55
89
144
233
377


첫 번째 피보나치 수와 두 번째 피보나치 수인 F0 와 F1 은 각각 0 과 1 로 정의되어있다. 그 후 이어지는 피보나치 수 Fi 는 앞선 피보나치 수 두 개, Fi-2 와 Fi-1 을 더한 값으로 정의된다. 그러므로 F2 는 F0 와 F1 을 더한 값으로 계산된다. 이것은 앞의 프로그램에서 Fibonacci[0] 와 Fibonacci[1] 을 더해 Fibonacci[2] 를 계산하는 것과 대응된다. 이 계산은 for 문안에서 F2 부터 F14 까지의 값을 계산한다(혹은 Fibonacci[2] 에서 Fibonacci[14] 까지의 값을 계산한다).


배열 원소 초기화하기

변수를 선언할 때 초기값을 대입하는 것처럼 배열의 원소에도 초기값을 대입할 수 있다. 그저 배열의 첫 번째 원소부터 초기값을 나열해 주면 된다. 값들은 쉼표로 구분하며, 전체 목록은 중괄호로 감싼다.

다음명령문을보자.

int integers[5] = { 0, 1, 2, 3, 4 };


이 명령문은 integers[0] 의 값을 0, integers[1] 의 값을 1, integers[2] 의 값을 2 이런식으로 계속 설정한다.

문자배열도 비슷한 방식으로 초기화할 수 있다.

char letters[5] = { 'a' , 'b' , 'c' , 'd' , 'e' };


이 명령문은 문자배열 letters 를 정의하고 원소 다섯 개를 각각 문자 a, b, c, d, e 로 설정한다.

배열의 전체 원소를 모두 초기화할 필요는 없다. 만일 전체 크기보다 적은 수로 초기값을 지정하면, 해당 원소들만 초기화되고 나머지는 0 으로 설정된다. 따라서 다음 선언에서는 simple_data 의 첫 원소 세 개만 100.0, 300.0 , 500.5 로 초기화되고 나머지 497 개는 0 으로 초기화된다.

float sample_data[500] = { 100.0, 300.0, 500.5 };


원소 번호를 대괄호 안에 입력하면, 지정한 원소를 순서에 상관없이 초기화할 수 있다. 예로, 다음코드를 보자.

int x = 1233;
int a [] = { [9] = x + 1, [2] = 3, [1] = 2, [0] = 1 };


이 코드는 원소를 10개 갖는(배열의 가장큰 인덱스로 알 수 있다) 배열 a 를 정의하고 마지막 원소가 x + 1 의 값(1234)을 갖도록 초기화한다. 게다가 첫 세 원소의 값을 각각 1, 2, 3 으로 초기화한다.


문자 배열

프로그램 13.2 는 문자배열을 사용하는 법을 보여준다. 여기서 한가지 논의할만한 사항이 있다. 무엇인지 찾아보자.


프로그램 13.2


#import <Foundation/Foundation.h>

int main (int argc, char *argv[])
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    char word[] = { 'H', 'e', 'l', 'l', 'o', '!'};
    int i;

    for ( i = 0; i < 6; ++i )
        NSLog (@"%c", word[i]);

    [pool drain];
    return 0;
}

프로그램 13.2의 출력결과


H
e
l
l
o
!


이 프로그램에서 주의 깊게 살펴볼 요점은 문자배열 word 의 선언이다. 이 배열은 원소가 몇 개 들어가는지 언급하지 않았다. Objective-C 에서는 원소 개수를 지정하지 않고도 배열을 선언할 수 있다. 이 경우, 배열 크기는 초기화되는 원소 개수를 기반으로 해서 자동 결정된다. 프로그램 13.2 에서 배열 word 가 초기화 값을 여섯 개 가지고 있으므로, Objective-C 는 자동으로 배열 크기를 여섯 칸으로 지정한다.

배열이 정의될 때 배열에 들어있는 모든 원소를 초기화 한다면 이 방법도 아무 문제없이 동작한다. 만일, 선언할 때 모든 원소를 초기화 할 수 없다면 배열 크기를 명시적으로 지정해 주어야 한다.

만일 문자 배 열의 끝에 종료 널 문자('\0')를 넣어 주면, '문자열'이 만들어진다. 프로그램 13.2 의 word 초기화 코드를 다음과 같이 바꿔보자.

char word[] = { 'H', 'e', 'l' 'l', 'o', '!', '\0' };


다음과 같은 NSLog 호출로 이 스트링을 표시할 수 있다.

NSLog (@"%s", word) ;


% 포맷 문자는 NSLog 에게 종료 널 문자가 나을 때까지 분자를 표시하라고 알려 준다. 이 문자는 word 배열의 마지막에 넣은문자다.


다차원 배열

지금껏 본 배열은 모두 선형 배열로,하나의 차원만 다룬다는 의미가 된다. Objective-C 에서는 다차원 배열도 정의할 수 있다. 이 절에서는 이차원 배열을 살펴보겠다.

이차원 배열을 사용하는 가장 자연스러운 적용은 행렬이다. 다음 4x5 행렬을 살펴보자.

10 5 -3 17 82
9 0 0 8 -7
32 20 1 0 14
0 0 8 7 6


수학에서 행렬의 원소는 보통 첨자 연산자 두 개를 사용하여 표시된다. 예컨대 위의 행렬을 M 이라고 칭할 때, Mi,j 라고 표기하면 i번째 행(1 부터 4 까지), j번째 열(1 부터 5 까지)에 있는 원소를 나타낸다. M3,2 라고 표기하면 행렬의 셋째 행, 둘째 열에 있는 값인 20 을 나타낸다. 이와 마찬가지로, M4,5 는 행렬의 넷째 행 , 다섯째 열에 있는 원소(값 6)를 나타낸다.

Objective-C 는 유사한 표기법을 사용하여 이차원 배열의 원소를 나타낸다. 그러나 Objective-C 에서는 숫자가 0 부터 시작하므로 행렬의 첫 행과 첫 열은 0 으로 시작한다. 위 행렬은 다음 도표로 행과 열을 표시할 수 있다.

Row(i) Column(j)
0 1 2 3 4

0 10 5 -3 17 82
1 9 0 0 8 -7
2 32 20 1 0 14
3 0 0 8 7 6


수학에서는 Mi, j로 표기하고, 에ec디veζ에서는 다음과 같이 표기한다.

M[i][j]


첫 인덱스는 행을, 두 번째 인덱스는 열을 나타낸다. 따라서 다음명령문은, 0 행 2 열에 있는 값(-3)과 2행 4열에 있는 값(14)을 더해 변수 sum에 결과 값 11을 대입한다.

sun = M[0][2] + M[2][ 4];


이차원 배열도 일차원 배열과 동일한 방식으로 선언된다.

int M[4][5];


이 코드는 배열 M을 4행 5 열, 즉 원소 20개짜리 이차원 배열로 선언한다. 배열안의 각 위치는 정수값을 담도록 정의되었다.

이차원 배열은 일차원 배열과 유사한 방식으로 초기화한다. 초기화의 인수를 나열할 때 같은 행끼리 묶어서 나열한다. 초기화 값 목록을 담은 각 행은 중괄호로 묶어 구별한다. 따라서, 앞의 표에 있는 원소로 배열 M 을 정의하고 초기화 하려면 다음과 같이 명령문을 작성한다.

int M[4][5] = {
            { 10, 5 , -3, 17, 82 },
            { 9 , 0 , 0, 8, -7 },
            { 32, 20, 1, 0, 14 },
            { 0, 0, 8, 7, 6 }
};


이 명령문의 문법을 주의 깊게 살펴보자. 행이 끝날 때마다 중괄호를 사용하고 그 뒤에 쉼표를 불이는데, 마지막 행에는 쉼표가 붙지 않는다. 사실, 행을 구분하기 위해 중괄호를 쓰는 것은 선택하기 나륨이다. 만일 중괄호가 없어도 초기화는 행을 따라 진행된다. 따라서 앞의 명령문은 다음과 같이 작성할수도 있다.

int M[4][5] = { 10, 5, -3, 17, 82 , 9, 0, 0, 8, -7, 32 ,
                20, 1, 0, 14 , 0, 0, 8, 7, 6};


일차원 배열과 마찬가지로 배열 전체를 초기화할 필요는 없다. 다음 명령문은 각 행에서 처음부터 세 번째까지만 지정한 값으로 초기화한다.

int M[4][5] = {
            { 10, 5, -3 },
            { 9, 0, 0 },
            {32 , 20 , 1},
            { 0, 0, 8 }
};


나머지 값들은 0 으로 설정된다. 이 경우, 정확히 초기화해 주려면 내부 중괄호를 사용해야만 한다. 그렇지 않으면 처음 두 행과 세 번째 행의 둘째 원소까지만 지정한 값으로 초기화 될 것이다(직접 확인해 보자).


함수

지금껏 모든 프로그램에서 사용한 NSLog 루틴은 함수의 한 예다. 사실, 모든 프로그램에서 main 이라는 함수도 사용했다. 맨 처음 작성한, 터미널에 'Programmming is fun.'을 표시하는 프로그램으로 돌아가자(프로그램 2.1).

#import <Foundation/Foundation.h>

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

    NSLog (@"Programming is fun.");
    [pool drain];
    return 0;
}


printMessage 라는 다음 함수는 동일한 출력 결과를 생성한다.

void printMessage (void)
{
    NSLog (@"Programming is fun.");
}


printMessage 함수와 프로그램 2.1에 나온 main 함수 사이의 유일한 차이점은 첫째 줄에 존재한다. 함수 정의에서 첫째 줄은 컴파일러에게 함수에 대한 네 가지 정보를 이야기해 준다.

  • 누가 이 함수를 호출할 수 있는가?
  • 함수가 반환하는 값의 형은 무엇인가?
  • 함수 이름은 무엇인가?
  • 함수가 받는 인수의 숫자와 형은 무엇인가?


printMessage 함수의 정의 부분에서 첫째 줄은 컴파일러에게 printMessage 가 함수 이름이며 아무값도 반환하지 않는다(키워드 void가 처음사용되었다)는 것을 알려 준다. 메서드와 달리, 함수의 반환 형은 괄호 안에 집어넣지 않는다. 만일 그렇게 하면 컴파일러 오류메시지를 보게 된다.

컴파일러에게 printMessage 가 값을 반환하지 않는다는 것을 알려 준 다음, void 키워드가 두 번째로 사용되어 인수도 받지 않음을 나타낸다.

main 은 Objective-C 시스템에서 특별하게 인식되는 이름으로,프로그램은 늘 이곳에서 시작한다. 언제나 main이 하나 있어야 한다. 따라서 앞의 코드에 main 함수를 추가하여 프로그램 13.3 과 같이 완성된 프로그램을 만들 수 있다.


프로그램 13.3


#import <Foundation/Foundation.h>

void printMessage (void)
{
    NSLog (@"Programming is fun.");
}

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

    printMessage ();
    [pool drain];
    return 0;
}

프로그램 13.3의 출력결과


Programming is fun.


프로그램 13.3 은 printMessage 와 main 이라는 두 함수로 구성된다. 앞서 언급했듯이, 함수를 호출하는 개념은 새로운 것이 아니다. printMessage는 인수를 받지 않기 때문에, 함수 이름 뒤에 열고 닫는 괄호만 추가해주면 함수를 부를 수 있다.


인수와 지역 변수

5장 「프로그램 반복문」 에서 삼각수를 계산하는 프로그램을 개발했다. 삼각수를 계산하는 함수를 정의하고 이름을 calculateTriangularNumber 라고 붙였다. 이 함수는 인수를 받아서 어떤 삼각수를 계산할지 정한다. 그 후 원하는 값을 계산하여 결과를 표시한다. 프로그램 13.4 는 이 작업을 수행하는 함수와 이를 확인할 main 루틴이다.


프로그램 13.4


// n번째 삼각수를 계산하는 함수

void calculateTriangularNumber (int n)
{
    int i, triangularNumber = 0;

    for ( i = 1; i <= n; ++i )
        triangularNumber += i;

    NSLog (@"Triangular number %i is %i", n, triangularNumber);
}

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

    calculateTriangularNumber (10);
    CalculateTriangularNumber (20);
    calculateTriangularNumber (50);

    [pool drain];
    return 0;
}

프로그램 13.4 의 출력결과


Triangular number 10 is 55
Triangular number 20 is 210
Triangular number 50 is 1275


이 코드는, 컴파일러에게 calculateTriangularNumber 가 아무런 값도 반환하지 않고(키워드 void) int 형 인수 n 하나만 받는 함수라고 알려 준다. 메서드에서 하듯이 괄호 안에 인수 형을 쓰지 않는다는 점에 다시 한 번 주의하자.

여는 중괄호({)는 함수 정의가 시작된다는 뜻이다. n 번째 삼각수를 계산하려 하므로, 현재 계산되는 삼각수의 값을 저장해 둘 변수률 준비해야 한다. 또한, 반복문의 인덱스 역할을 맡을 변수도 필요하다. triangularNumber 와 i 라는 변수는 이런 목적으로 정의되고 int 형으로 선언되었다. 이 변수들은 이전 프로그램의 main 루틴에서와 동일한 방식으로 정의하고 초기화해 준다.

함수 내 지역 변수는 메서드와 마찬가지로 동작한다. 함수에 들어 있는 변수에 초기값이 주어진다면, 함수가 호출될 때마다 변수에 이 초기값이 대입된다.

함수 내에서 정의된 변수는 (메서드에서와 마찬가지로) 함수가 호출될 때마다 자동으로 생성되고, 그 값이 함수 내에서만 지역적으로 존재한다. 즉, 함수 내 변수는 '자동지역변수'다.

'정적 지역 변수'는 키워드 static 으로 선언하고, 함수가 여러 번 호출되더라도 그값을 계속 유지하고 기본 초기값으로 0 을 갖는다.

지역 변수의 값은 변수가 정의된 함수내에서만 접근할 수 있다. 함수외부에서는 이 값에 직접 접근할 방법이 없다.

프로그램 예제로 돌아가자. 지역 변수가 정의된 후에 함수는 삼각수를 계산하고 결과를 터미널에 표시한다. 그 후닫는 중괄호(})를 달아 함수의 끝을 정의한다.

main 루틴에서 calculateTriangularNumber를 처음 호출할 때 인수로 값 10이 넘겨진다. 실행은 바로 함수로 넘어가 10 은 함수 내 매개변수 n의 값이 된다. 그 후 함수는 10 번째 삼각수를 계산하고 결과를 표시한다.

calculateTriangularNumber 가 다시 호출될 때는 인수로 20 이 넘겨진다. 앞에서 설명한 절차와 비슷하게 이 값은 함수 내 n 의 값이 된다. 그 후 함수는 20 번째 삼각수를 계산하고, 답을 표시한다.


함수 결과 반환하기

메서드와 마찬가지로 함수도 값을 반환할 수 있다. return 문으로 반환되는 값의 형은 함수에 대해 선언한 반환 형과 일치해야 한다. 다음과 같이 시작하는 함수선언을 보자.

float kmh_to_mph (float km_speed)


이 함수는 float 형 인수 km_speed 를 하나 받고, 부동소수점 값을 반환한다.

int gcd (int u, int v)


이 코드도 정수 인수인 u와 v를 받고 정수 값을 반환하는 함수 gcd 를 정의한다.

프로그램 5.7 에서 사용한 최대공약수 알고리즘을 함수 형태로 다시 작성해 보자. 함수의 두 인수로는 최대공약수(gcd)를 계산하려는 두 수를 쓴다(프로그램 13.5 를 보라).


프로그램 13.5


#import <Foundation/Foundation.h>

// 이 함수는 두 양의 정수 사이의 최대공약수를 구해 결과를 반환한다.

int gcd (int u, int v)
{
    int temp;

    while ( v != 0 )
    {
        temp = u % v ;
        u = v;
        v = temp;
    }

    return u;
}

main ()
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    int result;

    result = gcd (150, 35);
    NSLog (@"The gcd of 150 and 35 is %i", result);

    result = gcd (1026, 405);
    NSLog (@"The gcd of 1026 and 405 is %i", result);

    NSLog (@"The gcd of 83 and 240 is %i", gcd (83, 240));
    [pool drain];
    return 0;
}

프로그램 13.5 의 출력결과


The gcd of 150 and 35 is 5
The gcd of 1026 and 405 is 27
The gcd of 83 and 240 is 1


gcd 함수는 정수 인수를 두 개 받는다. 함수는 이 인수들을 자신의 공식 매개변수 이름인 u 와 v 로 참조한다. 변수 temp 를 int 형으로 선언한 후, 프로그램은 인수 u 와 v 의 값을 적절한 메시지와 함께 터미널에 표시한다. 그 후 함수는 두 수의 최대공약수를 계산하여 반환한다.

result = gcd (150, 35);


이 명령문은 함수 gcd 를 인수 150 과 35 으로 호출하여 함수가 반환하는 값을 변수 result 에 저장한다.

만일 함수 선언에서 반환 형이 빠져 있다면, 함수가값을 반환할 경우 정수라고 가정한다. 많은 프로그래머들이 이 사실을 활용하여 정수 값을 반환하는 함수를 선언할 때 반환 형을 생략한다. 그러나 이것은 올바른 프로그래밍 습관이 아니므로 피하는 것이 좋다. 컴파일러는 기본 반환 형이 int로. 설정되었다고 경고할 것이고, 경고란 무엇인가 잘못했다고 지적하는 메시지다!

함수의 기본반환 형은 메서드와 다르다. 메서드의 반환형이 지정되지 않았을때, 컴파일러는 id형의 값을 반환한다고 가정한다. 다시 한 번 말하지만, 이런 특성에 기대지 않고 메서드의 반환 형을 언제나 선언해 주는 것이 좋다.


반환형과 인수형 선언하기

앞서 이미 언급하였지만, Objective-C 컴파일러는 기본적으로 함수가 int 형 값을 반환한다고 가정한다. 좀더 상세히 설명하면 함수가 호출될 때마다, 컴파일러는 다음중 어디에도 해당하지 않으면 함수가 int 형을 반환한다고 가정한다.

  • 함수는 호출되기 전에 프로그램 내에서 정의되었다.
  • 함수에 의해 반환되는 값이 함수호출전에 정의되었다. 함수의 반환형과 인수형을 선언하는것을 '프로토타입' 선언이라고 한다.


이 함수 선언은 함수의 반환 값을 선언하는 데 사용할 뿐 아니라, 컴파일러에게 함수가 인수를 얼마나 많이 받는지, 또 그 인수의 형들이 무엇인지 알려 주는 용도로도 쓴다. 마치 클래스를 새로 정의할 때 @interface 부분에서 메서드를 선언하는것과 유사하다.

absoluteValue 라는 함수가 float 형의 값을 반환하고, 마찬가지로 float 값인 인수를 하나 받는다고 선언하려면 다음과 같은 프로토타입 선언을 사용할수 있다.

float absoluteValue (float);


여기서 볼수 있듯, 괄호 안에 인수 이름이 아니라 인수의 형만 지정해 주어야 한다. 원한다면 다음과 같이 선택적으로 형 다음에 견본 이름을 지정해줄 수 있다.

float absoluteValue (float x);


이 이름은 함수를 정의할 때 사용할 이름과 동일할 필요가 없다. 컴파일러가 프로토타입 선언의 인수명은 무시하기 때문이다.

프로토타입 선언을 작성할 때 아주 간단한 방법은, 실제 함수 정의 부분에서 첫줄을 복사해 오는 것이다. 복사한 함수 정의의 마지막에 세미콜론을 잊지 말고 꼭 붙이자.

(NSLog 나 scanf 같이) 함수가 받는 인수의 개수가 가변적인 경우 이를 컴파일러에게 알려 주어야만 한다. 다음 선언을 보자.

void NSLog (NSString *format, ... );


이 선언은 컴파일러에게 NSLog가 첫 인수로 NSString 객체를 받고 (...을 사용하므로) 그 뒤에 인수가 몇 개든 올 수 있다고 알리는 의미다. NSLog 는 특수 파일인 Foundation/Foundation.h 에 선언되었다. 이 때문에 각 프로그램의 맨 앞줄에 다음코드를 넣어 주어야한다.[1]

#import <Foundation/Foundation.h>


이 부분이 없다면 컴파일러는 NSLog 가 인수를 고정된 개수로 받는다고 가정한다. 그 결과로 부정확한 코드가 생성될 것이다.

컴파일러는 함수가 호출되기 전에 함수 정의에서 인수형을 지정했거나 함수와 인수 형을 미리 선언한 경우에만 숫자 인수들을 적절한 형으로 자동 변환한다.

함수에 대한 몇 가지 조언과 제안을 살펴보자.

  • 기본적으로 컴파일러는 함수가 int 형을 반환한다고 가정한다 .
  • int 형을 반환하는 함수를 정의할때는 그대로 정의한다.
  • 값을 반환하지 않는 함수를 정의할때는 void 로 정의한다.
  • 컴파일러는 미리 정의되거나 선언된 함수에 대해서만 인수를 함수가 기대하는 것들로 변환한다.


비록 함수 호출 전에 함수 정의가 나오더라도, 조금 더 안전한 코드를 작성하자는 의미로, 모든 함수를 선언해 주자(함수 정의를 다른 곳이나 다른 파일로 옮길수도 있다). 함수의 선언 부분은 헤더파일에 두고, 이 파일을 모듈에 임포트해 주는 방법도 좋다.

함수는 기본적으로 '외부적 (external)'이다. 즉, 그 함수에 연결된 파일에 담겨 있는 함수나 메서드라면, 그 함수를 호출할 수 있다는 의미다. 이게 싫다면, 함수를 정적(static)으로 만들어 범위를 제한할 수 있다. 다음과 같이 키워드 static 을 함수 선언 앞에 적어주면 된다.

static int gcd (int u, int v)
{
    ...
}


정적 함수는 함수 정의가 담긴 파일에 들어 있는다른 함수와 메서드에 의해서만 호출할 수 있다.


함수, 메서드, 배열

함수나 메서드에 배열의 원소하나를 건네주려면 그냥 원소하나를 일반적인 방식으로 넘겨주면 된다. 만일 squareRoot 가 제곱을 하는 함수라고 해보자. averages[i]를 제곱하여 그 결과를 sq_root_result 에 저장하고 싶다면 다음과 같은 명령문을 작성한다.

sq_root_result = squareRoot (averages[i]);


배열 전체를 함수나 메서드에 넘기는 일은 완전히 다른 문제다. 배열을 넘기려면 아무런 첨자 연산자 없이 배열 이름만 함수나 메서드 호출 내에 적어 주면 된다. 예를 들어, grade_scores 를 원소 100 개짜리 배열로 선언했다고 하자.

minimum (grade_scores)


이 표현식은 grade_scores 에 든 원소 100 개 전체를 minimum 함수에 전부 건넨다. minimum 함수는 한 배열 전체가 인수로 넘어오리라 예상하고, 공식 매개변수를 적절하게 선언해야만 한다.

이 함수는 지정된 수의 배열에서 최소정수값을 찾는다.

// 배열에서 최솟값을 찾는 함수

int minimum (int values[], int numElements)
{
    int minValue, i;

    minValue = values[0];

    for ( i = 1; i < numElements; ++i )
        if ( values[i] < minValue )
            minValue = values[i];

    return (minValue);
}


함수 minimum 은 두 인수를 받도록 정의되었다. 먼저 최솟값을 찾을 배열이 넘어 오고 그 다음으로, 배열의 원소 수가 넘어온다. 함수 헤더에 나오는 values 바로 뒤의 여닫는 대괄호([ ])는 Objective-C 컴파일러에게 values 가 정수 배열임을 알려준다. 컴파일러는 이 배열이 얼마나 큰지는 전혀 궁금해하지 않는다.

공식 매개변수인 numElements 는 for 문에서 한계치 역할을 한다. 즉, for 문은 values[1] 부터 배열의 마지막 원소인 values[numElements - 1]까지 순차적으로 처리된다.

함수나 메서드에서 배열 원소의 값을 바꾼다면, 바로 그 함수나 메서드에 건네지는 원래 배열이 바뀌게 된다. 함수나 메서드의 실행이 완료된 후에도 배열에 적용된 이 수정은 계속 유지된다.

배열은 왜 간단한 변수나 배열 원소와 다르게 동작하는 것일까? 여기에는 설명이 살짝 필요하다. 간단한 변수와 배열 원소는 함수나 메서드에서 값을 바꿀 수 없다. 함수나 메서드가 호출될 때, 인수로 넘겨지는 값은 해당하는 공식 매개변수로 복사된다고 언급했다. 이 말은 아직 유효하다. 그러나 배열을 다룰 때는 공식 매개변수 배열에 넘겨지는 배열의 전체 내용물이 복사되지 않는다. 대신 배열이 있는 컴퓨터의 메모리를 가리키는 포인터가 넘어간다. 따라서, 공식 매개변수 배열에 생기는 변화는 배열의 사본이 아니라 원본에서 일어나는 것이다. 그러므로 함수나 메서드가 반환할때 이 변화도 계속 효과를 미치게 된다.


다차원 배열

일반적인 변수나 일차원 배열 원소와 마찬가지로 다차원 배열 원소도 함수나 메서드에 건네줄 수 있다.

result = squareRoot (matrix[i][j]);


이 명령문은 squareRoot 함수에 matrix[i][j] 에 포함된 값을 인수로 건네준다. 일차원 배열처럼 다차원 배열도 배열 전체를 인수로 넘길 수 있다. 예를 들어, 행렬 measuredValues 가 정수의 이차원 배열이라고 해보자.

scalarMultiply (measuredValues, constant);


이 Objective-C 명령문은 행렬의 각 원소에 constant 의 값을 곱하는 함수를 호출한다. 물론 여기서도 함수가 measuredValues 배열에 담긴 값을 바꿀 수 있음을 암시한다. 일차원 배열에서 이 주제에 대해 논의했던 내용은 여기서도 적용된다. 함수내부에 있는 공식 매개변수 배열의 원소에 값을 대입하면 함수에 건네진 배열에 영속적인 변화가 생긴다.

다시 한 번 말하지만, 일차원 배열을 공식 매개변수로 선언할 때, 배열의 실제차원까지 알릴 필요는 없다. 그저 Objective-C 컴파일러에게 매개변수가 배열임을 알리기 위해 빈 대괄호만 붙여 주면 된다. 반면 다차원 배열에는 차이점이 있다. 이차원 배열의 경우,선언할 때 배열 행의 개수를 생략할 수는 있지만, 배열 열의 개수는 반드시 적어 줘야한다.

다음 두 선언을 보자.

int arrayValues[100][50]
int arrayValues[][50]


두 가지 모두 100 행 50 열짜리 공식 매개변수 배열인 arrayValues 를 선언한 것이다. 둘다 유효한 선언이다. 그러나 다음 두 선언은 배열의 열을 지정하지 않았기 때문에 유효하지 않다.

int arrayValues[100][]
int arrayValues[][]


구조체

Objective-C 에는 원소들을 묶을 수 있는 다른 도구가 배열 외에도 있다. 이 절의 주제인 구조체 역시 원소들을 그룹 짓는다.

7/18/09(2009년 7월 18일)이라는 날짜를 프로그램 출력 결과의 표제로 표시하거나, 이 날짜를 가지고 무언가 계산하기 위해 프로그램에서 저장한다고 하자. 날짜를 저장하려면 당연히 달(month)은 정수 변수 month로, 일(day)은 정수 변수 day로, 년은 정수 변수 year로 할당하는 방법을 쓸 것이다. 따라서 다음 명령문도 아무 문제 없이 동작할 것이다.

int month = 7, day = 18, year = 2009;


이 방법도 물론 사용할 수 있다. 그런데 프로그램에서 날짜를 여러 개 저장해야 하는 경우는 어떨까? 이런 경우이 변수 세 개를 묶어 하나의 모음으로 만들 수 있다면 훨씬 편할것이다.

Objective-C 에서 년, 월, 일 을 나타내는 구성 요소 세 개로 된 date 라는 구조체를 만들 수 있다. 이런 구조체를 정의하는 방법은 꽤 간단하다.

struct date
{
    int month;
    int day;
    int year;
};


date 구조체는 month, day, year 라도 세 정수 멤버를 갖는다. date의 정의는 본질적으로 언어에 새로운 형을 추가하는 것이다. 다음정의와같이 struct date형의 변수를 선언할 수 있게 되기 때문이다.

struct date today;


purchaseDate 라는 변수도 동일한 형으로 정의할 수 있다.

struct date purchaseDate


혹은,다음과 같이 두 변수의 정의를 한꺼번에 할 수도 있다.

struct date today, purchaseDate;


데이터 형이 int, float, char 인 변수와 달리, 구조체 변수는 특별한 문법을 써서 다뤄야 한다. 구조체의 멤버에 접근하려면 변수 이름 뒤에 점(. 연산자)과 멤버 이름을 붙여야 한다. 예를 들어, 변수 today 내의 day 값을 21 로 설정하려면 다음과 같은 코드를 작성한다.

today.day = 21;


변수명과 점(dot), 멤버 이름 사이에는 띄어쓰기가 허용되지 않으니, 반드시 기억하자.

잠깐! 이것은 객체의 프로퍼티를 호출할때 사용했던 그 연산자 아닌가? 기억을 더듬어 우리가 다음과 같은 명령문을 작성했던 때를 떠을려 보자.

myRect.width = 12;


이 명령문으로 Rectangle 객체의 세터 메서드(setWidth)를 호출하여 인수로 12 를 넘겨 주었다. 햇갈릴 것은 없다. 컴파일러는 점 연산자 왼쪽이 구조체인지 객체인지를 보고 상황에 맞게 대처한다.

struct date 예제로 돌아가서 today 의 year 를 2010 으로 설정해 주려면 다음 표현식을 작성한다.

today.year = 2010;


마지막으로, month의 값이 12 와 같은지 알아보려면 다음 명령문을 쓸 수 있다.

if ( today.month == 12 )
    next_month = 1;


프로그램 13.6은 이 내용을 실제 프로그램에 적용한 것이다.


프로그램 13.6


#import <Foundation/Foundation.h>

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

    struct date
    {
        int month;
        int day;
        int year;
    };

    struct date today;

    today.month = 9;
    today.day = 25;
    today.year = 2009;

    NSLog (@"Today's date is %i/%i/%.2i.", today.month,
        today.day, today.year % 100);

    [pool drain];
    return 0;
}

프로그램 13.6 의 출력결과


Today's date is 9/25/09.


main 의 첫 명령문은 month, day, year 라는 세 정수 멤버를 갖는 date 구조체를 정의한다. 두 번째 명령문에서 변수 today 가 struct date 형으로 선언된다. 즉, 첫 번째 명령문은 date 구조체가 Objective-C 컴파일러에게 어떻게 보일지를 정의할 뿐이고, 컴퓨터 내부에는 이를 위해 어떤 저장 공간도 예약되지 않는다. 두 번째 명령문은 struct data 형 변수가 되도록 선언한다. 이에 따라 구조체 변수 today date 의 세 정수 멤버를 저장할 메모리 공간이 예약되는 것이다.

대입된 다음, 적절한 NSLog 를 호출하여 구조체에 담긴 값을 표시한다. NSLog 함수에 넘기기 전에 today.year 를 나눈 나머지가 먼저 계산되어 그해의 마지막 두자리 09 만 표시되도록 하였다. NSLog 에서 사용된 %.2i 포맷문자는 최소 두문자가 표시되도록 지정하여 연도에 0 이 표시되도록 한다.

표현식을 평가할 때는 Objective-C 에서 일반적인 변수를 다루는 규칙과 동일하게 구조체 멤버들을 대한다. 다음과 같이 정수 구조체 멤버를다른 정수로 나누면 정수나눗셈이 수행된다.

오늘 날짜를 입력받고 내일 날짜를 표시하는 프로그램을 작성한다고 해보자. 언뜻 보기에는 매우 간단한 작업 같다. 사용자에게 오늘 일자를 입력하도록 요청하고, 다음과 같은 명령문들로 내일 날짜를 계산할수 있을 것이다.

tomorrow.month = today.month;
tomorrow.day = today.day + 1;
tomorrow.year = today.year;


위 코드로도 대부분의 날짜 계산은 문제없이 동작한다. 그러나 다음 두 경우는 제대로 처리하지 못한다.

  • 만일 오늘이 그 달의 마지막 날인 경우
  • 만일 오늘이 그 해의 마지막 날인 경우(오늘 날짜가 12월 31 일이라면)


손쉽게 오늘이 달의 마지막 날인지를 계산하는 방법은 매달 며칠이 있는지를 정수 배열로 담아 두는 것이다. 배열을 검색해 특정한 달을 찾아 그 달에 며칠이 있는지 알 수 있다(프로그램 13.7을 보라).


프로그램 13.7


// 내일 날짜를 구하는 프로그램

#import <Foundation/Foundation.h>

struct date
{
    int month;
    int day;
    int year;
};

// 내일 날짜를 계산하는 함수

struct date dateUpdate (struct date today)
{
    struct date tomorrow;
    int numberOfDays (struct date d);

    if ( today.day != numberOfDays (today) )
    {
        tomorrow.day = today.day + 1;
        tomorrow.month = today.month;
        tomorrow.year = today.year;
    }
    else if ( today.month == 12 ) // 해의 마지막 날
    {
        tomorrow.day = 1;
        tomorrow.month = 1;
        tomorrow.year = today.year + 1;
    }
    else
    { // 달의 마지막 날
        tomorrow.day = 1;
        tomorrow.month = today.month + 1;
        tomorrow.year = today.year;
    }

    return (tomorrow);
}

// 매달 며칠이 있는지 찾는 함수

int numberOfDays (struct date d)
{
    int answer;
    BOOL isLeapYear (struct date d);
    int daysPerMonth[12] =
        { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

    if ( isLeapYear (d) == YES && d.month == 2 )
        answer = 29;
    else
        answer = daysPerMonth[d.month - 1];

    return (answer);
}

// 윤년인지 구하는 함수

BOOL isLeapYear (struct date d)
{
    if ( (d.year %4 == 0 && d.year % 100 != 0) ||
            d.year % 400 == 0 )
        return YES;
    else
        return NO;
}

int main (int argc, char *argv[])
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    struct date dateUpdate (struct date today);
    struct date thisDay, nextDay;

    NSLog (@"Enter today's date (mm dd yyyy): ");
    scanf ("%i%i%i", &thisDay.month, &thisDay.day,
            &thisDay.year);

    nextDay = dateUpdate (thisDay);

    NSLog (@"Tomorrow's date is %i/%i/%.2i.", nextDay.month,
            nextDay.day, nextDay.year % 100);

    [pool drain];
    return 0;
}

프로그램 13.7 의 출력결과


Enter today's date (mm dd yyyy):
2 28 2012
Tomorrow's date is 2/29/12.

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


Enter today's date (mm dd yyyy):
10 2 2009
Tomorrow's date is 10/3/09.

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


Enter today's date (mm dd yyyy):
12 31 2010
Tomorrow's date is 1/1/10.


비록 이 프로그램에서 클래스로 작업하지 않지만 Foundation.h 파일을 임포트하여 BOOL 형과 YES, NO 정의(define)을 사용하였다. 이것들은 모두 Foundation.h 에 정의되어 있다.

먼저, date 구조체가 함수 외부에서 나타남에 주의하자. 이는 구조체 정의가 변수와 거의 유사하기 때문이다. 만일 구조체가 특정 함수내에서 정의된다면 그 함수만이 구조체의 존재를 알수있다. 이것은 '지역' 구조체 정의다. 만일 구조체를 함수 바깥에서 정의하면 그 정의는 '전역'이다. 전역 구조체 정의라면 프로그램에서 그 다음에 어떤 변수든 (함수 내부에서도 외부에서도) 그 구조체 형으로 선언될 수 있다. 한 파일의 범위를 넘어 공유되는 구조체 정의는 보통 한 헤더파일에 모아서 정의한다. 그리고 그 구조체를 사용하는 파일들에 임포트된다.

main 루틴 안의 다음 선언을 보자.

struct date dateUpdate (struct date today);


이 선언은 컴파일러에게 dateUpdate 함수가 date 구조체를 인수로 받고 반환도 한다는 것을 알려 준다. 컴파일러는 이미 실제 함수 정의를 파일 앞에서 보았으므로 여기서 선언해줄 필요는 없다. 그러나 선언하는 편이, 좋은 프로그래밍 습관이다. 예를들어, 함수 정의와 main 을 각기 다른 파일로 나누면 이 선언이 필요할것이다.

일반 변수와 마찬가지로, (그리고 배열과달리) 함수가 구조체 인수에 담긴 값에 변화를 가하더라도 원래 구조체에는 아무런 영향이 미치지 않는다. 함수가 호출될 때 생성된 구조체의 사본에만 변화가 생긴다.

날짜가 입력되어 date 구조체 변수 thisDay 에 저장되고 나면, dateUpdate 함수는 다음과 같이 호출된다.

nextDay = dateUpdate (thisDay);


이 명령문은 dateUpdate 에게 thisDay 구조체의 값을 넘기며 이 함수를 호출한다. dateUpdate 함수에 다음 프로토타입 선언이 있다고 해보자.

int numberOfDays (struct date d);


이는 Objective-C 컴파일러에게 numberOfDays 함수가 정수 값을 반환하며 struct date 형 인수를 하나 받는다고 알려 준다.

if ( today.day != numberOfDays (today) )


이 명령문은 구조체 today 가 numberOfDays 함수에 인수로 넘겨진다는 뜻이다. 그 함수 내에서 다음과 같이 적절히 선언하여 시스템에게 구조체를 인수로 받는다고 알려주어야 한다.

int numberOfDays (struct date d)


numberOfDays 함수는 먼저 윤년 여부를 확인하고 2월인지 확인한다. 윤년인지 확인하려면, isLeapYear 라는 함수를 호출하면 된다. isLeapYear 함수는 단순하다. 인수로 넘겨진 date 구조체에 담긴 연도의 값을 테스트하여 윤년이면 YES 를, 그렇지 않으면 NO 를 반환한다.

프로그램 13.7 에서 함수 호출의 계층 구조가 어떻게 되어 있는지 잘 이해하자. main 함수는 dateUpdate 를 호출하고, 이 함수는 numberOfDays 를 호출하고, 이 함수는 다시 isLeapYear 를 호출한다.


구조체 초기화하기

구조체 초기화는 배열 초기화와 비슷하다. 원소들을 중괄호 안에 나열하고 각각 쉼표로 구분한다.

date 구조체 변수인 today 를 2011년 7월 2일로 초기화 하려면, 다음과 같은 명령문을 작성한다.

struct date today = { 7, 2, 2011 };


배열의 초기화와 마찬가지로, 구조체에 담긴 맴버보다 적은 수만 나열하여 초기화할 수도 있다.

struct date today = { 7 };


이 명령문은 today.month 를 7 로 설정하고 today.day 나 today.year 의 초기값은 설정하지 않는다. 이런 경우, 이 멤버들의 초기 기본값은 정의되지 않는다.

다음 표기법을 써서 순서와 상관없이 특정 멤버를 지정하여 초기화할 수도 있다 .

.member = value


다음과 같이 초기화 목록을 작성할 수 있다.

struct date today = { .month = 7, .day = 2, .year = 2011 };

struct date today = { .year = 2011 };


마지막 명령문은 구조체에서 연도를 2011 로 설정하기만 한다. 익히 알겠지만 나머지 값들은 정의되지 않은 채로 남는다.


구조체 배열

구조체 배열을 다루는 것은 꽤 간단하다.

struct date birthdays[15];


이 정의는 birthdays 배열이 struct date 형 원소를 15개 담는다는 뜻이다. 이 배열에 있는 특정 구조체 원소를 참조하는 것도 간단하다. birthdays 배열의 두 번째 생일을 1996 년 2 월 22 일로 설정하려면 다음과 같은 명령문을 작성하면 된다.

birthdays[1].month = 2;
birthdays[1].day = 22;
birthdays[1].year = 1996;


다음 명령문은 numberOfDays 함수에 배열의 첫 번째 날짜를 넘겨 이 날자가 속한 달(month)이 며칠로 구성되어 있는지 알아낸다.

n = numberOfDays (birthdays[0] );


구조체 내의 구조체

Objective-C 로 구조체를 정의하는 작업에는 엄청난 유연성이 있다. 예를 들어, 하나 이상의 다른 구조체를 멤버로 삼는 구조체나 배열이 담긴 구조체도 정의할 수 있다.

우리는 년, 월, 일 을 논리적으로 묶어 date 라는 구조체를 만드는 방법을 배웠다. 유사한 구조체로 time 을 만들어 보자. 시, 분, 초를 묶어 시간을 표시한다고 하자. 어떤 프로그램에서는 시간과 날짜를 논리적으로 묶어야 할 수도 있다. 특정 일자와 시간에 발생한 사건의 목록을 작성해야 할 때가 그럴 것이다.

이 경우 시간과 날짜를 묶을 편리한 방법이 있으면 좋을 것이다. Objective-C 에서는 날짜와 시간을 멤버로 삼는 구조체를 새로(date_and_time 같은 이름으로) 정의하면된다.

struct date_and_time
{
    struct date sdate;
    struct time stime;
};


이 구조체의 첫 멤버는 struct date 형이고 이름이 sdate 다. date_and_time 구조체에서 둘째 멤버는 struct time 형이고 이 이름이 stime 이다. 이 date_and_time 구조체를 정의하려면 date 와 time 구조체가 이미 컴파일러에게 정의되어 있어야 한다.

이제 struct date_and_time 형으로 변수를 정의할 수 있다.

struct date_and_time event;


event 변수에서 date 구조체를 접근하는 데도 동일한 문법을 사용한다.

event.sdate


따라서, 다음 명령문을 작성해서 dateUpdate 함수를 호출할 때 이 날짜를 인수로 삼고 결과를 동일한 곳에 다시 대입할 수 있다.

event.sdate = dateUpdate (event.sdate);


date_and_time 구조체에 담긴 time 구조체도 동일하게 할 수 있다.

event.stime = timeUpdate (event.stime);


이 구조체들 안에 담긴 특정 멤버를 참조하려면 마지막에 점(dot)과 멤버 이름을 불이면 된다.

event.sdate.month = 10;


이 명령문은 event 에 담긴 date 구조체에 포함된 month 를 10 월로 설정해 준다. 그리고 다음 명령문은 time 구조체에 포함된 seconds 에 1 을 더한다.

++event.stime.seconds;


event 변수도 익숙한 방법으로 초기화할 수 있다.

struct date and time event =
        { { 12, 17, 1989 }, { 3, 30, 0 } };


이 코드는 변수 event 의 날짜를 1989 년 12 월 17 일로, 시간을 3:30:OO 으로 설정한다.

다음 선언과 같이 date_and_time 구조체의 배열을 만들수도 있다.

struct date_and_time events[100];


events 배열은 struct date_and_time 형 원소를 100 개 담고 있다. 이 배 열에 4번째로 포함된 date_and_time 은 event[3]을 써서 참조하는 것이 일반적인 방식이다. 그리고 배열의 25번째 날짜를 dateUpdate 함수에 보내려면 다음과 같이 작성한다.

events[24].sdate = dateUpdate (events[24].sdate);


배열의 첫 시간을 정오로 설정하려면 다음 명령문들을 사용한다.

events[0].stime.hour = 12;
events[0].stime.minutes = 0;
events[0].stime.seconds = 0;


구조체에 대한 추가 설명

구조체를 정의할 때는 어느 정도 융통성이 허용된다. 먼저, 변수가 특정 구조체 형임을 선언함과 동시에 그 구조체를 정의할 수도있다. 그저 간단히 구조체 정의를 마치는 세미콜론 앞에 변수이름(들)을 써주기만 하면 된다. 예를들어, 다음 명령문은 date 구조체를 정의함과 동시에 todaysDate, purchaseDate 변수를 이 구조체 형으로 선언한다.

struct date
{
    int month;
    int day;
    int year;
} todaysDate, purchaseDate;


또한, 변수의 초기값을 일반적인 방식으로 대입해줄 수 있다. 다음 코드는 구조체 date 를 정의하고 변수 todaysDate 를 지정한 초기 값으로 설정한다.

struct date
{
    int month;
    int day;
    int year;
} todaysDate = { 9, 25, 2010 };


만일 구조체가 정의될 때 특정 구조체 형의 모든 변수가 함께 정의된다면 구조체 이름을 생략해도 된다. 따라서 다음 명령문은 dates 라는 배열이 원소를 100 개 갖는다고 정의한다.

struct
{
    int month;
    int dayl
    int yearj;
} dates[100];


배 열의 각 원소는 정수 멤버 세 개 month, day, year 를 담은 구조체다. 구조체 이름을 정의해 주지 않았기 때문에, 이후에 동일한형의 변수를 선언할 때 이 구조체를 다시 명시적으로 정의해 주는 수밖에 없다.


비트 필드

Objective-C 에서 정보를 압축하는 방법은 두 가지다. 하나는 그저 데이터를 정수로 표시하고 4장 「데이터 형과 표현식」에 설명했던 것처럼 비트 연산자를 써서 정수에서 원하는 비트에 접근하는 것이다.

다른 방법은 Objective-C 구조인 비트 필드를 사용하여 압축된 정보의 구조체를 정의하는 것이다. 이는 구조체를 정의할 때 특별한 문법을 써서 비트의 필드를 정의하고 그 필드에 이름을 부여하는 방법이다.

이를테면 다음처럼 packedStruct 라는 구조체를 정의하여 비트 필드 할당을 정의하는 것이다.

struct packedStruct
{
    unsigned int f1:1;
    unsigned int f2:1;
    unsigned int f3:1;
    unsigned int type:4;
    unsigned int index:9;
};


packedStruct 구조체는 멤버를 다섯 개 포함하도록 정의되었다. 첫째 멤버 f1 은 unsigned int 형이다. 멤버 이름 바로 뒤에 1 이 따라오는데, 이는 이 멤버가 1 비트에 저장된다는 의미다. f2, f3 플래그도 마찬가지로 길이가 1 비트라고 정의되었다. type 멤버는 4 비트를 차지하도록 정의되었고 index 멤버는 9 비트 길이로 정의되었다.

컴파일러는 자동으로 이 비트 필드 정의를 함께 압축한다. 이 방법을 취하면, packedStruct 형으로 정의된 변수의 필드를 일반적인 구조체 멤버와 같은 방식으로 참조할 수 있다는 장점이 생긴다. 만일 packedData 라는 변수를 다음과 같이 선언했다고 해보자.

struct packedStruct packedData;


다음 명령문을 사용하여 packedData 의 type 필드를 7 로 설정할 수 있다.

packedData.type = 7;


다음 명령문처럼 이 필드를 n 값으로 설정할 수도 있다.

packedData.type = n;


이 경우, n 값이 type 필드에 들어가기에 너무 크지 않을까? 이 점은 염려하지 않아도 된다. n 의 우측 4 비트만 packedStruct.type 에 대입된다.

비트 필드에서 값도 자동으로 추출된다.

n = packedData.type;


따라서 위 명령문을 작성하면 packedStruct 에서 type 필드를 추출하여 (필요에 따라 자동으로 우측 비트시프팅을 하여)n 에 대입한다.

일반적인 표현식에서 비트필드를 사용하면 자동으로 정수로 변환된다. 따라서 다음 명령문은 유효하다. 그 다음코드 또한 유효하다.

i = packedData.index 15+ 1;

if ( packedData.f2 )
    ...


이 표현식은 플래그 f2 가 켜졌는지 아닌지를 테스트한다. 여기서 한 가지는 염두에 두자. 비트 필드는 내부에서 필드가 왼쪽에서 오른쪽으로 할당될지, 아니면 오른쪽에서 왼쪽으로 할당될지를 보장할 수 없다. 따라서, 비트 필드가 오른쪽에서 왼쪽으로 할당된다면 f1 이 맨 우측 비트를 차지할 것이고, f2 는 f1 의 바로 좌측위치를 차지할 것이다. 다른 프로그램이나 머신에서 만든 데이터를 다루지 않는한 이것 때문에 문제가 발생하지는 않을 것이다.

비트 필드가 담긴 구조체에 일반 데이터 형을 포함할 수도 있다. 만일 int, char 와 1 비트 플래그 두 개를 담은 구조체를 정의하려 한다면 다음과 같이 작성하면 된다.

struct table_entry
{
    int count;
    char c;
    unsigned int f1:1;
    unsigned int f2:1;
};


비트 필드는 구조체 정의에 나타나면 '유닛(unit)'으로 압축되고, 유닛 크기는 구현에 따라 달리 정의되는데 보통 워드 크기다. Objective-C 컴파일러는 저장 공간을 최적화 하기 위해 비트필드를 정의한 부분을 재배치하지는 않는다.

이름 없는 비트 필드를 정의할 경우, 워드 내의 비트를 건너뛰게 된다. 다음은 x_entry 구조체가 type 이라는 4 비트 필드와 count 라는 9 비트 필드를 포함하도록 정의한 코드다.

struct x_entry
{
    unsigned int type:4;
    unsigned int :3;
    unsigned int count:9;
};


이름이 붙지 않은 필드는 type 과 count 필드 사이를 3 비트 간격으로 분리해 준다. 이름 없는 필드의 길이가 0 인 특별한 경우도 있다. 이 필드를 사용하면 구조체내의 다음 필드가 유닛의 경계점에서 시작하도록 강제로 정렬할 수 있다.


객체지향 프로그래밍을 잊지 말자!

이제 구조체를 정의하여 날짜를 저장하는 방법을 알았고 이 날짜 구조체를 다루는 다양한 루틴을 작성하였다. 그런데 이 과정에서 객체지향 프로그래밍은 어찌되었는가? Date 클래스를 만들고, Date 객체를 다룰 메서드를 만들었어야 하는 것 아닌가? 이게 나은 접근법이 아니었을까? 음, 그렇다. 프로그램에 날짜를 저장하는 방법에 대해 얘기하기 시작했을 때 이런 생각이 들었기를 바란다.

물론, 프로그램에서 날짜를 아주많이 다뤄야 한다면 클래스와 메서드를 사용하는 편이 나은 방법일 것이다. 사실 Foundation 프레임워크에는 이런 용도로 정의된 NSDate, NSCalendarDate 클래스들이 있다. 구조체 대신 날짜를 객체로 처리하는 Date 클래스를 구현하는 것은 연습문제로 남겨두겠다.


포인터

'포인터'를 사용하면 복잡한 데이터 구조체를 효과적으로 표시할 수 있고, 함수나 메서드에 넘기는 인수의 값을 변경할 수 있다. 또한 배열을 더 정확하고 효율적으로 다룰 수 있다. 이 장의 마지막에 Objective-C 언어에서 객체를 구현하는 데 포인터가 얼마나중요한지 단서를 약간 제시할 것이다.

포인터 개념은 8장 「상속」 에서 Point 와 Rectangle 클래스에 대해 이야기할 때 소개했고 동일한 객체에 참조가 여러개 있을 수 있다고 말했다.

포인터가 동작하는 방식을 이해하기 위해서는 먼저 '간접 참조'라는 개념을 이해해야 한다. 이 개념은 매일매일 삶에서 접하고 있다. 예를 들어, 프린터 토너 카트리지를 새로 사야 한다고 해보자. 지금 일하는 회사에서는 구매 담당부서가 모든 구매를 맡는다. 그래서 구매부의 철수 에게 전화를 걸어 새 카트리지를 하나 주문해 달라고 요청할 것이다. 철수는, 지역 공급처에 전화를 걸어 새 카트리지를 주문할 것이다. 과정을 정리해 보면, 새 카트리지를 얻으려고 직접 공급처에 카트리지를 주문하지 않았다. 간접 접근을 택한 것이다.

Objective-C 에서 포인터도 동일한 방식으로 동작한다. 즉, 간접 참조한다. 포인터는 특정 데이터 항목의 값에 간접적으로 접근한다. 그리고구매 부서를 통해 새 카트리지를 주문하는 이유(나는 카트리지를 어느 가게에서 주문하는지 알 필요가 없다)가 있는 것처럼, Objective-C 에서도 포인터를 사용하는 편이 유용한 이유가 있다.

이제 말로만 하지 말고 실제로 포인터가 어떻게 동작하는지 살펴보자. 다음과 같이 count 라는 변수를 정의했다고 하자.

int count = 10;


count 의 값에 간접적으로 접근할 수 있도록 다음과 같이 intPtr 이라는 변수를 선언하자.

int *intPtr;


별표(*)는 Objective-C 시스템에 변수 intPtr 이 int 의 포인터 형임을 나타낸다. 이말은, 프로그램이 intPrt 을 하나 이상의 정수 변수에 간접적으로 접근하는 데 사용할것 이라는 의미다.

이전 프로그램에서 scan 호출에서 & 연산자를 사용한 것을 기억나는가? 이 연산자는 '주소 연산자' 로 알려져 있으며, Objective-C에서 변수의 포인터를 만들어준다. 따라서 x 가 특정 형의 변수라면 표현식 &x 는 그 변수를 향한 포인터다. 원한다면 &x 표현식을, x 와 동일한 형의 포인터로 선언된 포인터 변수에 대입할 수 있다.

따라서 count 와 intPtr 를 위와 같이 정의했을 때, 다음 명령문을 통해 intPrt 과 count 사이에 간접 참조를 생성할 수 있다.

intPtr = &count;


주소 연산자는 count 값이 아니라 count 변수를 가리키는 포인터를 변수 intPtr 에 대입한다. 그림 13.1 은 intPtr 과 count 의 관계를 보여 준다. 화살표는 intPtr 가 count 의 값을 직접 포함하지 않고, 변수 count 를 가리키는 포인터를 포함함을 나타낸다.

그림 13.1 정수를 가리키는 포인터


포인터 변수 intPtr 를 써서 count 의 내용물을 참조하려면 간접 참조 연산자 * 를 붙여야 한다. 만일 x 가 정수형으로 정의되었다면, 다음 명령문은 intPtr 를 통해 간접적으로 참조한 값을 변수 x 에 대입할 것이다.

x = *intPtr;


앞에서 intPtr 가 count 를 가리키도록 설정했기 때문에, 이 명령문으로 인해 변수 count 에 저장된 값(10)이 변수 x 에 할당될 것이다.

프로그램 13.8 은 앞의 명령문들을 사용하고, 주소 연산자(&)와 간접 참조 연산자(*)라는 이 두가지 기본포인터 연산자를 설명한다.


프로그램 13.8


// 포인터를 설명하는 프로그램

#import <Foundation/Foundation.h>

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

    intPtr = &count;
    x = *intPtr;

    NSLog (@"count = %i, x = %i", count, x);

    [pool drain];
    return 0;
}

프로그램 13.8 의 출력결과


count = 10, x = 10


변수 count 와 x 는 일반적인 정수 변수로 선언되었다. 다음 줄에서는 변수 intPtr 가 '정수 포인터' 형으로 선언되었다. 이 두 줄짜리 선언은 다음과 같이 한 줄에 표현할 수도 있다.

int count = 10, x, *intPtr;


그 다음에는 변수 count 에 주소 연산자가 적용되어 이 변수의 포인터를 생성하고 변수 intPtr 에 대입한다.

그 후 다음 명령문이 실행된다.

x = *intPtr;


이 간접 참조 연산자는 Objective-C 시스템에게 변수 intPtr 가 다른 데이터 항목을 가리키는 포인터를 담도록 지시한다. 그 후 이 포인터를 사용하여 원하는 데이터 항목에 접근한다. 이 데이터 항목의 형은 포인터 변수를 선언할 때 지정한다. intPtr 를 선언할 때 컴파일러에게 이 변수가 정수를 가리키고 있다고 알려 주었으므로, 컴파일러는 *intPtr 표현식으로 참조한 값이 정수임을 안다. 또한 intPtr 가 정수 변수 count 를 가리키도록 설정해 놓았으므로 이 표현식은 간접적으로 count 의 값에 접근한다.


프로그램 13.9 는 포인터 변수의 흥미로운 특정을 보여 준다. 이 프로그램은 문자를 가리키는 포인터를 사용한다.


프로그램 13.9


#import <Foundation/Foundation.h>

int main (int argc, char *argv[])
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    char c = 'Q';
    char *charPtr = &c;

    NSLog (@"%c %c", c, *charPtr);

    c = '/';
    NSLog (@"%c %c", c, *charPtr);

    *charPtr = '(';
    NSLog (@"%c %c", c, *charPtr);

    [pool drain];
    return 0;
}

프로그램 13.9 의 출력결과


Q Q
/ /
( (


문자 변수 c 가 정의되고문자 'Q'로 초기화 되었다. 프로그램의 다음 줄에서, 변수 charPtr 는 'char 의 포인터' 형으로 정의되는데, 이는 이 변수에 저장된 값이 무엇이든 문자의 간접 참조(포인터)로 처리되어야 한다는 의미다. 이 변수의 초기값도 일반적인 방식으로 지정해 줄수 있음을 기억해 두자 charPtr 에 대입한 값은 변수 c 를 향한 포인터로, 변수 c 에 주소 연산자를 적용하여 얻어낸다(만일 c 가 이 명령문 다음에 정의된다면 컴파일러 오류가 발생할 것이다. 변수는 표현식에서 값이 참조되기 전에 선언되어 있어야만 한다).

변수 charPtr 를 선언하고 거기에 초기값을 대입하는 일은 다음과 같이 분리된 명령문 두 개로 표현할 수도 있다.

char *charPtr;
charPtr = &c;


(한줄로 선언할 때와 달리 다음 명령문으로 표현할 수는 없다.)

char *charPtr;
*charPtr = &c;


Objective-C 에서 포인터의 값은 실제로 무엇인가를 가리키기 전까지는 아무 의미가 없음을 기억하자.

첫 NSLog 호출은 변수 c 의 내용과 charPtr 로 참조한 변수의 내용을 표시한다. charPtr 가 변수 c 를 가리키도록 설정되었기 때문에, 표시되는 값은 프로그램 출력결과의 첫 줄에서 확인할 수 있듯이 c 의 내용이다.

프로그램의 다음 줄에서 문자 '/' 는 문자 변수 c 에 대입된다. charPtr 가 여전히 변수 c 를 가리키고 있으므로, 뒤따르는 NSLog 호출에서 *charPtr 의 값을 표시하면 터미널에 c 의 새 값이 정상적으로 표시된다. 이것은 매우 중요한 개념이다. charPtr 의 값이 바뀌지 않는 한, 표현식 *charPtr 는 언제나 c 의 값에 접근한다. 따라서 c 의 값이 변하면 *charPtr 의 값도 변한다.

다음 프로그램 명령문들이 어떻게 동작하는지 이해하는 데 앞서 논의한 내용이 도움이 될 것이다. charPtr 가 변하지 않는다면, 표현식 *charPtr 는 언제나 c 의 값을 참조할 것이라고 말했다. 따라서, 다음 표현식에서 여는 소괄호가 c 에 대입된다.

*charPtr = '(' ;


좀 더 올바르고 정확하게 말하자면, 문자 '(' 는 charPtr 가 가리키는 변수에 대입된다. 프로그램의 앞부분에서 charPtr 에 c 의 포인터를 대입하였으므로 이 문자가 변수 c 이다.

앞에서 설명한 개념들은 포인터의 동작을 이해하는 데 반드시 파악해야 한다. 만일 분명히 이해하지 못했다면 다시 한 번 살펴보자.


포인터와 구조체

int, char 같은 기본 데이터 형을 가리키는 포인터를 정의하는 방법을 보았다. 그런데 구조체를 가리키는 포인터도 정의할 수 있다. 이 장 초반에서 date 구조체를 다음과 같이 정의했다.

struct date
{
    int month;
    int day;
    int year;
};


다음과 같이 struct date 형 변수를 정의할 수 있다.

struct date todaysDate;


혹은 다음과 같이 todaysDate 변수의 포인터 형 변수도 정의할 수 있다.

struct date *datePtr;


방금 정의한 datePtr 를 기존 방식대로 사용할 수 있다. 예를 들어, 다음 명령문으로 todyasDate 를 가리키도록 설정할 수 있다.

datePtr = &todaysDate;


이렇게 대입하고나면, datePtr 가 가리키는 date 구조체의 어느 멤버든 간접적으로 접근할 수 있다.

(*datePtr).day = 21;


이 명령문은 datePtr 가 가리키는 date 구조체의 날짜를 21 로 설정한다. 구조체 멤버 연산자 '.' 이 간접 참조 연산자 * 보다 우선순위가 높기 때문에 괄호로 감싸야 한다.

datePtr 가 가리키는 date 구조체에 저장된 month의 값을 확인하려면 다음과 같은 명령문을 작성한다.

if ( (*datePtr).month == 12 )
    ...


구조체의 포인터는 보통 언어가 제공하는 특별한 연산자 를 사용한다. 다음 표현식을 한번 보자.

(*x) .y


구조체 포인터 연산자인 -> 를 쓰면 다음과 같이 더 분명히 표현할 수 있다.

x->y


즉,이 연산자로 이전의 if 문을 다음처럼 편하게 작성할 수 있다.

if ( datePtr->month == 12 )
    ...


구조체를 처음 설명했던 프로그램 13.6 을 구조체 포인터를 써서 다시 작성해 보자. 프로그램 13.10 은 새로 작성한 프로그램이다.


프로그램 13.10


// 구조체 포인터를 설명하는 프로그램
#import <Foundation/Foundation.h>

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

    struct date
    {
        int month;
        int day;
        int year;
    };

    struct date today, *datePtr;

    datePtr = &today;
    datePtr->month = 9;
    datePtr->day = 25;
    datePtr->year = 2009;

    NSLog (@"Today's date is %i/%i/%.2i.",
            datePtr->month, datePtr->day, datePtr->year % 100);

    [pool drain];
    return 0;
}

프로그램 13.10 의 출력결과


Today's date is 9/25/09.


포인터, 메서드, 함수

포인터를 메서드나 함쳐l 인수로 넘기고 반환 값으로 사용할 수도 있다. 곰곰이 생각해 보면, 결국 이것은 alloc, init 메서드가 지금까지 계속 해왔던 일(포인터를 반환하는 일)이다. 이 장의 마지막에서 이에 대해 좀더 상세히 다룰 것이다.

이제 프로그램 13.11을보자.


프로그램 13.11


// 함수의 인자로 사용하는 포인터
#import <Foundation/Foundation.h>

void exchange (int *pint1, int *pint2)
{
    int temp;
    temp = *pint1;
    *pint1 = *pint2;
    *pint2 = temp;
}

int main (int argc, char *argv[])
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    void exchange (int *pint1, int *pint2);
    int i1 = -5, i2 = 66, *p1 = &i1, *p2 = &i2;

    NSLog (@"i1 = %i, i2 = %i", i1, i2);

    exchange (p1,p2);
    NSLog (@"i1 = %i, i2 = %i", i1, i2);

    exchange (&i1,&i2);
    NSLog (@"i1 = %i, i2 = %i", i1, i2);

    [pool drain];
    return 0;
}

프로그램 13.11 의 출력결과


i1 = -5, i2 = 66
i1 = 66, i2 = -5
i1 = -5, i2 = 66


exchange 함수를 쓰는 목적은 함수의 두 인수가 가리키는 두 정수 값을 맞바꾸기 위해서다. 지역 정수 변수 temp 를 사용하여 값을 교체하는 도중에 정수 값 하나를 저장할 수 있다. 그 값은 pint1 이 가리키는 정수의 값과 동일하게 설정된다. pint2 가 가리키는 정수는 pint1 이 가리키는 정수에 복사되고, temp 의 값은 pint2 가 가리키는 정수에 저장되어 교체가 완료된다.

main 루틴은 정수 i1 과 i2 의 값을 각각 -5 와 66 으로 정의한다. 그 후, 정수 포인터 p1 과 p2 가 정의되어 각각 i1 과 i2 를 가리킨다. 프로그램은 그 후 i1, i2 의 값을 표시하고 exchange 함수에 두 포인터 (p1, p2)를 인수로 넘기며 호출한다. exchange 함수는 p1 이 가리키는 정수에 든 값을 p2 가 가리키는 정수에 든 값과 교환한다. p1 은 i1 을 가리키고, p2 는 i2 를 가리키므로 이 함수는 i1 과 i2 의 값을 교체한다. NSLog 를 두 번째로 호출한 결과를 보면 교체가 제대로 완료 되었음을 확인할 수 있다.

exchange 의 두 번째 호출은 좀더 흥미롭다. 이번에는, 함수에 건네지는 인수가 바로 i1 과 i2 에 주소 연산자를 적용해서 얻어낸 포인터들이다. &i1 이 정수 변수 i1 의 포인터를 만들어 내묘로, 함수가 첫 번째 인수로 기대하는 인수 형(정수 포인터)과 일치한다. 두 번째 인수에서도 마찬가지다. 프로그램의 출력 결과에서 볼 수 있듯이, exchange 함수가 i1, i2 를 원래 값으로 바꿔 주어 작업은 무사히 완료했다.

프로그램 13.11 을 자세히 살펴보자. Objective-C 에서 포인터를 다루는 핵심 요소들을 간단한 예제로 잘 설명하고 있다.


포인터와 배열

만일 정수 100개짜리 배열 values 가 있다면 valuesPtr 라는 포인터를 정의하여 이 배열에 든 정수에 접근하는 용도로 사용할 수 있다.

int *valuesPtr;


배열의 원소를 가리키는 포인터를 정의할때는, 포인터를 '배열의 포인터' 형으로 정의하지 않는다. 그 대신 포인터를 배열에 포함된 원소의 형을 가리키는 포인터로 지정한다.

마찬가지로, 만일 Fraction 객체의 배열인 fracts 가 있다면, fracts 안에 포함된 원소들을 가리킬 포인터를 다음과 같이 정의할 수 있다. 이 정의는 Fraction 객체를 정의할 때와 똑같은 선언임에 유의하자.

Fraction **fractsPtr;


valuesPtr 를 values 배열의 첫 원소를 가리키도록 설정하려면 다음과 같이 한다.

valuesPtr = values;


이 경우에는 주소 연산자가 사용되지 않는다. Objective-C 컴파일러는 첨자 연산자([ ])를 사용하지 않는 배열 이름은, 배열의 첫 원소를 가리키는 포인터로 판단하기 때문이다. 따라서, values 에 첨자를 사용하지 않으면 첫 원소를 향한 포인터가 생성된다.

values 의 시작을 가리키는 포인터를 만드는 다른 방법으로는 배열의 첫 원소에 주소 연산자를 적용하는 것이 있다.

valuesPtr = &values[0];


따라서 위 명령문도 포인터 변수 valuePtr 에 values 배열의 첫 원소의 포인터를 대입한다.

fractsPtr 가 가리키는 fracts 배열에 있는 Fraction 객체를 표시하려면 다음과 같은 명령문을 작성할 것이다.

[*fractsPtr print];


배열의 포인터를 사용하면 배열 원소를 연속적으로 훝고 싶을 때 강점을 맛보게 된다. 만일 valuesPtr 가 앞에서와 같이 정의되어 있고, values 의 첫 원소를 가리킨다면 다음 표현식으로 values 배열의 첫 원소인 values[0] 에 접근할 수 있다.

*valuesPtr


valuesPtr 변수를 통해 values[3] 를 참조할 경우에는 valuesPtr 에 3을 더하고 간접 참조 연산자를 적용한다.

*(valuesPtr + 3);


일반적으로, 다음 표현식을 사용하여 values[i]에 포함된 값에 접근할 수 있다.

*(valuesPtr + i)


그러므로 values[10] 을 27 로 설정하려면 다음과 같은 표현식을 작성하거나, valuesPtr 를 사용하여 그 다음과 같은 표현식을 작성해도 된다.

values[10] = 27;

*(valuesPtr + 10) = 27;


valuesPtr 가 values 배열의 두 번째 원소를 가리키게 하기 위해서는, 주소 연산자를 values[1] 에 적용한다음 결과를 valuesPtr 에 대입한다.

valuesPtr = &values[1];


만일 valuesPtr 가 values[1] 이를 가리키고 있다면, valuesPtr 에 1 을 더하여 간단히 valuesp[1] 을 가리키도록 할 수 있다.

valuesPtr += 1;


이 표현식은 Objective-C 에서 유효하며 어느 데이터 형의 포인터든 상관없이 사용할 수 있다.

일반적으로 x 형 원소의 배열 a 가있고, px는 'x 를 가리키는 포인터' 형이고, i 와 n 은 변수의 정수 상수라고 해보자.

px = a;


이 명령문은 px 가 a 의 첫 원소를 가리키도록 한다.

*(px + i)


이 문장은 a[i]에 포함된 값을 참조하도록 지시한다.

px += n;


이 명령문은 px 가, 이 배열에 들어있는 원소가 어떤 형인지에 상관없이 배열의 n 번째 원소를 참조하도록 설정한다.

fractsPtr가 분수 배열 안에 담긴 분수를 가리킨다고 할 때, 배열의 다음 원소에 들어 있는 분수를 더하여 결과를 Fraction 객체인 result 에 저장한다고 하자. 다음 명령문으로 이를 달성할 수 있다.

result = [*fractsPtr add: fractsPtr + 1];


포인터에 증가 혹은 감소 연산자(++,--)를 사용하면 더욱 편리하다. 증가 연산자를 포인터 에 적용하면 포인터 에 1을 더하는 효과를 얻고, 감소 연산자는 포인터에 1 을 빼는 효과를 낸다. textPtr 가 char 포인터로 정의되었고 text 라는 char 배열의 앞부분을 가리킨다고 해보자.

++textPtr;


그러면 이 명령문은 textPtr 가 text 의 다음 문자 text[1] 을 가리키도록 할 것이다. 감소 연산자에도 적용해 보자.

--textPtr;


여기서는 textPtr 가 text 의 이전 문자를 가리키게 된다(물론, textPtr 가 이 명령문을 실행하기 전에 text 의 맨 앞부분을 가리키지 않았다고 가정한다).

Objective-C 에서 포인터 변수 두 개를 비교할 수도 있다. 동일한 배 열에 있는 두 포인터를 비교할 때 특히 유용하다. 예를들어, 배열의 마지막 원소를 가리키는 포인터와 valuesPtr 포인터를 비교함으로써 포인터가 원소 100 개짜리 배열의 끝을 지나쳐서 가리키고 있지는 않은지 확인할 수 있다.

valuesPtr > &values[99]


이렇게 표현식을 작성한 결과, valuesPtr가 values 배열의 마지막 원소를 지나서 가리키고 있다면 TRUE(O이 아님) 가 되고, 그렇지 않으면 FALSE(O) 가 될 것이다. 앞에서 말했듯이, 이 표현식은 다음과 같이 써도 동일하다.

valuesPtr > values + 99


첨자 연산자 없이 values 를 사용하면, 이것을 values 배열의 처음을 가리키는 포인터로 쓰기 때문이다(&values[O] 을 작성하는 것과 동일함을 기억하라).

프로그램 13.12는 배열의 포인터를 설명한다. arraySum 함수는 정수 배열에 포함된 원소의 합을 계산한다.


프로그램 13.12


// 정수 배열의 원소와 합을 구하는 함수

#import <Foundation/Foundation.h>

int arraySum (int array[], int n)
{
    int sum = 0, *ptr;
    int *arrayEnd = array + n;

    for ( ptr = array; ptr < arrayEnd; ++ptr )
        sum += *ptr;

    return (sum);
}

int main (int argc, char *argv[])
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    int arraySum (int array[], int n);
    int values[10] = ( 3, 7, -9, 3, 6, -1, 7, 9, 1, -5 };

    NSLog (@"The sum is %i", arraySum (values, 10));
    [pool drain];
    return 0;
}

프로그램 13.12 의 출력결과


Ths sum is 21


arraySum 함수 내부에서 정수포인터 arrayEnd 가 정의되어, array 의 마지막 원소 바로 다음을 가리키도록 설정되었다. 그 다음으로 for 문이 돌며 array의 원소들을 거친다. 이 반복문이 시작할 때, ptr 의 값은 array 의 맨 앞을 가리키도록 설정된다. 반복문이 돌 때마다 ptr 가 가리키는 array 의 원소가 sum 에 더해진다. 그 후 for 문은 ptr 의 값을 1 씩 증가시켜 array 의 다음 원소를 가리키도록 만든다. ptr 가 array 의 끝을 지나쳐 가리키게 되면 for 문은 종료되고, sum 의 값이 반환된다.


이것이 배열인가? 아니먼 포인터인가?

배열을 함수에 넘겨줄때, 앞의 arraySum 함수에서 했던 것처럼 배열 이름만 지정해 준다. 그런데 이 절에서 배열의 포인터를 만드는 데도 배열의 이름만 지정해 준다고 이야기했다. 이것은 arraySum 함수를 호출할 때, 배열 values 를 가리키는 포인터가 함수로 건네졌다는 의미다.

그런데 만일 배열의 포인터가 함수에 건네진다면, 함수에 있는 공식 매개변수가 왜 포인터로 선언되지 않았을까? 다시 말해 arrarySum 함수 내의 array 를 왜 다음과 같이 선언하지 않았을까?

int *array;


함수 내에서 배열을 참조하려면 모두 포인터 변수를 사용해야 하는 것 아닌가?

이 질문들에 답하려면 먼저 포인터와 배열에 대해 이미 설명했던 내용을 다시 살펴보아야 한다. valuePtr 가 배열 values 에 포함된 동일한 형태의 원소를 가리키고, valuesPtr 가 values 의 맨 처음을 가리키고 있다고 가정하면, 표현식 *(valuesPtr +i) 는 표현식 values[i] 와 동일하다. 이에 따라 *(values + i)를 사용하여 배열 values 의 i 번째 원소를 참조할 수 있다. 그리고 일반적인 경우 Objective-C 에서는 x 가 어느 형이든 배열이기만 하면, 표현식 x[i] 는 언제나 *(x + i) 와 동일한 표현으로 쓸 수 있다.

Objective-C 에서 포인터와 배열은 서로 매우 긴밀히 관련되어 있다. 이 때문에 arrarySum 함수 내에서 array 를 'int의 배열'로 선언하거나 'int의 포인터'로 선언할 수 있다. 두 선언 모두 앞의 프로그램 예제에서 잘 동작한다. 한번 확인해 보라.

인덱스 번호를 사용하여 배열의 원소를 참조할 계획이라면, 해당하는 공식 매개변수를 배열로 선언하라. 이렇게 하면 함수가 배열을 사용한다는 것이 더 정확하게 반영된다. 이와 마찬가지로, 인수를 배열의 포인터로 사용할 생각이라면, 포인터 형이 되도록 선언하자.


문자열의 포인터

배열을 가리키는 포인터의 가장 흔한 용도로는 문자열이 있다. 그 포인터가 표기하기 편리하고 효율적이기 때문이다. 문자열의 포인터를 얼마나 쉽게 사용할 수 있는지 알아보자. 한 스트링을 다른 스트링에 복사하는 copyString 이라는 함수를 작성한다. 이 함수를 일반적인 배열-인덱스 방법으로 작성한다면 다음과 같은 코드가 나오게 된다.

void copyString (char to[], char from[])
{
    int i;

    for ( i = 0; from[i] != '\0'; ++i )
        to[i] = from[i];

    to[i] = '\0';
}


for 문은 to 배열에 널 문자가 복사되기 전에 종료되므로 함수에서 마지막 명령문이 필요하게 된다.

만일 포인터를 사용하여 copyString 을 작성하면 인덱스 변수 i 가 더는 필요없게 된다. 프로그램 13.13 에서 그 예를 살펴보자.


프로그램 13.13


#import <Foundation/Foundation.h>

void copyString (char *to, char *from)
{
    for ( ; *from != '\0'; ++from, ++to )
        *to = *from;

    *to = '\0';
}

int main (int argc, char *argv[])
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    void copyString (char *to, char *from);
    char string1[] = "A string to be copied.";
    char string2[50];

    copyString (string2, string1);
    NSLog (@"%s", string2);

    copyString (string2, "So is this.");
    NSLog (@"%s", string2);

    [pool drain];
    return 0;
}

프로그램 13.13 의 출력결과


A string to be copied.
So is this.


copyString 함수는 두 공식 매개변수인 to 와 from 을 이전 버전처럼 문자 배열이 아니라 문자 포인터로 정의한다. 여기에 함수가 이 두 변수를 어떻게 사용할지가 반영되어 있다.

그 다음 from 이 가리키는 스트링을 b 가 가리키는 스트링에 복사하기 위해 (초기 조건 없이)for 문에 들어선다. 반복문을 돌 때마다, from 과 포인터는 각각 1 씩 증가된다. 이를 통해 from 포인터는 소스 스트링에서 복사할 다음 문자를 가리키게 되고 to 포인터는 복사된 다음 문자가 저장될 '목적 스트링(destination)'의 위치를 가리키게 된다.

from 포인터가 널 문자를 가리키면 for 문이 종료된다. 그 후 함수는 널 문자를 목적 스트링의 끝에 넣는다.

main 루틴에서 copyString 함수는 두 번 호출된다. 처음 호출 때에는 string1 의 내용을 string2 로 복사한다. 두 번째 호출에서는 "So is this." 라는 스트링 상수의 내용을 string2 에 복사한다.[1]


문자 스트링 상수와 포인터

이전 프로그램에서 다음 호출이 동작하는 것을 보면, 함수에 인수로 넘기는 스트링 상수가 사실 포인터 임을 알 수 있다.

copyString (string2, "So is this.");


이 스트링 상수가 포인터라는 것뿐 아니라, Objective-C 에서 문자 스트링 상수가 사용될 때도 일반적으로 그 문자 스트링의 포인터가 생성된다고 할 수 있다.

지금은 이 말이 좀 혼란스러울 수 있겠지만 4장에서 잠시 언급했듯이, 여기서 말하는 문자스트링 상수는 C 스타일 스트링이다. 이것은 객체가 아니다. 스트링 상수 객체는 @"This is okay." 와 같이 스트링 앞에 @ 를 붙여서 생성한다. 스트링 객체와 C 스타일 스트링은 서로 맞바꾸어 사용할수 없다.

만일 textPtr 가 문자 포인터로 선언됐다고 해보자.

char *textPtr;


다음 명령문은 textPtr 에 문자 스트링 상수 "A character string." 을 가리키는 포인터를 대입한다.

textPtr = "A character string.";


주의할 것이 있다. 문자 배열에서는 이런식의 대입이 유효하지 않으므로, 문자 포인터와 문자 배열 사이를 구분해야 한다. 예컨대 다음 명령문으로 text가 char의 배열이라고 정의했다고 하자.

char text[80];


이 상황에서 다음과 같은 명령문을 작성해서는 안 된다.

text = "This is not valid.";


Objective-C 에서 문자 배열에 이런식으로 대입할 수 있는 상황은 배열을 초기화 할때 뿐이다.

char text[80) = "This is okay.";


text 배열을 이렇게 초기화하면, text 내에 문자 스트링 "This is okay."의 포인터를 저장하는 것과 달리, 실제 문자 자체가 종료하는 널 문자와 함께, text 배열의 해당 원소에 저장된다.

만일 text 가 문자 포인터일경우, 다음 명령문으로 text 를 초기화한다고 해보자.

char *text = "This is okay.";


문자스트링 "This is okay."를 가리키는 포인터를 text 에 대입하게 될것이다.


증가 연산자, 감소 연산자 다시 보기

지금까지 증가 연산자와 감소 연산자를 사용할 때는 항상 이것들이 표현식의 유일한 연산자였다. 표현식 ++x 를 작성하면 변수 x 의 값에 1 이 더해진다. 그리고 x 가 배열의 포인터라면 이 표현식은 x 가 배열의 다음 원소를 가리키도록 설정한다.

증가 연산자나 감소 연산자는 다른 연산자가 나오는 표현식 에서도 사용할 수 있다. 이때 이 연산자들이 어떻게 동작하는지를 정확히 이해해야 한다.

증가 연산자와 감소 연산자는 값이 증가 혹은 감소되는 변수 앞에 붙여서 사용했다. 예를 들어, 변수 i 를 증가시킬 때는 다음과 같이 작성했다.

++i;


또는 증가 연산자를 변수 뒤에 배치해도 된다.

i++;


이 두 표현식 모두 유효하며 결과(i의 값을 증가한다)는 동일하다. 더 정확하게 말하면 ++ 가 피 연산자 앞에 있는 경우에 증가 연산이 '선-증가(pre-increment)'로 분류된다. ++ 가 피 연산자 뒤에 있는 경우에는 '후-증가(post-increment)' 연산이라고 말한다.

감소 연산자도 마찬가지다. 다음 명령문을 살펴보자.

--i;


위의 식은 i 의 선-감소 를 수행한다.

i--;


위의 식은 i 의 후-감소 를 수행한다. 두 식 모두 i 의 값에서 1 을 빼는 결과가 나온다. 한편 좀더 복잡한 표현식에서 증가 및 감소 연산자를 사용하면 이 연산자들이 선 연산 인지 후 연산 인지에 따라 차이가 발생한다. 두 정수 i 와 j 가 있다고 가정하자. i의 값을 0 으로 설정하고 다음 명령문을 작성하자.

j = ++i;


i 에 대입되는 값은 예상대로 0 아니라 1이다. 선-증가 연산자를 쓰면, 그 값이 표현식에서 사용되기 전에 변수부터 증가한다. 따라서 이 표현식 에서는, 먼저 i 의 값이 0 에서 1 로증가된 뒤, 그 값이 j 에 대입된다. 마치 다음 두 명령문을 실행한 것과 같다.

++i;
j = i;


만일,다음 명령문처럼 후-증가 연산자를 사용한다고 해보자.

j = i++;


i 의 값이 j 에 대입된다음, i 가 증가하게 된다. 이 명령문실행 전에 i 가 0 이었다면, j 에 0 이 대입되고 i 의 값이 1 만큼 증가한다. 다음 명령문과 동일한 결과가 발생할 것이다.

j = i;
++i;


다른 예로, i 가 1 이라고 해보자.

x = a[--i];


위 명령문은변수 i 가 a 의 인덱스로 사용되기 전에 값이 감소하므로 a[0] 의 값을 x 에 대입하는 효과가 나타난다. 다음 명령문을 보자.

x = a[i--];


변수 i 의 값이 a 의 인덱스로 사용된 후에 값이 감소하므로 a[1] 의 값을 x 에 대입하는 효과가 나타난다.

증가 연산자와 감소 연산자의 선, 후 연산을 구별하는 세 번째 예제로 다음 함수 호출을보자.

NSLog (@"%i", ++i);


이 경우 i 를 증가시킨 다음, 그 값을 NSLog 함수에 건넨다.

NSLog (@"%i", i++);


반면 위 호출은 i 의 값을 쓰고 나서 증가시킨다. 따라서 i 가 100 이었다면 첫 번째 NSLog 호출은 터미널에 101 을 표시하고 두 번째 NSLog 호출 때는 100 을 표시할 것이다. 어느 경우든, 명령문이 실행되고 난다음에 i의 값은 101 이 된다.

실제 프로그램을 보기 전에 마지막 예를보자. 여기서 textPtr 는 문자 포인터이다.

*(++textPtr)


이 표현식은 먼저 textPtr 를 증가시키고, 이 포인터가 가리키는 문자를 가져온다. 반면 다음 표현식은 좀 다르다.

*(textPtr++)


위 명령문은 먼저 textPtr 가 가리키는 문자를 가져온 후 textPtr 를 증가시킨다. 어느 경우든, * 와 ++ 연산자는 우선순위가 같고, 오른쪽부터 결합이 이루어지므로 괄호를 쓸 필요는 없다.

프로그램 13.13 의 copyString 함수로 돌아가 보자. 대입문에 증가 연산자를 직접 사용하도록 작성한다.

for 문이 돌 때마다 반복문 내 대입 명령문이 실행되고 나서 to 와 from 포인터가 증가하므로, 대입 명령문에 후-증가 연산자로 통합되어야 한다. 프로그램 13.13 에 나오는 for 반복문을 개선하면 다음과 같다.

for ( ; *from != '\0'; )
    *to++ = *from++;


반복문 내의 대입 명령문은 다음과 같이 실행된다. from 이 가리키는 분자를 받아온 후, from 은 증가하여 소스 스트링의 다음문자를 가리키게 된다. 참조한 문자는 to 가 가리키는 위치에 저장되고, to 도 증가하여 목적 스트링에서 다음 위치를 가리키게 된다.

앞의 for 문은 초기 표현식도, 반복 표현식도 없기 때문에 더 들여다볼 필요는 없을 것이다. 사실, 이 로직은 while 문을 사용하면 더 잘 표현되었을 것이다. 프로그램 13.14 에서 copyString 을 새로 작성하는데, 여기서는 while 문을 사용한다. 이 while 문에서는 숙련된 Objective-C 프로그래머들이 흔히 그렇듯, 널 문자가 0과 같다는 사실을 활용한다.


프로그램 13.14


// 스트링을 다른 포인터로 복사하는 함수 - 두 번째 버전

#import <Foundation/Foundation.h>
void copyString (char *to, char *from)
{
    while ( *from )
        *to++ = *from++;
    *to = '\0';
}

int main (int argc, char *argv[])
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    void copyString (char *to, char *from);
    char string1[] = "A string to be copied.";
    char string2[50];

    copyString (string2, string1);
    NSLog (@"%s", string2);

    copyString (string2, "So is this.");
    NSLog (@"%s", string2);
    [pool drain];
    return 0;
}

프로그램 13.14 의 출력결과


A string to be copied.
So is this.


포인터 연산

이 장에서 계속 본 것처럼 포인터에서 정수값을 더하거나 뺄 수 있다. 게다가 두 포인터가 동일한지, 혹은 하나가 다른 하나보다 크거나 작은지도 비교할 수 있다. 또한 포인터 형이 같다면 포인터끼리 뺄셈 연산도 가능하다. Objective-C 에서 두 포인터 간 뺄셈의 결과는 두 포인터 사이에 있는 원소의 개수다. 만일 a 가 특정 형의 배열을 가리키고, b 는 동일한 배열의 어딘가를 가리키고 있다면, 표현식 b - a 는 이 두포인터 사이에 있는 원소의 개수를 나타낸다. 예를들어, p 가 배열 x 의 어느 원소를 가리키고 있다고 하자.

n = p - x;


이 명령문은 p 가 가리키는 x 내 원소의 인덱스번호를 변수 n 에 대입한다(여기서는 n을 정수 변수로 가정하였다). 만일 p 가 다음 명령문으로 x 의 100 번째 원소를 가리키도록 설정되었다고 해보자.

p = &x[99];


앞서 나온 뺄셈 연산후 n 의 값이 99 가 될 것이다.


함수의 포인터

함수의 포인터는 약간 수준 높은 주제가 되겠지만, 포인터에 대한 설명을 마무리 한다는 의미로 이곳에서 짚고 넘어가겠다. 함수의 포인터를 다룰 때는, Objective-C 컴파일러가 포인터 변수가 함수를 가리키고 있음은 물론, 함수가 반환하는 값의 형과 인수형,받는인수 개수도 알아야 한다. 변수 fnPtr 를 'int 형을 반환하고 인수를 받지않는 함수의 포인터' 형으로 정의하려면 다음과 같이 선언한다.

int (*fnPtr) (void);


fnPtr 주변에 괄호는 반드시 씌워야 한다. 괄호를 쓰지 않으면, 컴파일러는 이 명령문을 int 형 포인터를 반환하는 함수 fnPtr 를 선언한 것이라고 판단한다(함수 호출 연산자 ()가 포인터 간접 참조 연산자 * 보다 우선순위가 높기 때문이다).

함수 포인터가 특정 함수를 가리키도록 하려면, 함수 이름을 대입하면 된다. 만일 lookup 이 int형을 반환하고 인수를 받지 않는함수라고 해보자.

fnPtr = lookup;


이 명령문은 함수 포인터 변수 fnPtr 에 이 함수의 포인터를 저장하게 해준다. 함수 이름에 괄호를 붙이지 않으면 배열 이름에 첨자없이 사용하는 것과 유사한 상황이 된다. Objective-C 컴파일러는 지정된 함수의 포인터를 자동으로 생성한다. 함수 이름앞에 & 를붙일 수 있지만 꼭 붙여야 하는건 아니다.

만일 lookup 함수가 아직 정의되지 않았다면, 대입 전에 함수부터 선언해야 한다. 다음 명령문을 먼저 선언해 두어야 이 함수의 포인터를 변수 fnPtr 에 대입 할 수있다.

int lookup (void);


포인터 변수를통해 간접 참조된 함수도 함수 호출 연산자를 포인터에 적용하고 괄호 안에 함수의 인수를 나열함으로써 호출할 수 있다. 예를 들어서 다음호출은 fnPtr 가 가리키는 함수를 호출하고, 반환 값을 변수 entry 안에 저장한다.

entry = fnPtr ();


함수의 포인터는 흔히 다른 함수의 인수로 건네는 식으로 사용된다. 표준 라이브러라는 qsort 에서 함수 포인터를 사용한다. qsort 는 데이터 원소배열을 빠르게 정렬(quick sort)하는 함수다. 이 함수는 qsort 가 정렬하는 배열의 두 원소를 비교할 때 필요한 함수의 포인터를 인수중 하나로 넘겨받는다. 이런 방식으로 배열내 두 원소를 실제로 비교하는 작업은 qsort 자신이 아니라 사용자가 제공한 함수로 수행된다. 그러므로 qsort 는 배열 형이 무엇이든 상관없이 정렬할 수 있다.

Foundation 프레임워크에서 몇몇 메서드는 함수 포인터를 인수로받는다. 예를 들어 보자. NSMutableArray 클래스에 정의되어 있는 sortUsingFunction:context: 는 정렬할 배열의 두 원소를 비교해야 할 때, 지정한 함수를 호출한다.

함수 포인터를 쓰는 흔한 예로는 '디스패치' 표를 생성하는 작업이 있다. 함수자체를 배열의 원소에 넣을 수는 없다. 그렇지만 함수 포인터는 배열 안에 넣을수 있다. 이 특징을 응용해서, 호출할 함수 포인터를 담은 표를 생성하는 것이다. 예를 들어, 사용자가 입력할 다른 커맨드를 처리할 용도로 표를 생성할 수 있다. 표의 각항목이 커맨드 이름과 특정 커맨드를 처리하고자 호출할 함수의 포인터를 담고있는 것이다. 이제 사용자가 커맨드를 입력하면, 표 안의 커맨드를 찾아 해당 함수를 호출하여 커맨드를 처리하도록 할 수 있다.


포인터와 메모리주소

Objective-C 의 포인터 에 대한 논의를 마치기 전에, 이것이 실제로 어떻게 구현되어 있는지 상세하게 살펴볼 필요가 있다. 컴퓨터의 메모리는 연속적인 저장 셀 이라고 생각해볼 수 있다. 컴퓨터 메모리에 있는 각 셀에는 '주소' 라는 숫자가 부여된다. 흔히 컴퓨터 메모리의 첫 주소는 0 이다. 그리고 컴퓨터 시스템 대부분에서 '셀'은 1바이트다.

컴퓨터는 메모리를 사용하여 여러분의 컴퓨터 프로그램 명령어를 저장하고 프로그램에 관련된 변수의 값을 저장한다. 만일 int 형 변수인 count 를 선언한다면, 시스템은 프로그램을 실행하는 도중에 count 의 값이 담긴 메모리 공간을 할당할 것이다. 예를 들어, 이 위치는 컴퓨터 메모리의 주소 1000FF(16)이 될 수 있다.

다행히도, 변수에 할당되는 메모리 주소는 시스템이 자동으로 처리하므로 직접 신경 쓸 필요는 없다. 그러나 각 변수가 각자의 메모리 주소에 연계됨을 알고 있으면 포인터가 작동하는 방식을 이해하는 데 도움이 될 것이다.

주소 연산자를 Objective-C 의 변수에 적용하면 컴퓨터 메모리에서 그 변수의 실제 주소가 값으로 생성된다(물론 이 때문에 주소 연산자라는 이름이 붙었다). 따라서 다음 명령문을 보자

intPtr = &count


intPtr 에 count 변수에 할당된 컴퓨터 메모리의 주소를 대입한다. 만일 count 가 1000FF(16) 이라는 주소에 자리 잡는다면 이 명령문은 intPrt 에 0x1000FF 라는 값을 대입할 것이다.

다음 표현식과 같이 간접 참조 연산자를 포인터 변수에 적용해 보자.

*intPtr


이 식은 포인터 변수에 든 값을 메모리 주소로 처리해 준다. 그런 다음, 이 메모리 주소에 저장된 값을 가져와 포인터 변수에 선언된 형에 맞춰 해석한다. 만일 intPtr 가 int 형 포인터 였다면, 시스템은 intPtr 의 메모리 공간에 저장된 값을 정수로 해석할 것이다.


공용체

Objective-C 프로그래밍 언어에서 보기 드문 구조 가운데는 '공용체(union)'가 있다. 이 구조는 보통 고급 응용 프로그램에서 동일한 저장 공간에 다른 형의 데이터를 저장해야 할 때 사용한다. 예를 들어 x 라는 단일 변수를 정의하고 단일 문자나 부동소수점 수, 혹은 정수를 저장하는 데 사용하려 한다면 다음과 같이 먼저 공용체 (아마도) mixed 를 정의할 것이다.

union mixed
{
    char c;
    float f;
    int i;
};


공용체를 선언하는 법은 struct 를 사용해야 하는 곳에 키워드 union 을 대신 쓴다는 점만 빼면 구조체와 동일하다. 구조체와 공용체의 진정한 차이점은 메모리 할당 방식에 있다. 다음과 같이 변수를 union mixed 형으로 선언해 보자.

union mixed x;


x 가 각기 다른 세 멤버 c, f, i 를 담도록 정의하지는 않는다. 그 대신에 c, f, i 가운데 한 멤버만 담도록 정의한다. 이런 식으로, 변수 x가 char와 R때, int 가운데 하나를 저장할 수 있지만 셋 모두를 저장할 수는 없게끔 (셋 중 둘도 저장할 수 없게) 설정한다. 다음 명령문을 사용하여 변수 x 에 문자 하나를 저장할 수 있다.

x.c = 'K' ;


x 에 부동소수점을 저장하려면 x.f 표기법을 사용한다.

x.f = 786.3869;


마지막으로 정수 count 를 2 로 나눈 결과를 x 에 저장하려면 다음 명령문을 사용한다.

x.i = count / 2;


x의 멤버인 float, char, int 가 동일한 메모리 공간에 공존하므로, 한 번에 값 하나만 x 에 저장될 수 있다. 게다가 공용체에서 받은 값이 마지막에 저장한 것과 일치하도록 보장해야 한다.

공용체를 정의할때 공용체 이름을 꼭 지정해야 하는건 아니며, 변수도 바로 정의해도 된다. 또한 공용체의 포인터를 선언할 수도 있다. 이때 구조체의 포인터와 동일한 문법, 연산 규칙들이 적용된다. 마지막으로, 공용체 변수를 초기화하는 작업은 다음과 같이 한다.

union mixed x = { '#' } ;


이 코드는 x 의 첫 멤버인 c 를 문자 '#' 으로 설정한다. 다음과 같이 특정 멤버를 초기화하는 일도 가능하다.

union mixed x = { .f=123.4; };


자동 공용체 변수를 다른 공용체 변수로 초기화할 수 있다. 이때 데이터 형이 같아야한다.

공용체를 쓰면 다른 형의 원소를 담는 배열을 정의할 수도 있다. 다음 명령문을 보자. 원소를 kTableEntries 개 갖는 table 이라는 배열을 만드는 것이다.

struct
{
    char *name;
    int type;
    union
    {
        int i;
        float f;
        char c;
    } data;
} table [kTableEntries];


배열의 각 원소는 name 이라는 문자포인터, 정수 멤버 type, 공용체 멤버 data 로 구성된 구조체를 담고 있다. 각 배열의 data 멤버는 int, float, char 가운데 하나를 담을수 있다. 정수 멤버 type 은 data 멤버에 담긴 값이 무슨 형인지 추적하는 데 사용한다. 예를 들어, int 형을 담았다면 (적절히 정의된) INTEGER 값을 대입하고, float 형을 담았을 경우에는 FLOATING 을, char 형이라면 CHARACTER 를 대입할 수 있을 것이다. 이 정보는 특정 배열 원소의 특정 data 멤버에 어떻게 접근해야 하는지를 알려준다.

문자 '#'를 table[5] 에 저장하고 type 필드에 문자가 저장되었음을 지정해 주려면 다음 명령문을 사용한다.

table[5].data.c = '#' ;
table[5].type = CHARACTER;


table의 원소를 차례로 훝어볼 때, 테스트 명령문을 적절히 설정하여 각 원소에 저장된 data 값이 무슨 형인지 알아낼 수 있다. 예컨대, 다음 반복문은 table 원소의 각 이름과 연관된 값을 터미널에 표시한다.

enum symbolType { INTEGER, FLOATING, CHARACTER };
    ...

for ( j = 0; j < kTableEntries; ++j )
{
    NSLog (@"%s ", table[j].name);

    switch ( table[j].type )
    {
        case INTEGER:
            NSLog (@"%i", table[j].data.i);
            break;
        case FLOATING:
            NSLog (@"%g", table[j].data.f);
            break;
        case CHARACTER:
            NSLog (@"%c", table[j].data.c);
            break;
        default:
            NSLog (@"Unknown type (%i), element %i",
                table[j].type, j );
            break;
    }
}


이 예제는 각 심벌의 이름, 형, 값을 (그리고 그 심벌에 관련된 다른 정보도) 담는 심벌 표를 저장하는 경우에 유용하다.


이것들은 객체가 아니다!

이제 배열, 구조체, 문자 스트링, 공용체를 정의하고 다루는 방법을 배웠다. 한 가지 중요한 사실을 잊지 말자. 이것은 객체가 아니다! 따라서 이것들에게 메시지를 보낼 수 없다. 또한 Foundation 프레임워크 에서 제공하는 메모리 할당 정책 같은 훌륭한 것들(혜택)을 누릴 수 없다. 이 장 도입부에 이 장을 건너 뛰고 나중에 돌아와서 살펴보라고 한 일이 기억나는가? 그 이유 가운데 하나가 이것이다. 일반적으로 배열과 스트링을 객체로 정의하는 Foundation 의 클래스들이 언어에서 지원하는 것들보다 사용하기에는 훨씬낫다. 이 장에서 설명한 형들은 정말 필요할때만 사용하는 게 좋다. 그리고 되도록 그럴 일이 없기를 바란다.


기타 언어 기능

언어가 지원하는 기능중에는 다른장에서 다루기에도 썩 맞지않은 것들이 몇가지 있다. 여기서 이 기능들을 살펴보자.


복합 리터럴

'복합 리터럴'은 초기화 목록이 뒤따르는, 괄호 안에 표시된 형 이름이다. 이것은 지정된 형의 이름없는 값을 생성한다. 블록 안에서 정의되면 그 범위는 블록 내로 제한된다. 만일 모든 블록 바깥에서 정의되었다면 전역 범위가 된다. 전역 범위일 경우 초기화 값들은 모두 상수 표현식이어야 한다.

예를한번 보자.

(struct date) {.month = 7, .day = 2, .year = 2004}


이 표현식은 지정된 초기값을 갖는 struct date 형의 구조체를 생성한다. 다음 코드로 이를 다른 struct date 구조체에 대입할수 있다.

theDate = (struct date) {.month = 7, .day = 2, .year = 2004};


아니면 struct date 를 인수로 받는 함수나 메서드에 이 복합 리터럴을 넘겨줄 수도 있다.

setStartDate ((struct date) {.month = 7, .day = 2, .year = 2004});


게다가 구조체 외의 다른 형도 정의할 수 있다. 다음 명령문을 보자. 이것은 intPtr 가 int* 형일 때, intPtr 가 원소 3 개를 지정한 대로 초기화하는 100 개짜리 정수 배열을 가리키도록 설정할 수 있다.

intPt;= (int [100]) ([0] = 1, [50] = 50, [99] = 99 }j


배열 크기가 지정되지 않았다면, 초기화 목록이 크기를 결정한다.


goto 문

goto 문을 쓰면 프로그램의 특정 위치로 바로 분기하게 된다. 이 분기가 프로그램의 어디로 갈지 알려면 '레이블'이 있어야 한다. 레이블(Label) 생성 시, 변수 이름을 지을 때와 동일한 규칙이 적용된다. 또, 레이블 뒤에는 반드시 콜론이 붙어야한다. 레이블은 분기를 따라 이동할 명령문 바로 앞에 자리 잡고, goto 와 동일한 함수 혹은 메서드내에 있어야 한다.

예를 들어, 다음과 같은 명령문을 작성하면 레이블 out_of_data: 뒤의 명령문으로 분기를 타게 된다.

goto out_of_data;


이 레이블은 함수나 메서드 내에 goto 앞뒤 혹은 어디서든 나을 수 있다. 또한 다음과 같이 사용할 수도 있다.

out_of_data: NSLog (@"Unexpected end of data.");


게으른 프로그래머들은 코드의 다른부분으로 이동하고자 goto 문을 자주 남용한다. 그러나 goto 문은 프로그램의 일반적인 순차적 흐름을 방해한다. 그 결과, 프로그램의 흐름을 따라가기가 힘들어진다. goto 문을 많이 사용하면 프로그램을 해석할 수 없게 된다. 이런 이유로, goto 문을 쓰는 것은 좋은 프로그래밍 습관이라고 볼 수 없다.


null 문

Objective-C 에서는 일반 프로그램 명령문이 나오는 곳에 세미콜론만 사용할 수 있다. 이게 바로 '널' 명령문인데, 아무 작업도 하지 않아서 전혀 쓸모없어 보일수도 있다. 그러나 프로그래머들은 while , for, do 문에서 이 널 명령문을 자주 사용한다. 예를 들어 보자. 다음 명령문은 '표준 입력'(standard input, 기본적으로 터미널)에서 새줄 문자가 나타날 때까지 나오는 모든 문자를 읽어 text 가 가리키는 문자배열에 저장하는 것이 목적이다. 여기서는 표준입력에서 한 번에 한 문자씩 읽고 반환하는 라이브러리 루틴인 getchar 를 사용한다.

while ( (*text++ = getchar ()) != ' ')
    ;


모든 연산은 while 문의 반복 조건부분에서 수행된다. 이때 컴파일러는 반복 조건 표현식 다음에 오는 명령문을 반복문의 몸체로 여긴다. 이 때문에 널 명령문이 필요한 것이다.


콤마연산자

우선순위 체계에서 가장 아래에 '콤마 연산자' 가 있다.5장 「프로그램 반복문」 에서 for 문 안의 어느 필드든,하나 이상 되는 표현식을 쉼표로 구분하여 넣을 수 있다고 언급하였다. 예를 들어 다음 for 문을 보자.

for ( i = 0, j = 100; i 1= 10; ++i, j -= 10 )


반복문을 시작하기 전에 i 의 값은 0 으로, j 의 값은 100 으로 초기화한다. 반복문의 몸체가 실행되고 나면 i 의 값은 1 씩 증가하고, j 의 값은 10 씩 감소된다.

Objective-C 내에 있는 모든 연산자는 값을 생성한다. 콤마 연산자의 값은 가장 우측 표현식의 값이다.


sizeof 연산자

프로그램에서 결코 데이터 형의 크기를 가정해서는 안 된다. 그러나 때로는 데이터 형이 얼마나 큰지 알 필요가 있다. malloc 과 같은 라이브러리 루틴을 사용해 동적 메모리 할당을 수행할 때나, 데이터를 파일로 아카이빙 혹은 기록할 때가 그 예다. Objective-C에서 제공하는 sizeof 는 연산자로 데이터 형이나 객체의 크기를 알아볼 수 있다. sizeof 연산자는 지정된 항목의 크기를 바이트 단위로 반환해 준다. 인수로는 변수나 배열 이름, 기본 데이터 형의 이름, 객체, 파생된 데이터 형의 이름, 혹은 표현식도 가능하다. 예를 들어, 다음 코드는 정수를 저장하는 데 필요한 바이트 수를 반환한다.

sizeof (int)


내 맥북 에어에서는 이 결과가 4(혹은 32비트)로 나왔다. 만일 x 가 정수 100 개를 담는 배열이라고 해보자.

sizeof (x)


이 표현식은 x의 정수 100 개를 저장할 공간을 반환할 것이다.

예컨대 myFract 가 int 인스턴스 변수 두 개 (numeratior 와 denominator)를 포함하는 Fraction 객체라면 다음 표현식의 결과는 어떨까?

sizeof (myFract)


포인터를 4 바이트로 표시하는 시스템에서는 이 표현식의 결과로 값 4 를 생성할것이다. 사실, sizeof 를 어느 객체에 적용하든 이 값을 반환한다. 그 이유는 객체의 데이터를 가리키는 포인터의 크기를 묻고 있기 때문이다. Fraction 객체의 인스턴스가 담긴 실제 데이터 구조의 크기를 알아내려면 다음과 같이 작성해야 한다.

sizeof (*myFract)


이 코드는 내 맥북 에어에서 12 라는 값을 반환한다. 어떻게 이 값이 나왔을까? numerator와 denominator 가 각각 4 바이트씩 차지하고, 여기에 상속 받은 isa 멤버(이 장의 마지막절 'Objective-C 가 동작하는 방식'에서 설명한다)가 4 바이트를 차지한다.

sizeof (struct data_entry)


이 표현식은 data_entry 구조체 하나를 저장하는 데 필요한 저장 공간의 값을 반환한다. 만일 data가 struct data_entry 원소들의 배열로 정의되어 있다고하자.

sizeof (data) / sizeof (struct data_entry)


이 경우 위 표현식을 적으면 data 안에 포함된 원소의 수가 결과 값으로 나온다(data 는 공식 매개변수나 외부에서 참조한 배열이 아니라 앞에서 정의한 배열이어야만 한다).

sizeof (data) / sizeof (data[0])


이 표현식도 똑같은 결과를 낳는다.

그러나 sizeof 연산자를 사용하여 프로그램에서 크기를 계산하거나 직접 입력해 넣는 일은 피하는 펀이 좋다.


커맨드라인 인수

이따금 터미널에서 사용자가 간단한 정보를 입력해야 하는 프로그램들이 있다. 이를테면 계산하고 싶은 삼각수나, 사전에서 찾을 단어 등의 정보가 필요한 경우가 있다.

이런 정보는 사용자에게 요청해 받는 대신, 프로그램이 실행될 때 제공해줄 수 있다. 바로 '커맨드라인 인수' 가 이런 기능을 지원한다.

앞에서 말했듯이 main 함수의 특성은 그 이름이 특별하다는 데 있다. main 에서 프로그램 실행이 시작된다. 사실, 여러분이 프로그램에서 보통 함수를 호출하는 것처럼 런타임 시스템은 프로그램 실행이 시작할 때 main 함수를 호출한다. main 이 실행을 마치면 컨트롤은 런타임 시스템으로 돌아간다. 이때 런타임 시스템은 프로그램이 종료되었음을 알게 된다.

런타임 시스템이 main 을 호출할 때, 인수 두개가 main 함수로 넘어간다. 첫 번째 인수는 argc(argument count 의 줄임말)라고 부르는 것이 관례다. 바로 커맨드라인에서 입력된 인수의 개수를 나타내는 정수값이다. 두번째 인수는 문자포인터의 배열로, argv(argument vector의 줄임말)라고 부르는 것이 관례다. 이 배열에는 문자 포인터가 argc + 1 개 만큼 들어 있다. 이 배열에서 첫째 원소는 실행중인 프로그램 이름을 향한 포인터이거나, 시스템에 따라 프로그램 이름을 얻을수 없는 때는 널스트링이다. 그 뒤를 따르는 원소들은 프로그램실행을 시작한 명령어와 같은 줄에 지정된 값들을 가리킨다. argv 의 마지막 포인터인 argv[argc] 는 널로 정의된다.

커맨드라인 인수에 접근하려면 main 함수가 인수를 두 개 받겠다고 선언해야만 한다. 이 책에 나오는 모든 프로그램은 관례적으로 다음과같이 선언한다.

int main (int argc, char' *argv[])
{
    ...
}


이 argv의 선언은 'char의 포인터' 형인 원소를 담는 배열로 정의됨을 기억하자. 커맨드라인 인수를 쓰는 실용적인 예를보자. 사전에서 단어를 검색하고 그 뜻을 표시하는 프로그램을 개발했다고 하자. 커맨드라인 인수를 사용하면 뜻을 찾고 싶은 단어를 다음 명령어로 프로그램 실행과 동시에 지정할 수 있다.

lookup aerie


이렇게 하면 커맨드라인에서 단어를 바로 입력하기 때문에 사용자에게 단어를 입력하라고 요구할 필요가 없어진다.

앞의 명령어가 실행되면,시스템은 자동으로 main 함수에 'aerie' 문자스트링을 가리키는 포인터를 argv[1] 으로 넘긴다. argv[0] 에는 프로그램 이름(이 경우 'lookup')을 가리키는 포인터가 담겨 있음을 기억하자.

main 루틴은 다음과 같이 구성되었다.

#include <Foundation/Foundation.h>

int main (int argc, char *argv[])
{
    struct entry dictionary[100] =
    { { "aardvark", "a burrowing African mammal" },
        { "abyss", "a bottomless pit" },
        { "acumen", "mentally sharp; keen" },
        { "addle", "to become confused" },
        { "aerie", "a high nest" },
        { "affix", "to append; attach" },
        { "agar", "a jelly made from seaweed" },
        { "ahoy", "a nautical call of greeting" },
        { "aigrette", "an ornamental cluster of feathers"},
        { "ajar", "partially opened" } };

    int entries = 10;
    int entryNumber;
    int lookup (struct entry dictionary [], char search[],
            int entries);

    if ( argc != 2 )
    {
        NSLog (@"No word typed on the command line.");
        return (1);
    }

    entryNumber = lookup (dictionary, argv[1], entries);

    if ( entryNumber != -1 )
        NSLog (@"%s", dictionary[entryNumber].definition);
    else
        NSLog (@"Sorry, %s is not in my dictionary.", argv[1]);

    return (0);
}


main 루틴은 프로그램이 실행될 때 프로그램 이름 다음에 단어가 입력되었는지 확인한다. 만일 입력되지 않았거나, 혹은 하나 이상 입력되었다면 argc 의 값이 2 가 아닐 것이다. 이럴때, 프로그램은 표준 출력으로 오류메시지를 내보내고 종료 상태를 1 로 반환하며 종료한다.

만일 argc 가 2 와 같다면, lookup 함수가 호출되어 argv[1] 이 가리키는 단어를 사전에서 찾는다. 만일 단어가 검색되면 단어 정의가 표시된다.

커맨드라인 인수는 언제나 문자 스트링으로 저장됨을 기억하자. 만일 프로그램 power를 커맨드라인 인수 2 와 16 으로 다음과 같이 실행하면 어떻게 될까?

power 2 16


argv[1] 안에 문자 스트링 '2' 의 포인터가 저장되고, argv[2] 안에 문자 스트링 '16' 을 가리키는 포인터가 저장된다. 만일 프로그램이 인수를 숫자로 해석하려면(이 power 프로그램에서는 당연히 그렇게 해야 할 것이다) 프로그램이 직접 변환해 줘야 한다. 이 런 변환 작업은 sscanf, atof, atoi, strtod, strtol 같은 루틴들이 수행하는데, 이것들은 모두 라이브러리로 제공된다. 2부에서 NSProcessInfo 클래스를 사용하여 C 스트링 대신 스트링 객체로 커맨드라인 객체에 접근하는 방법도 배울 것이다.


Objective-C 가 동작하는 방식

여기서 C 와 연관된 몇 가지 특정을 짚지 않고 이 장을 은근슬쩍 마칠 수는 없다. Objective-C 가 C 언어를 근간으로 하기 때문에, 두 언어의 관계에 대해서 언급해두는 펀이 좋겠다. 이런 상세 구현 내용은 무시해도 되지만, 포인터를 메모리 주소라고 배우는 펀이 포인터를 이해하는 데 도움이 되듯이, 알아두면 Objective-C 의 동작을 이해하는 데 도움이 될 것이다. 그렇다고 무척 상세한 내용까지 다루지는 않는다. Objective-C 와 C 의 관계에 대한 네 가지 사실만 알아보고 넘어가자.


사실 1: 인스턴스 변수는 구조체에 저장된다

새 클래스와 그에 속한 인스턴스 변수를 정의하면, 이 인스턴스 변수들은 사실 구조체 안에 저장된다. 이를 통해 객체를 다룰 수 있게 된다. 실은 객체는 인스턴스변수로 멤버들이 구성된 구조체다. 따라서 상속받은 인스턴스 변수와 클래스에, 새로 정의한 인스턴스 변수가 합쳐져 하나의 구조체를 형성한다 alloc 으로 새 객체를 생성하면 구조체 하나를 담기에 충분한 메모리공간이 예약된다.

구조체의 상속받은 멤버 중에 보호(protected) 멤버 isa(루트 객체에게서 상속받은 것이다)가 있는데, 이것이 객체가 속한 클래스를식별한다. 이 멤버가 구조체의 일부이므로 (따라서, 객체의 일부이므로) 계속 객체에 담겨 있게 된다. 런타임 시스템은 언제든 isa 멤버를 들여다보면 (심지어 객체가 포괄적인 id 객체 변수에 대입되었을 지라도) 객체의 클래스가 무엇인지 알 수 있다.

객체의 구조체에 속한 멤버를 @public 으로 지정해 주면 직접 접근할 수도 있다. (10장 「변수와 데이터 형에 대하여」를 참고하라). 예를 들어, Fraction 클래스의 멤버인 numerator 와 denominator 를 @public 으로 지정했다면, 다음 표현식을 이용하여 Fraction 객체인 myFract 의 numerator 멤버에 직접 접근할 수 있다.

myFract -> numerator


그렇지만 이 방법에는 강하게 반대한다. 10장에서 언급했듯이, 이것은 데이터 캡슐화에 어긋나기 때문이다.


사실 2: 객체 변수는 사실 포인터다

Fraction과 같은 객체 변수를 다음과 같이 정해 보자.

Fraction *myFract;


이는 사실 myFract 는 포인터 변수를 정의하는 것이다. 이 변수는 클래스 이름인 Fraction 형의 무엇인가를 가리키도록 정의되었다. 자, 다음과 같이 Fraction 의 새 인스턴스를 생성한다.

myFract = [Fraction alloc];


이 식으로 메모리에 새 Fraction 객체를 저장할 공간(구조체를 위한 공간)을 할당하고 반환되는 구조체의 포인터를 포인터 변수 myFrnct 안에 저장한다.

이제 다음과 같이 한 객체를 다른 객체에 대입하면 포인터가 복사되는 것이다.

myFract2 = myFract1;


이러면 두 변수 모두 메모리의 어딘가에 저장된 동일한 구조체를 가리키게 된다. myFract2 로 참조한(가리키는) 멤버 중 하나에 변화를 가하면, myFract1 이 참조하는 동일한 인스턴스 변수(구조체 멤버)에 변화를 가한 셈이 된다.


사실 3: 메서드는 함수이고, 메시지 표현식은 함수 호출이다

메서드는 사실 함수다. 메서드를 호출하면, 수신자의 클래스에 연결된 함수를 호출하는 셈이다. 함수에 건네는 인수들은 수신자(self)와 메서드의 인수들이다. 따라서, 함수에 인수를 건네는 것, 반환값, 자동 혹은 정적 변수와 관련된 모든 규칙은 함수에도 메서드에도 모두 동일하게 적용된다. Objective-C 컴파일러는 클래스 이름과 메서드 이름을 조합해서 각 함수에 맞는 독특한 이름을 생성한다.


사실 4: id형은 일반 포인터 형이다

객체는 메모리 주소인 포인터로 참조되기 때문에 id 변수에 자유롭게 할당할 수 있다. id 를 반환하는 메서드는 결과적으로 메모리의 어느 객체를 가리키는 포인터를 반환하는 것이다. 그 후, 그 값을 어느 객체 변수에든 대입할 수 있다. 객체는 언제나 isa 멤버를 보유하기 때문에 id 형인 일반 객체 변수에 저장해도 클래스를 언제든 확인할 수 있다.


연습문제

1. 부동소수점 값 10 개로 구성된 배열의 평균을 구하여 그 결과를 반환하는 함수를 작성하라.


2. Fraction 클래스의 reduce 메서드는 분수를 약분하기 위해 분자와 분모의 최대공약수를 찾는다. 이 메서드롤 프로그램 13.5 의 gcd 함수를 사용하도록 수정하라. 함수 정의를 어디에 두어야 할까? 함수를 정적으로 만들었을때 장점이 있는가? gcd 함수를 사용하는 방식과 이전처럼 코드를 메서드에 직접 작성하는 방식 가운데 어느 쪽이 낫다고 생각하는가? 그 이유는 무엇인가?


3. '에라토스테네스의 체(Sieve of Erastosthenes)'라고 알려진 알고리즘은 소수(prime number)를 생성한다. 알고리즘의 절차는 아래에 제시했다. 이 알고리즘을 구현하는 프로그램을 작성하여, n = 150 까지 이르는 모든 소수를 찾아라. 이전에 소수를 계산하는 알고리즘과 이 알고리즘을 비교하면 어떤가?

1 단계: 정수 배열 p 를 정의한다. p 의 모든 원소 pi 가 0 과 2 <= i <= n 이 되도록 설정한다.
2 단계: i 를 2 로 설정한다.
3 단계: 만일 i > n 이면 알고리즘은 종료된다.
4 단계: 만일 p 가 0 이면 i 는 소수다.
5 단계: 모든 양의 정수 j 에 대해 i x j <= n 이면 p(i x j)를 1 로 설정한다.
6 단계: i 에 1 을 더하고 3단계로 간다.


4. 배열로 넘겨지는 Fraction 을 모두 더하여 그 결과를 Fraction 으로 반환하는 함수를 작성하라.


5. struct date 에 대한 typedef 정의인 Date 를 작성하여 다음과 같은 선언이 가능하게 만들어라.

Date todaysDate;


6. 앞에서 언급했듯이, Date 클래스를 정의하는 펀이 date 구조체를 정의하는 것보다 더 객체지향 프로그래밍의 개념에 일치한다. 적절한 세터, 게터 메서드와 함께 Date 클래스를 정의하라. 또한 dataUpdate 라는 메서드를 작성하여 인수 다음 날짜를 반환하도록 하라.

Date 를 구조체 대신 클래스로 정의하여 생기는 장점이 있는가? 단점은 무엇인가?


7. 다음과 같은 정의가 있다.

char *message = "Programming in Objective-C is fun";
char message2[] = "You said it";
int x = 100;

다음의 각 NSLog 호출 모음이 유효한지, 그리고 다른 호출 모음과 동일한 출력 결과가 나오는지 알아보라.

/*** set 1 ***/
NSLog (@"Programming in Objective-C is fun");
NSLog (@"%s", "Programming in Objective-C is fun");
NSLog (@"%s", message);

/*** set 2 ***/
NSLog (@"You said it");
NSLog (@"%s", message2);
NSLog (@"%s", &message2[0]);

/*** set 3 ***/
NSLog (@"said it");
NSLog (@"%s", message2 + 4);
NSLog (@"%s", &message2[4]);


8. 프로그램을 작성하여 그 자신의 모든 커맨드라인 인수를 터미널에 각각 한줄로 표시하라. 따옴표 안에 공백을 포함한 문자 인수가 들어가면 어떻게 되는지 알아보라.


9. 다음중 어느 명령문이 "This is a test?"를 출력하는가? 그 이유를 설명하라.

NSLog (@"This is a test");
NSLog ("This is a test");

NSLog (@"%s", "This is a test");
NSLog (@"%s", @"this is a test");

NSLog ("%s", "This is a test");
NSLog ("%s", @"This is a test");

NSLog (@"%@", @"This is a test");
NSLog (@"%@", "This is a test");


Notes

  1. 1.0 1.1 엄밀히 말하면 NSObjectRuntime.h 파일에 정의되어 있는데, 이 파일이 Foundation.h 파일에서 임포트된다. Cite error: Invalid <ref> tag; name "주석8" defined multiple times with different content