무엇을 하는 것인가
모든 변수는 메모리 어딘가에 있습니다. 그 자리의 번호가 주소입니다. &나이는 나이가 있는 자리의 번호를 꺼냅니다. 포인터는 그 번호를 담아 두는 변수일 뿐입니다. 어렵게 느껴지는 건 개념이 아니라 기호 두 개가 헷갈리기 때문입니다.
기호는 딱 둘입니다. &는 "주소를 내놔", *는 "그 주소로 찾아가"입니다. int *p = &나이;는 "나이의 주소를 p에 담아라", *p = 30;은 "p가 가리키는 자리에 30을 넣어라"입니다.
*가 두 가지 뜻으로 쓰이는 게 혼란의 원인입니다. 선언할 때 int *p;의 *는 "p는 포인터다"라는 표시이고, 그 뒤 *p의 *는 "찾아가라"는 동작입니다. 같은 기호지만 자리가 다릅니다.
포인터를 쓰는 이유는 셋입니다. 함수에서 원본을 고치려고(scanf가 &를 요구하는 이유), 큰 데이터를 복사하지 않고 넘기려고, 그리고 실행 중에 메모리를 잡아 쓰려고입니다.
가장 흔한 사고는 아무 데도 가리키지 않는 포인터를 따라가는 것입니다. 값을 넣지 않은 포인터는 쓰레기 주소를 담고 있고, 그걸 *로 따라가면 프로그램이 죽습니다. 포인터는 만들 때 NULL로 초기화하고, 쓰기 전에 NULL인지 확인하는 습관을 들이세요.
예제 코드
아래 코드가 위 버튼으로 받는 파일과 같은 내용입니다. 통째로 복사해 실행해 보고, 값을 바꿔 가며 결과가 어떻게 달라지는지 보는 편이 읽기만 하는 것보다 훨씬 빨리 익습니다.
/* 포인터 — & 는 주소를 꺼내고, * 는 그 주소로 찾아간다
컴파일: gcc c-pointer.c -o ptr -Wall
사고 추적: gcc c-pointer.c -o ptr -Wall -fsanitize=address */
#include <stdio.h>
void 바꿔주기(int *p, int 새값);
int main(void)
{
int 나이 = 20;
/* 선언의 * 는 "포인터다" 라는 표시입니다 */
int *p = &나이; /* 나이가 있는 자리의 번호를 담는다 */
printf("값: %d\n", 나이);
printf("포인터로: %d\n", *p); /* * 는 "찾아가라" */
printf("나이의 주소: %p\n", (void *)&나이);
printf("p 가 담은 값:%p\n", (void *)p); /* 위와 같은 주소 */
/* p 가 가리키는 자리를 고치면 원본이 바뀝니다 */
*p = 30;
printf("바꾼 뒤 나이: %d\n", 나이); /* 30 */
/* 함수에서 원본을 고치려면 주소를 넘깁니다 — scanf 가 & 를 요구하는 이유 */
바꿔주기(&나이, 45);
printf("함수가 바꾼 뒤: %d\n", 나이); /* 45 */
/* ── 배열 이름은 그 자체가 첫 원소의 주소입니다 ── */
int 값들[5] = {10, 20, 30, 40, 50};
int *q = 값들; /* &값들[0] 과 같습니다 */
printf("값들[2] = %d\n", 값들[2]);
printf("*(q + 2) = %d\n", *(q + 2)); /* 같은 값입니다 */
printf("q[2] = %d\n", q[2]); /* 포인터에도 [] 를 씁니다 */
/* 포인터를 옮겨 가며 훑기 */
for (int i = 0; i < 5; i++) {
printf("%d ", *(값들 + i));
}
printf("\n");
/* ── 포인터는 NULL 로 초기화하고 쓰기 전에 확인합니다 ── */
int *아직없음 = NULL;
if (아직없음 != NULL) {
printf("%d\n", *아직없음);
} else {
printf("아직 아무 데도 가리키지 않습니다.\n");
}
/* 이 줄의 주석을 풀면 프로그램이 죽습니다 (Segmentation fault)
printf("%d\n", *아직없음);
*/
/* const 를 붙이면 실수로 고치는 것을 컴파일러가 막아 줍니다 */
const int *읽기전용 = &나이;
printf("읽기는 됩니다: %d\n", *읽기전용);
/* *읽기전용 = 1; ← 이 줄은 컴파일 오류가 납니다 */
return 0;
}
void 바꿔주기(int *p, int 새값)
{
if (p == NULL) { /* 받은 주소부터 확인하는 습관 */
return;
}
*p = 새값;
}
기억할 것
&= 주소를 꺼낸다,*= 그 주소로 찾아간다.int *p = &a;그리고*p = 10;은a = 10;과 같습니다.- 선언의
*는 "포인터다"라는 표시, 사용할 때의*는 "찾아가라". - 포인터는
NULL로 초기화하고 쓰기 전에 확인하세요. - 배열 이름은 그 자체가 첫 원소의 주소입니다.
a[i]와*(a + i)는 같습니다.
자주 묻는 것
Segmentation fault 가 납니다
대개 셋 중 하나입니다 — 초기화 안 한 포인터를 따라갔거나, NULL을 따라갔거나, 이미 해제한 메모리를 따라갔습니다. 포인터를 NULL로 초기화하고 if (p != NULL)로 확인하는 것만으로 대부분 막힙니다. gcc -fsanitize=address가 어디서 났는지 짚어 줍니다.
int *p 와 int* p 는 다른가요
같습니다. 다만 int* a, b;라고 쓰면 b는 포인터가 아니라 그냥 int가 됩니다. *가 변수마다 따로 붙기 때문입니다. 그래서 int *a, *b;처럼 변수 쪽에 붙여 쓰는 편이 오해가 없습니다.
배열과 포인터는 같은 건가요
비슷하지만 다릅니다. 배열 이름은 첫 원소의 주소로 바뀌어 쓰이지만, 배열 자체는 크기를 아는 실체이고 포인터는 주소를 담는 변수입니다. 그래서 배열이 선언된 함수 안에서는 sizeof가 전체 크기를 주지만, 함수로 넘어가면 포인터가 되어 주소 크기만 나옵니다.
const int *p 와 int *const p 는요
const int *p는 "가리키는 값을 못 바꾼다", int *const p는 "가리키는 대상을 못 바꾼다"입니다. 함수가 남의 데이터를 읽기만 할 때 const int *로 받으면, 실수로 고치는 것을 컴파일러가 막아 줍니다.