포인터 기초
지금까지 변수, 배열, 함수를 배우면서 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는 전혀 영향을 받지 않았을 것입니다.
핵심 요약
- 모든 변수는 메모리 상의 고유한 주소를 가지며,
&연산자로 그 주소를 확인할 수 있습니다. - 포인터는 값이 아니라 “주소”를 저장하는 변수이며,
자료형 *변수명형태로 선언합니다. - 포인터 앞에
*을 붙이는 역참조를 이용하면 포인터가 가리키는 주소의 실제 값을 읽거나 바꿀 수 있습니다. - 함수에 변수의 주소를 넘기면, 함수 안에서도 원본 변수의 값을 직접 변경할 수 있어 매우 유용합니다.
다음 편에서는 오늘 배운 포인터 개념을 확장해서, “포인터와 배열/함수 관계”를 주제로 배열이 포인터와 어떻게 연결되는지, 그리고 함수와 함께 쓰일 때 어떤 점을 주의해야 하는지 살펴보겠습니다.