지금까지 변수, 배열, 함수를 배우면서 C언어의 기본기를 많이 다졌습니다. 이번 편에서는 많은 사람들이 어려워하지만, 제대로 이해하고 나면 C언어의 진짜 재미를 느끼게 해주는 주제인 “포인터”를 다뤄보겠습니다. 포인터는 겁먹을 필요 없이, 딱 한 가지 개념만 확실히 잡으면 됩니다. 바로 “변수는 메모리 어딘가에 저장되어 있고, 포인터는 그 저장 위치(주소)를 담는 변수”라는 것입니다.

메모리 주소란 무엇인가

우리가 int number = 10; 이라고 변수를 선언하면, 컴퓨터는 메모리(RAM) 어딘가에 공간을 마련해서 10이라는 값을 저장합니다. 이때 그 공간에는 각각 고유한 주소가 있습니다. 마치 아파트의 각 호수마다 주소가 있는 것처럼, 메모리의 한 칸 한 칸에도 주소가 붙어 있는 것이죠.

C언어에서는 & 기호를 변수 앞에 붙이면 그 변수가 저장된 주소를 확인할 수 있습니다.

#include <stdio.h>

int main(void) {
    int number = 10;
    printf("number의 값: %d\n", number);
    printf("number의 주소: %p\n", (void *)&number);
    return 0;
}

여기서 %p는 주소값을 출력할 때 쓰는 서식 지정자입니다. 실행할 때마다 실제 주소값은 다르게 나올 수 있지만, “변수마다 고유한 주소가 있다”는 사실은 변하지 않습니다.

포인터 변수 선언하고 사용하기

포인터는 “값” 대신 “주소”를 저장하는 변수입니다. 포인터를 선언할 때는 자료형 뒤에 *을 붙여줍니다.

int number = 10;
int *ptr = &number; // ptr에는 number의 주소가 저장됨

여기서 int *ptr은 “ptr은 int형 변수의 주소를 가리키는 포인터”라는 뜻입니다. 포인터가 가리키는 주소에 저장된 실제 값을 확인하고 싶다면, 포인터 앞에 다시 *을 붙이면 됩니다. 이를 “역참조(dereference)”라고 부릅니다.

#include <stdio.h>

int main(void) {
    int number = 10;
    int *ptr = &number;

    printf("number의 값: %d\n", number);
    printf("ptr이 가리키는 주소: %p\n", (void *)ptr);
    printf("ptr을 역참조한 값: %d\n", *ptr);

    *ptr = 20; // 역참조로 number의 값을 직접 변경
    printf("변경 후 number의 값: %d\n", number);

    return 0;
}

여기서 헷갈리기 쉬운 부분이 있는데, int *ptr 선언에서의 *은 “이 변수는 포인터입니다”라는 표시이고, *ptr = 20;에서의 *은 “ptr이 가리키는 곳의 값을 다룹니다”라는 역참조 동작입니다. 같은 기호지만 상황에 따라 의미가 다르다는 점을 기억해두면 좋습니다.

코드 동작 설명 및 예상 실행 결과

  • int *ptr = &number;가 실행되면, ptr이라는 포인터 변수에는 number가 저장된 메모리 주소가 담깁니다. ptr 자체는 정수를 저장하는 것이 아니라 “주소”를 저장한다는 점이 핵심입니다.
  • *ptr을 출력하면 ptr이 가리키는 주소에 들어있는 값, 즉 number의 값인 10이 그대로 나옵니다.
  • *ptr = 20;은 ptr이 가리키는 주소에 직접 20을 써넣으라는 뜻입니다. 이 주소는 number의 주소와 같기 때문에, number를 직접 건드리지 않았는데도 number의 값이 20으로 바뀝니다.
  • 이 코드를 gcc pointer_basic.c -o pointer_basic으로 컴파일하고 실행하면 다음과 비슷한 결과를 볼 수 있습니다. (주소값은 실행할 때마다 달라집니다.)
number의 값: 10
ptr이 가리키는 주소: 0x7ffee2a1c9ac
ptr을 역참조한 값: 10
변경 후 number의 값: 20

포인터가 왜 유용한가

“그냥 변수를 직접 쓰면 되지, 왜 굳이 주소를 거쳐서 접근할까?”라는 의문이 들 수 있습니다. 포인터가 진짜 힘을 발휘하는 순간은 함수에 변수를 넘길 때입니다. C언어에서 함수에 인자를 넘기면 기본적으로 “값”만 복사되어 전달되기 때문에, 함수 안에서 그 값을 바꿔도 원래 변수는 영향을 받지 않습니다. 하지만 주소를 넘기면 함수 안에서도 원본 변수를 직접 조작할 수 있습니다.

#include <stdio.h>

void addTen(int *p) {
    *p = *p + 10; // 포인터를 통해 원본 값을 직접 수정
}

int main(void) {
    int score = 70;
    addTen(&score);
    printf("score = %d\n", score);
    return 0;
}

이 코드를 실행하면 score = 80이 출력됩니다. addTen 함수는 score의 주소를 받아서, 그 주소에 저장된 값을 직접 10만큼 늘려주었기 때문입니다. 만약 포인터 없이 int p처럼 값만 받았다면, addTen 함수 안에서 아무리 값을 바꿔도 main의 score는 전혀 영향을 받지 않았을 것입니다.

핵심 요약

  • 모든 변수는 메모리 상의 고유한 주소를 가지며, & 연산자로 그 주소를 확인할 수 있습니다.
  • 포인터는 값이 아니라 “주소”를 저장하는 변수이며, 자료형 *변수명 형태로 선언합니다.
  • 포인터 앞에 *을 붙이는 역참조를 이용하면 포인터가 가리키는 주소의 실제 값을 읽거나 바꿀 수 있습니다.
  • 함수에 변수의 주소를 넘기면, 함수 안에서도 원본 변수의 값을 직접 변경할 수 있어 매우 유용합니다.

다음 편에서는 오늘 배운 포인터 개념을 확장해서, “포인터와 배열/함수 관계”를 주제로 배열이 포인터와 어떻게 연결되는지, 그리고 함수와 함께 쓰일 때 어떤 점을 주의해야 하는지 살펴보겠습니다.