Objective-C · 기본
Objective-C 문법과 Cocoa 기초
값과 타입 - C 타입과 객체 타입
C 기본 타입, NSInteger·CGFloat·BOOL(YES·NO), 객체 포인터와 *, id, nil 과 NULL, 형식 지정자(%ld·%@)
개발자KR · 원고 갱신
이 장에서 배우는 것
Objective-C는 C 언어 위에 만들어졌으므로 변수에 쓰는 타입도 두 갈래다. 하나는 int, double처럼 값 자체를 담는 C 타입이고, 다른 하나는 NSString *처럼 객체의 주소를 담는 객체 포인터 타입이다. 이 장은 두 갈래를 구분하는 법과, 값을 화면에 찍을 때 타입마다 다른 형식 지정자(format specifier)를 맞춰 쓰는 법을 다룬다. 사내 도서 대출 관리 도구에 들어갈 연체 일수, 연체료, 대출 중 여부, 도서 제목 같은 값을 변수에 담아 출력하는 작은 프로그램이 이 장의 결과물이다.
- C 기본 타입의 종류와 크기, 그에 맞는 형식 지정자를 말할 수 있다.
NSInteger,CGFloat,BOOL(YES·NO)을 언제 쓰는지 설명할 수 있다.- 객체 변수에
*가 붙는 이유와, 두 변수가 같은 객체를 가리키는 상황을 그림으로 설명할 수 있다. id,nil,NULL을 구분해 쓸 수 있다.%ld와%@로 값을 출력하고, 결과 출력에NSLog를 쓰지 않는 이유를 설명할 수 있다.
문제 상황
대출 관리 도구에 연체 안내 기능을 넣는다고 하자. 회원 한 명의 연체 일수가 6이고 하루 연체료가 150.5원이면 연체료는 903.0원이다. 이 값을 계산해서 화면에 찍는 코드는 몇 줄 되지 않는데, 처음 쓰면 다음과 같은 일이 생긴다.
- 연체 일수를
int로 둘지long으로 둘지, Cocoa 코드에서 흔히 보이는NSInteger는 무엇인지 알 수 없다. - 열람률을
읽은 쪽수 / 전체 쪽수로 계산했더니 항상0이 나온다. printf에%d를 썼더니 컴파일러가 경고를 내거나 값이 이상하게 찍힌다.- 도서 제목을 담은 변수를
printf("%@", title)로 찍으면 원하는 글자가 나오지 않는다. - 값이 비어 있는 객체와 비어 있는 C 문자열 포인터를 어떻게 표현하는지 헷갈린다.
모두 "이 변수가 어떤 종류의 값인가"를 정확히 알면 풀리는 문제다. 아래에서 종류별로 정리한다.
C 타입과 Foundation 타입
C 기본 타입
Objective-C에서는 C의 기본 타입을 그대로 쓸 수 있다. 정수 타입은 담을 수 있는 범위에 따라 char, short, int, long, long long이 있고, 실수 타입은 float와 double이 있다. 크기는 플랫폼마다 다를 수 있다. 아래 표는 64비트 macOS와 64비트 Linux에서 흔한 값이며, 프로그램 안에서는 sizeof로 직접 확인할 수 있다.
| 타입 | 크기(바이트) | 형식 지정자 | 도구에서의 예 |
|---|---|---|---|
char | 1 | %c | 등급 문자 'A' |
short | 2 | %hd | 작은 번호 값 |
int | 4 | %d | 보유 권수 |
long | 8 | %ld | 서가 번호 |
long long | 8 | %lld | 아주 큰 식별 번호 |
float | 4 | %f | 쓰는 일이 드물다 |
double | 8 | %f | 도서 가격 |
float를 printf에 넘기면 C 규칙에 따라 double로 바뀌어 전달되므로 둘 다 %f를 쓴다. 소수 자릿수는 %.1f처럼 점 뒤에 적는다.
NSInteger, CGFloat, BOOL
Foundation은 C 타입에 별명을 붙인 타입을 제공한다. Cocoa API의 매개변수와 반환값은 거의 이 별명으로 적혀 있어서 같은 이름으로 맞춰 쓰는 편이 자연스럽다.
| 타입 | 실제 모습 | 출력할 때 | 쓰임 |
|---|---|---|---|
NSInteger | 64비트에서 long, 32비트에서 int | %ld + (long) 캐스트 | 부호 있는 정수 |
NSUInteger | 64비트에서 unsigned long | %lu + (unsigned long) 캐스트 | 개수, 길이 |
CGFloat | 64비트에서 double, 32비트에서 float | %f + (double) 캐스트 | 실수 계산 |
BOOL | 1바이트 정수 또는 bool | 직접 문자열로 바꿔 출력 | 참·거짓 |
NSInteger는 컴파일 대상 플랫폼에 따라 int일 수도 long일 수도 있다. 그래서 %d와 %ld 중 하나로 고정하면 한쪽 환경에서 형식이 어긋난다. 항상 (long)으로 바꿔서 %ld로 찍으면 어느 환경에서나 맞는다. CGFloat도 같은 이유로 (double)로 바꿔 %f로 찍는다. 이 규칙을 묶어서 그림으로 나타내면 다음과 같다.
BOOL에는 참을 뜻하는 YES와 거짓을 뜻하는 NO가 대응한다. YES는 1, NO는 0이다. 조건식 lateDays > 0의 결과를 BOOL 변수에 바로 담을 수 있다. printf에는 BOOL용 형식 지정자가 따로 없으므로, 이 장의 예제는 작은 함수로 "YES"나 "NO" 문자열을 골라 출력한다.
정수끼리 나누면 몫만 남는다는 C 규칙도 그대로 적용된다. 100 / 250은 0.4가 아니라 0이다. 한쪽을 먼저 CGFloat로 바꾸면 실수 나눗셈이 된다.
객체 타입: 포인터, id, nil
객체 포인터와 *
C에서 int *p = &copies;는 p가 copies의 주소를 담는다는 뜻이다. Objective-C의 객체도 같은 방식으로 다룬다. 객체는 메모리의 다른 곳에 만들어지고, 변수에는 그 객체의 주소만 들어 있다. 그래서 문자열 객체를 담는 변수는 NSString *title로 쓴다. NSString title처럼 *를 빼면 컴파일 오류다. 객체는 변수 안에 직접 들어갈 수 없기 때문이다.
변수가 값이 아니라 주소를 담으므로, 대입은 객체를 복사하지 않고 주소만 복사한다. NSString *alias = title;을 실행하면 변수 두 개가 같은 객체를 가리킨다. alias == title은 두 변수가 같은 주소를 담았는지 비교한다.
선언할 때 변수 여러 개를 쉼표로 이으면 *는 바로 뒤의 이름에만 붙는다. NSString *a, b;에서 a는 포인터이고 b는 NSString 값이므로 컴파일 오류가 난다. 객체 변수는 한 줄에 하나씩 선언하는 편이 안전하다.
id
id는 "어떤 클래스인지 정하지 않은 객체 포인터"다. *를 붙이지 않는다. id item은 문자열 객체도, 숫자 객체도 담을 수 있다. 서로 다른 종류의 객체를 한 변수에서 다루어야 할 때 쓰며, 종류를 알고 있다면 NSString *처럼 구체적으로 적는 편이 컴파일러가 실수를 더 많이 잡아 준다. 객체가 실제로 어떤 클래스인지는 isKindOfClass:라는 메시지로 물어볼 수 있다. 메시지를 보내는 대괄호 문법은 다음 장에서 자세히 다루므로 지금은 "객체에게 질문하고 BOOL을 돌려받는다" 정도로만 읽으면 된다.
nil과 NULL
아무것도 가리키지 않는 포인터에도 이름이 있다. 객체 포인터가 비어 있을 때는 nil, C 포인터(char *, int * 등)가 비어 있을 때는 NULL을 쓴다. 둘 다 내부적으로는 주소 0이지만, 읽는 사람에게 변수의 종류를 알려 주므로 구분해서 쓴다. 클래스 자체를 담는 변수에는 Nil을 쓰는데, 이 책에서 쓸 일은 거의 없다.
| 이름 | 쓰는 대상 | 예 | 출력 |
|---|---|---|---|
nil | 객체 포인터 | NSString *memo = nil; | %@로 찍으면 (null) |
NULL | C 포인터 | const char *raw = NULL; | %s에 넘기면 안 되므로 먼저 비교 |
NO, 0 | BOOL, 정수 | BOOL done = NO; | 값 그대로 |
Objective-C에서는 nil에게 메시지를 보내도 프로그램이 멈추지 않고, 반환 타입이 숫자라면 0을 돌려받는다. 이 장의 예제에서도 nil 문자열의 길이를 물어 0을 확인한다. 이 동작을 어떻게 활용하는지는 메시지를 다룰 다음 장에서 이어 설명한다. 반면 NULL 포인터를 %s에 넘기거나 역참조하는 것은 C의 정의되지 않은 동작이므로 반드시 먼저 비교해야 한다.
형식 지정자와 출력 함수
문자열을 만들 때는 [NSString stringWithFormat:@"..."]를 쓴다. 이 형식 문자열에서는 C의 지정자에 더해 객체를 위한 %@를 쓸 수 있다. %@는 객체의 설명 문자열을 끼워 넣는다. 문자열 객체는 자기 내용을, 숫자 객체는 숫자를 보여 준다. 한편 C의 printf는 %@를 모른다. 객체를 printf로 찍으려면 [title UTF8String]처럼 C 문자열(const char *)로 바꿔서 %s에 넘긴다. UTF8String이 돌려주는 포인터는 이 객체가 살아 있는 동안 쓰는 값이라, 곧바로 출력하는 용도로만 쓴다.
NSLog는 쓰지 않는다. NSLog는 출력 앞에 날짜, 시각, 프로세스 이름과 번호를 붙이고, 그 내용을 표준 출력이 아닌 표준 오류로 보낸다. 실행할 때마다 시각이 달라지므로 결과가 항상 같아야 하는 예제 출력에 어울리지 않고, 셸에서 >로 표준 출력만 파일에 저장하면 NSLog 내용은 빠진다. 이 책에서는 결과 출력을 printf("%s\n", [문자열 UTF8String])로 하고, NSLog는 진단 메시지를 남기는 용도로만 따로 다룬다. 지정자 전체 목록은 Apple의 문자열 형식 지정자 문서에서 확인할 수 있다.
| 지정자 | 받는 값 | 쓸 수 있는 곳 | 넘기는 모양 |
|---|---|---|---|
%d | int | printf, stringWithFormat: | copies |
%ld | long | 둘 다 | (long)lateDays |
%lu | unsigned long | 둘 다 | (unsigned long)[title length] |
%f | double | 둘 다 | (double)fee |
%s | const char * | 둘 다 | [title UTF8String] |
%@ | 객체 | stringWithFormat: 같은 Foundation 함수만 | title |
%% | 없음(퍼센트 기호) | 둘 다 | 없음 |
완성 코드
아래 프로그램은 앞에서 설명한 타입을 한 번씩 사용한다. 파일 하나(main.m)로 되어 있다.
#import <Foundation/Foundation.h>
static void say(NSString *text) {
printf("%s\n", [text UTF8String]);
}
static const char *yesNo(BOOL flag) {
return flag ? "YES" : "NO";
}
int main(void) {
@autoreleasepool {
// 1. C 기본 타입
int copies = 3;
long shelfNo = 12L;
double price = 18500.5;
char grade = 'A';
printf("[C 타입] copies=%d shelfNo=%ld price=%.1f grade=%c\n",
copies, shelfNo, price, grade);
printf("[크기] int=%zu long=%zu double=%zu char=%zu\n",
sizeof(int), sizeof(long), sizeof(double), sizeof(char));
printf("[크기] NSInteger=%zu CGFloat=%zu BOOL=%zu\n",
sizeof(NSInteger), sizeof(CGFloat), sizeof(BOOL));
// 2. NSInteger 와 CGFloat
NSInteger lateDays = 6;
CGFloat feePerDay = 150.5;
CGFloat fee = lateDays * feePerDay;
printf("[NSInteger/CGFloat] lateDays=%ld fee=%.1f\n",
(long)lateDays, (double)fee);
NSInteger pages = 250;
NSInteger readPages = 100;
NSInteger wrongRatio = readPages / pages;
CGFloat ratio = (CGFloat)readPages / pages;
printf("[나눗셈] 정수=%ld 실수=%.1f\n", (long)wrongRatio, (double)ratio);
printf("[진행률] %.0f%%\n", (double)(ratio * 100));
// 3. BOOL
BOOL isLate = lateDays > 0;
BOOL isOnShelf = NO;
printf("[BOOL] isLate=%s isOnShelf=%s\n", yesNo(isLate), yesNo(isOnShelf));
if (isLate && isOnShelf == NO) {
say(@"연체 중이며 서가에 없음");
}
// 4. 객체 포인터
NSString *title = @"Objective-C 입문";
NSString *alias = title;
say([NSString stringWithFormat:@"[객체] title=%@ 길이=%lu",
title, (unsigned long)[title length]]);
printf("[포인터] alias==title: %s\n", yesNo(alias == title));
// 5. id
id item = @"사내 규정집";
say([NSString stringWithFormat:@"[id] 문자열 담김: %@", item]);
item = @42;
say([NSString stringWithFormat:@"[id] 숫자 담김: %@", item]);
printf("[id] 문자열인가: %s\n", yesNo([item isKindOfClass:[NSString class]]));
// 6. nil 과 NULL
NSString *missing = nil;
const char *rawName = NULL;
say([NSString stringWithFormat:@"[nil] missing=%@ 길이=%lu",
missing, (unsigned long)[missing length]]);
printf("[NULL] rawName=%s\n", rawName == NULL ? "NULL" : rawName);
}
return 0;
}
줄별 해설
#import <Foundation/Foundation.h>:NSString,NSInteger,CGFloat,BOOL등이 선언된 Foundation을 읽어 들인다.#include와 달리 같은 파일을 두 번 읽지 않는다.say: 문자열 객체를 받아UTF8String으로 C 문자열로 바꾼 뒤printf로 한 줄 출력한다. 결과를 표준 출력으로 보내는 통로를 한곳에 모은 것이다. 매개변수NSString *text가 객체 포인터 선언의 예다.yesNo:BOOL을 받아"YES"또는"NO"를 돌려준다. 조건 연산자? :는 C와 같다.int main(void)와@autoreleasepool { ... }: 프로그램 시작점과, 객체의 메모리 관리를 위한 블록이다. 메모리 관리는 뒤에서 따로 다루므로 지금은 Foundation 코드를 감싸는 관용구로 본다.- 1번 묶음:
int,long,double,char변수를 만들어%d %ld %f %c로 찍는다.12L의L은long리터럴 표시다.sizeof의 결과 타입은size_t이고 지정자는%zu다. [크기] NSInteger=...줄: 64비트 환경에서NSInteger와CGFloat가 8바이트,BOOL이 1바이트임을 확인한다.- 2번 묶음:
lateDays * feePerDay에서 정수가 실수로 자동 변환되어903.0이 된다. 출력할 때는(long),(double)캐스트로 지정자와 타입을 맞춘다. wrongRatio와ratio: 같은100과250으로 나누지만 앞은 정수 나눗셈이라0, 뒤는(CGFloat)readPages로 먼저 바꿔 실수 나눗셈을 하므로0.4다.%.0f%%는 소수 없이 출력하고 뒤에 퍼센트 기호를 붙인다.- 3번 묶음: 비교식의 결과를
BOOL에 담고,isLate && isOnShelf == NO가 참이면 안내 문장을 출력한다. - 4번 묶음:
@"..."는 문자열 객체를 만드는 리터럴이다.NSString *alias = title;은 객체를 복사하지 않고 주소만 복사한다.stringWithFormat:안의%@는title을,%lu는(unsigned long)으로 바꾼 길이를 받는다. "Objective-C 입문"은 영문·기호 11자, 공백 1자, 한글 2자라 길이가 14다. - 5번 묶음:
id item에 문자열을 담았다가@42(숫자 객체 리터럴)로 바꿔 담는다. 숫자 객체는 7장에서 자세히 다룬다.isKindOfClass:의 결과는 이제 숫자가 들어 있으므로NO다. - 6번 묶음:
nil문자열은%@로(null)이 찍히고, 길이를 물으면0이다.NULLC 포인터는%s에 넘기기 전에== NULL로 확인해서 안전하게 출력한다. return 0;: 정상 종료를 알린다. 표준 입력은 읽지 않으므로 실행하면 곧바로 끝난다.
실행 결과
파일 이름을 main.m으로 저장하고 다음 명령으로 컴파일한 뒤 실행한다. 경고는 나오지 않아야 한다.
clang -fobjc-arc -Wall -framework Foundation main.m -o app
./app
예상 출력은 다음과 같다. 크기 출력은 64비트 환경 기준이다.
[C 타입] copies=3 shelfNo=12 price=18500.5 grade=A
[크기] int=4 long=8 double=8 char=1
[크기] NSInteger=8 CGFloat=8 BOOL=1
[NSInteger/CGFloat] lateDays=6 fee=903.0
[나눗셈] 정수=0 실수=0.4
[진행률] 40%
[BOOL] isLate=YES isOnShelf=NO
연체 중이며 서가에 없음
[객체] title=Objective-C 입문 길이=14
[포인터] alias==title: YES
[id] 문자열 담김: 사내 규정집
[id] 숫자 담김: 42
[id] 문자열인가: NO
[nil] missing=(null) 길이=0
[NULL] rawName=NULL
Linux에서 GNUstep으로 빌드할 때는 -framework Foundation 대신 gnustep-config가 알려 주는 옵션을 써야 한다. 이 책의 명령은 macOS 기준이다.
실무에서 자주 틀리는 것
NSInteger를 %d로 찍는다
틀린 코드:
NSInteger lateDays = 6;
printf("연체 %d일\n", lateDays);
64비트에서 NSInteger는 long이므로 %d와 크기가 맞지 않는다. -Wall이 형식 경고를 내고, 값이 큰 경우 잘못된 숫자가 찍힐 수 있다. 고친 코드:
printf("연체 %ld일\n", (long)lateDays);
정수끼리 나눈 뒤 실수에 담는다
틀린 코드:
NSInteger readPages = 100;
NSInteger pages = 250;
CGFloat ratio = readPages / pages; // 0.0
나눗셈은 두 피연산자가 정수이면 정수로 계산된 뒤에 변환된다. 결과를 담는 변수의 타입은 계산 방식을 바꾸지 않는다. 고친 코드:
CGFloat ratio = (CGFloat)readPages / pages; // 0.4
printf에 %@를 쓰거나 객체를 %s에 넘긴다
틀린 코드:
NSString *title = @"Objective-C 입문";
printf("%@\n", title);
printf("%s\n", title);
printf는 %@를 해석하지 못한다. %s는 C 문자열 주소를 기대하는데 객체 주소를 받으므로 엉뚱한 바이트를 읽는다. 고친 코드:
printf("%s\n", [title UTF8String]);
BOOL을 YES와 직접 비교하거나 정수를 그대로 담는다
틀린 코드:
NSInteger copies = 3;
BOOL hasStock = copies;
if (hasStock == YES) { /* ... */ }
어떤 환경에서는 BOOL이 1바이트 정수라서 3이 그대로 들어가고, 3 == YES는 거짓이 된다. 참이면서도 조건에 걸리지 않는 코드가 되는 것이다. 고친 코드는 조건식의 결과를 담고, 검사할 때는 변수 자체를 쓴다.
BOOL hasStock = copies > 0;
if (hasStock) { /* ... */ }
한눈에 보기
| 주제 | 핵심 | 출력 | 주의 |
|---|---|---|---|
| C 기본 타입 | 값을 변수에 직접 담는다 | %d %ld %f %c | 크기는 플랫폼마다 다르다 |
NSInteger | 플랫폼에 맞는 정수 타입 | %ld + (long) | %d 사용 금지 |
CGFloat | 플랫폼에 맞는 실수 타입 | %f + (double) | 정수끼리 나누지 않는다 |
BOOL | YES·NO | 문자열로 바꿔 출력 | == YES 비교를 피한다 |
| 객체 포인터 | NSString *처럼 주소를 담는다 | %@(Foundation), UTF8String(printf) | 대입은 주소 복사다 |
id | 클래스를 정하지 않은 객체 포인터 | %@ | *를 붙이지 않는다 |
nil / NULL | 객체용 / C 포인터용 빈 값 | (null) / 먼저 비교 | NULL을 %s에 직접 넘기지 않는다 |
연습 문제
NSInteger renewCount = 2; CGFloat fine = 450.0; NSString *member = @"김하나";세 변수를 한 줄 문자열김하나 연장 2회 연체료 450.0으로 만들어 출력하는 문장을 써라. 결과는 표준 출력으로 보내야 한다.- 다음 코드는 반납률을
0.75로 계산하려 하지만0.0이 나온다. 이유를 설명하고 고쳐라.NSInteger returned = 3; NSInteger total = 4; CGFloat rate = returned / total; - 다음 코드의 출력을 예측하라.
NSString *memo = nil; id value = memo; printf("%s\n", value == nil ? "비어 있음" : "있음"); value = @"대출"; printf("%s\n", value == nil ? "비어 있음" : "있음"); - 다음 네 변수 중
nil로 초기화해야 할 것과NULL로 초기화해야 할 것을 나누고, 이유를 한 줄로 써라:NSString *a;,const char *b;,id c;,char *d;
정답과 해설
- 정답:
printf("%s\n", [[NSString stringWithFormat:@"%@ 연장 %ld회 연체료 %.1f", member, (long)renewCount, (double)fine] UTF8String]);%@는 문자열 객체,%ld는(long)으로 바꾼NSInteger,%.1f는(double)로 바꾼CGFloat를 받는다. 앞에서 만든say함수가 있다면say([NSString stringWithFormat:...])로 줄일 수 있다.NSLog는 표준 오류로 나가고 시각이 붙으므로 쓰지 않는다. returned와total이 둘 다 정수라서3 / 4가 정수 나눗셈으로 먼저 계산되어0이 되고, 그 뒤에0.0으로 변환되어 담긴다. 한쪽을 먼저 실수로 바꾸면 된다:CGFloat rate = (CGFloat)returned / total;이렇게 하면0.75가 된다.- 출력은
비어 있음,있음두 줄이다.memo가nil이라id변수value도 처음에는nil이다. 문자열을 대입하면nil이 아니게 된다.id변수는 객체 포인터이므로nil과 비교할 수 있다. a와c는 객체 포인터(NSString *,id)이므로nil,b와d는 C 문자열 포인터(const char *,char *)이므로NULL이다. 값은 같은 0이지만 변수의 종류를 이름으로 드러내는 관례다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.