7편에서 배열과 문자열로 연속된 값을 다뤘다면, 이번 글에서는 그 값들이 실제로 메모리에 어디에 저장돼 있는지 살피는 포인터의 세계로 들어갑니다. 포인터는 "주소를 값처럼 다루는 변수"입니다. 어렵게 느껴지지만, 주소를 찍어 보고 값을 따라가다 보면 차근차근 감을 잡을 수 있습니다.
이번 글에서 새로 나오는 용어
- 포인터 (Pointer): 다른 변수의 메모리 주소를 저장하는 변수
- 주소 연산자 (Address-of Operator): 변수 앞에 붙이는
&, 해당 변수의 주소를 구합니다. - 역참조 (Dereference): 포인터 앞에
*를 붙여 그 주소에 저장된 실제 값을 읽는 동작 - 널 포인터 (Null Pointer): 어떤 주소도 가리키지 않는 포인터, 보통
NULL또는0으로 표현 - 포인터 타입 (Pointer Type): 포인터가 어떤 자료형의 주소를 보관하는지 나타내는 정보 (
int *,char *등)
핵심 개념
학습 메모
- 소요 시간: 70분 내외
- 준비물: 배열 길이 계산과 함수 호출을 편하게 할 수 있는 상태
- 학습 목표: 주소 출력, 역참조, 포인터 매개변수 사용 패턴 이해하기
포인터를 이해할 때는 "주소"와 "값"을 명확히 구분해야 합니다. int a = 10;이면 a는 값 10을 저장하고, &a는 10이 저장된 메모리 주소를 나타냅니다. 포인터 변수는 바로 그 주소를 저장합니다.
처음에는 아래 세 줄을 함께 비교해서 읽으면 훨씬 덜 헷갈립니다.
int *ptr;: 정수 주소를 저장할 포인터 변수 선언ptr = &number;:number의 주소를 포인터에 저장*ptr: 포인터가 가리키는 실제 값 읽기 또는 쓰기
코드로 따라하기
변수 주소 출력하기
#include <stdio.h>
int main(void) {
int score = 90;
printf("score 값 = %d\n", score);
printf("score 주소 = %p\n", (void *)&score);
return 0;
}
%p는 주소를 16진수로 출력합니다. &score 앞에 (void *)를 붙인 이유는 %p가 void *를 기대하기 때문입니다.
포인터 선언과 역참조
#include <stdio.h>
int main(void) {
int number = 42;
int *ptr = &number;
printf("ptr가 가리키는 값 = %d\n", *ptr);
*ptr = 100;
printf("number = %d\n", number);
return 0;
}
int *ptr는 "정수형 주소를 가리키는 포인터"라는 뜻입니다. *ptr을 사용하면 포인터가 가리키는 위치의 값을 읽거나 쓸 수 있습니다. 포인터를 통해 값을 바꾸면 원래 변수도 함께 바뀝니다.
포인터와 함수 인자
#include <stdio.h>
void set_to_zero(int *value) {
if (value != NULL) {
*value = 0;
}
}
int main(void) {
int counter = 5;
set_to_zero(&counter);
printf("counter = %d\n", counter);
return 0;
}
set_to_zero는 주소를 받아 직접 값을 바꿉니다. 포인터를 사용하면 함수가 외부 변수를 수정할 수 있지만, 동시에 잘못된 주소를 건드릴 위험도 있으므로 널 포인터인지 먼저 확인하는 습관을 들입니다.
널 포인터 활용하기
#include <stdio.h>
int main(void) {
int *ptr = NULL;
if (ptr == NULL) {
printf("아직 유효한 주소를 가리키지 않습니다.\n");
}
return 0;
}
포인터를 선언하고 나서 바로 주소를 넣지 못할 경우에는 NULL로 초기화해 두면, 나중에 사용하려 할 때 실수로 쓰지 않았는지 쉽게 판단할 수 있습니다. NULL은 "유효한 메모리 위치"가 아니라 "아직 아무것도 가리키지 않음"을 뜻하므로, 역참조하면 안 됩니다.
배열과 포인터의 첫 만남
#include <stdio.h>
int main(void) {
int scores[3] = {10, 20, 30};
int *p = scores; // &scores[0]와 동일
printf("첫 번째 값 = %d\n", *p);
printf("두 번째 값 = %d\n", *(p + 1));
return 0;
}
배열 이름은 대부분의 표현식에서 첫 요소의 주소로 바뀝니다. 아직 포인터 산술을 깊이 다루지는 않지만, *(p + 1)이 scores[1]와 같은 값을 가져온다는 사실만 기억해 둡니다.
실전 예시: 사용자 입력 검증
#include <stdio.h>
int read_positive(int *out_value) {
int temp;
if (scanf("%d", &temp) != 1) {
return 0;
}
if (temp <= 0) {
return 0;
}
*out_value = temp;
return 1;
}
int main(void) {
int number;
printf("양수를 입력하세요: ");
if (read_positive(&number)) {
printf("입력한 값: %d\n", number);
} else {
printf("양수만 허용됩니다.\n");
}
return 0;
}
함수에서 결과를 여러 개 돌려줘야 할 때 포인터 매개변수를 사용하면 편리합니다. read_positive는 성공 여부를 반환값으로 알려 주고, 실제 숫자는 포인터가 가리키는 변수에 저장합니다.
왜 중요한가
- 포인터는 C에서 배열, 함수 인자, 동적 메모리, 구조체를 다루는 핵심 추상화입니다.
- 주소를 출력하고 역참조하는 패턴을 익혀야 메모리 관련 버그를 읽을 수 있습니다.
- 널 포인터 체크 습관은 프로그램이 예상치 못한 주소를 건드리는 사고를 막습니다.
- 이후 글에서 배울 배열·포인터 관계, 동적 메모리, 구조체 등은 모두 포인터 감각 위에서 작동합니다.
CodeSandbox로 이어서 실습하기
아래 샌드박스는 CodeSandbox의 Universal starter입니다. C는 터미널에서 직접 컴파일하고 다시 실행하는 흐름이 중요하니, 이번 글의 코드를 파일로 만들고 빌드-실행 사이클을 다시 따라가 보세요.
💬 댓글
이 글에 대한 의견을 남겨주세요