Starbucks Caramel Frappuccino
본문 바로가기
  • 그래 그렇게 조금씩
iOS Developer/Objective-C

11. Objective-C Pointers 포인터

by Toughie 2024. 5. 9.

KAKAO-Choonsik

동적 메모리 할당과 같은 기능을 포함해 C계열 언어에서 포인터 개념은 필수적이다.

 

변수는 사실상 메모리 주소이며, &(ampersand)연산자로 접근할 수 있다.

#import <Foundation/Foundation.h>

int main (void) {
   int  var1;
   char var2[10];

   NSLog(@"Address of var1 variable: %p\n", &var1 ); //0x16fdff238
   NSLog(@"Address of var2 variable: %p\n", &var2 ); //0x16fdff23e
   NSLog(@"Size of var1: %d, size of var2: %d,",sizeof(var1),sizeof(var2)); // 4, 10

   return 0;
}

 

출력되는 메모리 주소는 계속 변할 수 있고, 위 예시 코드에서 int는 4바이트, char 배열은 1 * 10으로 10바이트임을 알 수 있다.


포인터란?

포인터는 값이 다른 변수의 주소인 변수이다. 

 

//type  *(asterisk) var-name;
int     *ip;
double  *dp;
float   *fp;
char    *ch;

 

모든 포인터의 값은 16진수의 메모리 주소이다.(값의 타입은 동일하다.)

포인터 변수의 타입은  '포인터가 가리키는 대상의 타입'을 의미한다.

 

위 포인터들의 size를 찍어보면 전부 동일하게 8byte가 나온다.

(C의 경우 포인터의 크기는 32비트 환경에서는 4바이트, 64비트 환경에서는 8바이트였다.)


포인터 사용하기

1. 포인터 변수를 정의한다.

2. 포인터에 변수의 주소를 할당한다.

3. 포인터 연산자를 통해 포인터가 가리키는 값에 접근한다. (* 연산자_해당 주소에 있는 값을 반환)

#import <Foundation/Foundation.h>

int main(void) {
    int value = 7;
    int *pointer;
    pointer = &value;
    
    NSLog(@"Address of var value: %p",&value); //0x16fdff248
    NSLog(@"Address stored in pointer variable: %p",pointer); //0x16fdff248
    NSLog(@"Access the value of pointer: %d\n",*pointer); //7
    
    return 0;
}

NULL Pointer

포인터를 정의하고 나서 제대로된 주소를 할당하지 않으면,

쓰레기값이 들어있어서 잘못된 접근을 하게되는 불상사가 일어날 수 있다.

이런 상황을 방지하기 위해서 정확한 주소 할당이 어려운 상황이라면 포인터를 NULL로 초기화 해주자. 

NULL 값이 할당된 포인터를 NULL Pointer라고 한다.(NULL 포인터는 0값을 갖는 상수이다.)

즉 포인터가 아무것도 가리키지 않는다는 것을 의미한다.

#import <Foundation/Foundation.h>

int main(void) {
    int *ptr = NULL;
    NSLog(@"value of the ptr: %x\n", ptr); //0
    
    if(ptr) {
        NSLog(@"ptr IS NOT NULL");
    } else {
        NSLog(@"ptr IS NULL!");
    }
    
    return 0;
}

 

대부분의 운영체제에서 주소0의 메모리에는 접근할 수 없다. 

즉 NULL은 포인터가 엑세스 가능한 메모리 위치를 가리키지 않는다는 것을 의미하는 값인데 

*를 통해 엑세스 하려고 하면

Thread 1: EXC_BAD_ACCESS (code=1, address=0x0)

 

무시무시한 NULL POINTER EXCEPTION이다.

이 에러는 런타임에 앱 크래시가 발생하기 때문에 아주 조심해야 한다.

NSLog(@"value of the ptr: %x\n", *ptr);

 

따라서 NULL 체크를 꼭 해줘야 한다.  방법은 다양하겠지만 조건문, 삼항 연산자, 코얼레싱 등을 주로 사용한다.

if(ptr) {
    NSLog(@"ptr IS NOT NULL");
} else {
    NSLog(@"ptr IS NULL!");
}
    
NSLog(@"value of the ptr: %x\n", ptr ? ptr : -1);

포인터 연산

포인터는 메모리주소인 16진수 정수이기 때문에  연산이 가능하다. 

모든 연산이 가능한 것은 아니고, 더하기, 빼기, 증감 연산(++,--)이 가능하다.

 

정수형 포인터 ptr이 있다고 생각해보자.

ptr은 1000번지 주소를 가리킨다고 가정한다.

 

ptr++을 하면 어떻게 될까?

일반 정수에 증감연산자++을 하면 1이 늘어난다.

하지만 ptr은 포인터 이기 때문에 '타입의 바이트 수만큼 주소값이 증가' 한다.

즉 1000번지에서 1004번지를 가리키게 되는 것이다.

(만약 ptr이 정수형 포인터가 아닌 char 타입 포인터였다면 1바이트 증가한 1001번지가 되는 것이다.)

이러한 연산은 메모리에 위치한 실제 값을 건드리는 것이 아니라, 포인터가 가리키는 주소만 변경하게 된다.

 

배열변수는 '포인터 상수'라고 했다. 즉 값을 변경할 수가 없다.

하지만 이 포인터 연산의 개념을 활용하면 포인터 변수를 만들어서 더 편리하게 배열을 순회할 수 있다.

 

++ 연산자 활용 예시

#import <Foundation/Foundation.h>

const int ARRSIZE = 3;

int main(void) {
    int var[ARRSIZE] = {1,2,3};
    int i, *ptr;
    
    ptr = var;
    for (i=0; i < ARRSIZE; i++) {
        NSLog(@"Address of Arr[%d] = %p",i,ptr);
        NSLog(@"Value of Arr[%d] = %d",i,*ptr);
        ptr++;
    }
    
    //'배열변수는 포인터 상수'의 개념을 이용하기
    for(i=0; i<ARRSIZE; i++){
        NSLog(@"Address of Arr[%d] = %p",i,(var+i));
        NSLog(@"Value of Arr[%d] = %d",i, *(var+i));
    }
    
    return 0;
}

 

--연산자 활용 예시

배열을 거꾸로 순회해보자

#import <Foundation/Foundation.h>

const int ARRSZ = 3;

int main(void) {
    
    int var[ARRSZ] = {100,200,300};
    int i, *ptr;
    
    ptr = &var[ARRSZ-1];
    
    for(i = ARRSZ-1; i >= 0; i--) {
        NSLog(@"address of var[%d] = %p",i,ptr);
        NSLog(@"value of var[%d] = %d", i,*ptr);
        ptr--;
    }
    
    return 0;
}

 

포인터 비교

포인터는 16진수 정수(unsigned)이므로, 크기 비교도 가능하다.

ptr이 첫번째 요소의 주소부터 시작해서 마지막 요소의 주소를 가리킬 때까지 반복

#import <Foundation/Foundation.h>

const int ARRSZ = 3;

int main(void) {
    int var[ARRSZ] = {100, 200, 300};
    int i, *ptr;
    
    ptr = var;
    i = 0;
    
    while(ptr <= &var[ARRSZ-1]) {
        NSLog(@"address of var[%d] = %p",i,ptr);
        NSLog(@"value of var[%d] = %d", i, *ptr);
        
        i++;
        ptr++;
    }
    
    return 0;
}

 

포인터 배열

말 그대로 포인터가 담겨 있는 배열이다.  (배열 포인터와 헷갈리지 말자)

#import <Foundation/Foundation.h>
const int ARRSZ = 3;

int main(void) {
    int arr[ARRSZ] = {100,200,300};
    //포인터 배열
    int *ptrArr[ARRSZ];
    int i;
    
    for(i=0; i<ARRSZ; i++) {
        ptrArr[i] = &arr[i];
    }
    
    for(i=0; i<ARRSZ;i++) {
        NSLog(@"value of arr[%d] = %d",i,*ptrArr[i]);
    }
    return 0;
}

문자열 배열

"Swift"와 같은 문자열 리터럴은 컴파일 시점에 메모리의 '상수 영역'에 저장된다.

C계열에서 문자열의 끝은 \0 즉 null문자로, %s 형식 지정자를 사용하면

문자열의 시작 주소로부터 null문자를 만날때까지 해당하는 문자열을 출력할 수 있게 된다.

#import <Foundation/Foundation.h>

const int MAX = 3;

int main(void) {
    //문자열의 시작 주소가 담긴 포인터 배열
    char *names[MAX] = {"toughie", "coffee","choonsik"};
    int i;
    for(i = 0; i<MAX; i++) {
        NSLog(@"literal of names[%d] = %s", i, names[i]);
    }
}

다중 포인터

포인터 또한 다른 포인터를 가리킬 수 있다.

아래는 이중 포인터 예시이다. 주로 동적할당이나 비동기 처리에서 활용되는 경우가 많다.

#import <Foundation/Foundation.h>

int main(void) {
    int var;
    int *ptr;
    int **pptr;
    
    var = 777;
    ptr = &var;
    pptr = &ptr;
    NSLog(@"Value of var: %d",var);
    NSLog(@"Value of var at *ptr: %d",*ptr);
    NSLog(@"Value of var at **pptr: %d", **pptr);
    
    return 0;
}

파라미터로 포인터 전달

포인터를 함수의 파라미터로 전달하기 위해서는 함수의 파라미터를 포인터 타입으로 선언해주면 된다.

#import <Foundation/Foundation.h>

@interface MyClass : NSObject
//long 포인터 타입의 파라미터 선언
-(void) getSecons: (long *) par;
@end

@implementation MyClass
-(void) getSecons:(long *)par {
    *par = time(NULL);
    return;
}
@end

int main(void) {
    MyClass *myClass = [[MyClass alloc] init];
    long sec;
    [myClass getSecons:&sec];
    NSLog(@"seconds: %ld",sec);
    return 0;
}

포인터 반환

파라미터 전달과 유사하게 함수의 반환타입을 포인터로 선언해주면 된다.

하지만 주의할 것은 내부에서 생성된 포인터를 반환하면 블록 호출이 끝나면서 변수가 소멸하기 때문에(stack)

static으로 선언해 주어야 한다.(이 방식이 과연 좋은 방식인지는 잘 모르겠다. static 영역에 계속 남아있을테니..)

 

랜덤한 숫자가 담겨있는 배열의 시작주소를 ptr에 담아서, 포인터 연산을 통해 값에 접근하는 예시이다.

 

#import <Foundation/Foundation.h>

const int MAX = 5;

int * getRandom(void) {
    static int arr[MAX];
    int i;
    srand((unsigned)time(NULL));
    for(i=0;i<MAX;i++) {
        arr[i] = rand();
        NSLog(@"value of arr[%d] = %d",i,arr[i]);
    }
    return arr;
}

int main(void) {
    int *ptr;
    int i;
    
    ptr = getRandom();
    for(i=0; i<MAX;i++) {
        NSLog(@"value of *(ptr + %d) = %d",i, *(ptr+i));
    }
    return 0;
}

 


이렇게 포인터의 기초를 알아보았다. 

 

Java나 Swift같은 고급 언어에서는 메모리 안정성을 위해 포인터와 같은 메모리 접근 자체를 막아놓은 경우가 많지만,

여전히 성능 등이 중요한 개발건에서는 c와 같은 low-level 언어도 많이 사용되고 있다.

 

하지만 메모리 직접 조작을 못한다고 고급언어에서 포인터의 개념이 사라진 것은 아니고,

그저 추상화 되고 가려졌다고 보는것이 더 정확하겠다. 결국 내부적으로는 똑같이 포인터, 메모리로 동작하니 말이다.

'iOS Developer > Objective-C' 카테고리의 다른 글

13. Objective-C Structure 구조체  (0) 2024.05.16
12. Objective-C Strings 문자열  (0) 2024.05.13
10. Objective-C Arrays 배열  (0) 2024.05.07
9. Objective-C NSNumbers  (0) 2024.05.07
8. Objective-C Blocks 블록  (0) 2024.05.07