Algorithm/Study

포인터

Gyuri 2022. 9. 9. 22:47

포인터

포인터 (pointer)는 다른 변수 주소를 갖고 있는 변수이다.

모든 변수는 메모리 공간에 저장되고, 메모리의 각 바이트엔 주소가 매겨져 있다. 이 주소가 포인터에 저장된다.

 

주소는 컴퓨터에 따라 다를 수 있으므로, 포인터 변수는 대개 정확한 숫자보단 화살표로 그려진다.

포인터는 변수를 가리킨다.

위의 그림에서

p는 a란 변수를 가리키는 포인터 변수이다! 

 

해당 그림에 대한 c언어 문장은 아래와 같다.

int a = 100;
int *p;
p = &a; // 변수 a의 주소 저장

int 형의 변수  a가 정의되고, p는 int형을 가리키는 포인터로 정의된다.

p가 a를 가리키게 하려면, a의 주소를 p에 대입한다.

변수의 주소는 &연산자를 변수에 적용시켜 추출할 수 있다.

 

포인터와 관련된 연산자

 & 연산자 : 주소 연산자

 * 연산자 : 간접 참조 연산자 (역참조 연산자라고도 함)

 

& 연산자는 변수의 주소를 추출하는 연산자로,

앞에서 선언한 포인터 p가 특정한 변수를 가리키게 하려면 변수의 주소를 &연산자로 추출해 p에 대입한다.

int a; // 정수형 변수
p = &a; // 변수의 주소를 포인터에 저장

 

* 연산자는 포인터가 가리키는 장소에 값을 저장하는 연산자로,

예를 들어 p가 가리키는 장소에 200을 저장하려면 아래와 같이 사용한다.

*p = 200;

이들 예제 문장에선 *p와 a가 동일한 메모리 위치를 참조함을 유의할 것!

즉, *p와 a는 전적으로 동일하다. 

값만 같은 것이 아니고, 동일한 실제적 객체를 가리키기 때문에 *p의 값을 변경하면 a의 값도 바뀌게 된다.

 

널 포인터

널 포인터는 어떤 객체도 가리키지 않는 포인터이다.

일반적으로, C언어에서 널 포인터는 NULL이란 매크로로 표시한다.

포인터를 사용하기 전엔, 반드시 널 포인터인지 검사해야 한다.

if(p==NULL) {
	fprintf(stderr, "오류 : 포인터가 아무 것도 가리키지 않음.");
    return;
}

포인터가 아무것도 가리키지 않을 땐 항상 널 포인터 상태로 만들어 두는 것이 좋다.

널 포인터를 갖고 간접참조(*) 하려면 컴퓨터 시스템에서 오류가 발생돼 쉽게 알 수 있기 때문이다.

잘못된 포인터를 갖고 메모리를 변경하는 건 치명적인 결과를 가져올 수 있다.

 

* stderr : 자신의 오류 메세지를(기본값은 터미널)로 파이프 하는데 사용되는 출력 스트림.

    => 버퍼 없이 바로 출력됨. 어떤 상황에도 빠르게 에러 메세지 출력이 가능함!

 

함수의 매개변수로 포인터 사용

포인터는 함수의 매개변수로 전달될 수 있다!

특정 변수를 가리키는 포인터가 함수의 매개변수로 전달되면, 그 포인터를 이용해 함수 안에서 외부 변수의 값을 변경할 수 있다.

 

예로, 외부 변수 2개의 값을 서로 바꾸는 swap() 함수를 포인터를 이용해 작성한 코드이다.

#include <stdio.h>

void swap(int* px, int* py) {
    int tmp;
    tmp = *px;
    *px = *py;
    *py = tmp;
}

int main(void) {
    int a = 1;
    int b = 2;
    
    printf("swap 호출 전 : a=%d, b=%d\n", a, b);
    swap(&a, &b);
    printf("swap 호출 전 : a=%d, b=%d\n", a, b);

    return 0;
}

 

배열과 포인터

함수로 배열이 전달되면, 함수 안에서 배열의 내용을 변경할 수 있다.

그 이유는, 아래와 같이 배열의 이름이 배열의 시작위치를 가리키는 포인터이기 때문이다!

배열의 이름은 배열의 시작 부분을 가리키는 포인터이다.

컴파일러가 실제로 배열의 이름에 공간을 할당하진 않는다.

대신, 배열의 이름이 있는 곳을 배열의 첫 번째 요소의 주소로 대치한다.

따라서, 배열의 이름이 포인터이기 때문에 배열이 함수의 매개변수로 전달될 때 사실은 포인터가 전달되는 것이다.

 

이것은 메모리 공간과 함수 호출 시간을 절약하는 기법이기도 하다.

함수 호출 시에 배열을 복사할 필요가 없기 때문이다!

 

위의 내용을 아래 코드와 함께 보면,

get_integers()와  cal_sum() 함수는 모두 배열을 매개변수로 받는다.

배열의 경우엔, 원본이 전달되므로 함수 안에서 배열의 내용을 변경할 시, 원본 배열이 변경된다!

/* 배열을 함수의 매개변수로 사용 */
#include <stdio.h>
#define SIZE 6

void get_integers(int list[]) {
    printf("6개의 정수 입력 : ");
    for(int i=0; i<SIZE; i++) {
        scanf("%d", *list[[i]]);
    }
}

int cal_sum(int list[]) {
    int sum = 0;
    for(int i=0; i<SIZE; i++) {
        sum += *(list+i);
    }
    return sum;
}

int main(void) {
    int list[SIZE];
    
    get_integers(list);
    printf("합 = %d\n", cal_sum(list));
    
    return 0;
}

 


 

동적 메모리 할당

일반적인 배열은 크기가 고정돼있다.

이 고정된 크기 때문에 많은 문제가 발생한다. 프로그램을 작성할 당시엔 얼마나 많은 입력이 있을 지 알 수 없기 때문이다.

 

만약 처음 결정된 크기보다 더 큰 입력이 들어오면 처리하지 못할 것이고,

더 작은 입력이 들어온다면 남은 메모리 공간은 낭비될 것이다.

 

따라서,

이런 문제를 해결하기 위해 필요한 만큼의 메모리를 운영체제로부터 할당받아 사용하고,

사용기 끝나면 시스템에 메모리를 반납하는 기능이 있다.

이것을 "동적 메모리 할당 (dynamic memory allocation)" 이라고 한다.

 

동적 메모리가 할당되는 공간을 "힙(heap)" 라고 한다.

힙은 운영체제가 사용되지 않는 메모리 공간을 모아 놓은 곳이다.

필요한 만큼만 할당 받고 또 필요한 때에 사용하고 반납하기 때문에 메모리를 효율적으로 사용할 수 있다.

 

다음은 동적 메모리 할당 코드이다.

int *p;

p = (int *)malloc(sizeof(int)); // 1) 동적 메모리 할당
*p = 1000; // 2) 동적 메모리 사용
free(p); // 3) 동적 메모리 반납

1) malloc () 함수는 size바이트 만큼의 메모리 블록을 할당한다.

sizeof 키워드는 변수/타입의 크기를 숫자로 반환하며, 크기의 단위는 바이트이다.

sizeof(int)는 int형의 크기를 반환한다. malloc()은 동적 메모리 블럭의 시작 주소를 반환한다. 

반환되는 주소의 타입은 void *이므로, 이를 적절한 포인터로 형변환시켜야 한다.

메모리 확보가 불가능하면 NULL을 함수의 반환값으로 반환한다.

 

2) 동적 메모리는 포인터로만 사용할 수 있다.

*p는 p가 가리키는 장소이다. *p=1000; 문장을 실행하면 p가 가리키는 장소에 1000이 저장된다.

 

3) free() 함수는 할당된 메모리 블록을 운영체제에 반환한다.

주의할 점은 malloc()함수가 반환했던 포인터 값을 잊으면 안 된다. 포인터값을 잊으면 동적 메모리를 반환할 수 없다.

malloc() 은시스템의 메모리가 부족해서 요구된 메모리를 할당할 수 없으면 NULL을 반환한다.

따라서, malloc()의 반환값은 항상  NULL인지 검사해야 한다

 

 

< malloc 동적 메모리 할당 예 >

/* malloc 동적 메모리 할당 */
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>

#define SIZE 10

int main(void) {
    int *p;

    p = (int *)malloc(SIZE * sizeof(int));
    if(p==NULL) {
        fprintf(stderr, "메모리 부족으로 할당 불가\n");
        exit(1);
    }

    for(int i=0; i<SIZE; i++) {
        p[i] = i;
    }

    for(int i=0; i<SIZE; i++) {
        printf("%d ", p[i]);
    }

    free(p);
    return 0;
}

 

구조체와 포인터

구조체에 대한 포인터를 선언하고, 포인터를 통해 구조체 멤버에 접근할 수 있다.

포인터를 통해 구조체의 멤버에 접근하는 표기법은 "->" 이다.

 

ps가 구조체를 가리키는 포인터라고 할 때,

(*ps).i보다 ps->i 라고 쓰는 것이 하다. 

자료 구조에서 구조체에 대한 포인터도 자주 함수의 매개변수로 전달된다.

 

구조체 자체를 함수로 전달하는 경우, 구조체가 함수로 복사돼 전달되기 때문에,

큰 구조체의 경우엔 구조체 포인터를 전달하는 것이 좋다.

/* 동적 메모리 할당을 이용해 구조체 생성 */
/* malloc 동적 메모리 할당 */
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>

typedef struct studentTag {
    char name[10];
    int age;
    double gpa; 
} student;

int main(void) {
    student *s;

    s = (student *)malloc(sizzeof(student));
    if(s==NULL) {
        fprintf(stderr, "메모리 부족으로 할당 불가\n");
        exit(1);
    }

    strcpy(s->name, "Park");
    s->age = 20;

    free(s);
    return 0;
}

위의 코드처럼,

s는 구조체를 가리키는 포인터로 선언됐다.

(*s).name이라고 할 수도 있지만, s->name이 더 편리하다.