C 언어 포인터 완벽 정리 – 이 글 하나로 끝내기
프로그래밍/C언어파이썬만 쓰다가 다시 C 언어를 보면 제일 먼저 막히는 게 바로 포인터(pointer)다.
개념만 애매하게 잡고 넘어가면, 나중에 구조체·동적 메모리·파일 입출력·함수 포인터에서 계속 발목을 잡는다.
이 글에서는
- 포인터가 메모리에서 어떻게 동작하는지
*,&, 배열,malloc,NULL과의 관계- 자주 터지는 실수와 디버깅 포인트
까지 한 번에 정리해본다.

1. 포인터란 무엇인가?
포인터 = “값” 대신 “주소”를 저장하는 변수
일반 변수는 값(value) 을 저장하고,
포인터 변수는 값이 저장된 메모리 주소(address) 를 저장한다.
int x = 10; // 값 10을 저장하는 int 변수
int *p = &x; // x가 저장된 "주소"를 저장하는 포인터 변수
&x: 변수x의 주소p: 그 주소를 저장하는 포인터 변수*p: 포인터p가 가리키는 주소에 있는 값
즉,
x→ 값: 10&x→ 주소: 예) 0x1000p→ 주소: 0x1000*p→ 값: 10
2. 포인터 선언과 기본 문법
2-1. 기본 선언
int *p; // int 를 가리키는 포인터
char *cp; // char 를 가리키는 포인터
double *dp; // double 을 가리키는 포인터
*는 “포인터 타입이다” 라는 의미int *p, q;처럼 쓰면p만 포인터고,q는 그냥 int 이다 → 조심!
int* p, q; // 혼동의 시작: p는 int*, q는 int
int *p, *q; // 둘 다 포인터
2-2. 포인터에 주소 대입
int x = 42;
int *p = &x; // x의 주소를 p에 저장
- 항상 같은 타입끼리 맞춰야 한다.
int*포인터에는int변수의 주소만 넣는다.
3. &와 * 정확히 이해하기
3-1. & 연산자: 주소 연산자
int x = 10;
printf("%p\n", (void*)&x); // x가 저장된 주소 출력
&x: 변수x가 메모리에 있는 위치
3-2. * 연산자: 간접 참조(역참조)
int x = 10;
int *p = &x;
printf("%d\n", *p); // 10
*p = 20; // x의 값을 20으로 변경
printf("%d\n", x); // 20
p는 “주소”를 저장*p는 그 주소에 있는 “값”에 접근
✔ 포인터를 사용할 때 항상 “지금 이게 주소인지, 값인지”를 구분해서 생각하면 헷갈림이 많이 줄어든다.
4. 포인터와 배열의 관계
C에서 배열 이름은 상수 포인터 비슷하게 동작한다.
int arr[3] = {10, 20, 30};
printf("%p\n", (void*)arr); // arr[0]의 주소
printf("%p\n", (void*)&arr[0]); // 동일한 주소
4-1. 배열 → 포인터 변환
int *p = arr; // 사실은 &arr[0] 와 같은 의미
printf("%d\n", p[1]); // 20
printf("%d\n", *(p + 1)); // 20
arr[i]≒*(arr + i)p[i]≒*(p + i)
4-2. 하지만 배열과 포인터는 완전히 같지는 않다
int arr[3] = {1, 2, 3};
int *p = arr;
sizeof(arr); // 3 * sizeof(int) => 배열 전체 크기
sizeof(p); // 포인터 변수 크기 (보통 4 또는 8바이트)
- 배열: 메모리에 연속된 공간 + 크기가 컴파일 시에 결정
- 포인터: 그 공간을 가리키는 주소만 저장
5. 포인터 연산(pointer arithmetic)
포인터에 +1을 하면 주소가 1 증가하는 게 아니라
그 타입 크기만큼 이동한다.
int arr[3] = {10, 20, 30};
int *p = arr;
printf("%d\n", *p); // 10
printf("%d\n", *(p + 1)); // 20
printf("%d\n", *(p + 2)); // 30
- 만약
sizeof(int) == 4라면,p + 1은 실제 주소로+4만큼 이동한 것이다.
✔ 그래서 포인터 연산은 배열 순회에 매우 유용하다.
6. NULL 포인터와 초기화
포인터는 사용 전에 반드시 초기화해야 한다.
초기화되지 않은 포인터는 “쓰레기 주소”를 가리키며,
접근 시 바로 세그멘테이션 폴트(segmentation fault) 로 이어질 수 있다.
int *p = NULL; // 초기화
if (p == NULL) {
// 아직 유효한 주소가 없음
}
NULL은 “아무 것도 가리키지 않는다”는 의미의 특별한 주소 값- 동적 할당 실패 시에도 보통
NULL을 반환한다.
7. 동적 메모리와 포인터 (malloc, free)
힙(heap) 영역에 메모리를 할당할 때 포인터가 필수로 등장한다.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *p = malloc(sizeof(int) * 3); // int 3개 공간 할당
if (p == NULL) {
// 메모리 할당 실패
return 1;
}
p[0] = 10;
p[1] = 20;
p[2] = 30;
printf("%d %d %d\n", p[0], p[1], p[2]);
free(p); // 반드시 해제!
return 0;
}
malloc: 메모리를 할당하고 그 첫 번째 바이트의 주소를 반환free: 더 이상 사용하지 않을 때 반드시 해제해야 함malloc으로 받은 주소를 잃어버리면 → 메모리 누수(memory leak)
8. 이중 포인터(double pointer) 맛보기
이중 포인터는 “포인터를 가리키는 포인터”다.
int x = 10;
int *p = &x; // x를 가리키는 포인터
int **pp = &p; // p를 가리키는 포인터
printf("%d\n", **pp); // 10
언제 쓰냐?
- 포인터를 함수 인자로 넘겨서 그 포인터 자체를 변경하고 싶을 때
- 2차원 배열처럼 데이터를 관리할 때
- 동적 할당된 배열의 배열을 다룰 때
이건 글이 길어지니, 이중 포인터만 따로 글 하나를 파도 될 정도다.
9. 함수 인자와 포인터: call by value vs call by reference 흉내
C는 무조건 call by value지만, 포인터를 사용해서
마치 “참조 호출”처럼 동작하게 만들 수 있다.
#include <stdio.h>
void add_one(int *p) {
(*p)++;
}
int main(void) {
int x = 10;
add_one(&x); // x의 주소를 넘김
printf("%d\n", x); // 11
return 0;
}
- 함수는
p라는 포인터의 복사본을 받지만 - 그 포인터가 가리키는 원래 변수 x를 직접 수정할 수 있다.
파이썬의 “mutable 객체를 함수에서 수정하는 느낌”과 비슷하다고 보면 이해가 편하다.
10. 포인터에서 자주 하는 실수들
10-1. 초기화 안 된 포인터 사용
int *p; // 초기화 안 됨 (쓰레기 값)
*p = 10; // 세그폴트 가능성 100%
→ 반드시 NULL 또는 유효한 주소로 초기화.
10-2. 해제 후 포인터를 계속 사용 (use-after-free)
int *p = malloc(sizeof(int));
*p = 10;
free(p);
printf("%d\n", *p); // 정의되지 않은 동작(UB)
→ free(p); p = NULL; 패턴을 습관처럼 넣는 게 좋다.
10-3. 배열 범위 밖 접근
int arr[3] = {1, 2, 3};
int *p = arr;
printf("%d\n", p[3]); // 쓰지 말 것! 범위 밖 접근
→ C는 범위 체크를 안 해준다.
→ 디버깅 어려운 버그의 주요 원인.
10-4. 잘못된 캐스팅
int x = 10;
double *dp = (double*)&x; // 타입이 전혀 다른 포인터
이런 코드는 가능하면 쓰지 않는 게 좋다.
메모리 레이아웃을 완전히 이해하고 쓸 필요가 있을 때만, 조심해서 사용.
11. 포인터를 진짜 이해했는지 스스로 체크해보기
아래 질문들에 스스로 답해볼 수 있다면,
포인터는 “완벽까지는 아니어도 꽤 잘 이해한 상태”다.
int *p와int (*p)[3]의 차이를 설명할 수 있는가?arr,&arr[0],&arr의 차이를 말할 수 있는가?malloc으로 2차원 배열을 만드는 코드를 직접 짤 수 있는가?NULL포인터와 초기화되지 않은 포인터의 차이를 구분하는가?- 함수 인자로 포인터를 넘겨서, 원래 변수 값을 바꾸는 코드를 직접 짤 수 있는가?
이 글에서 다룬 것은 그 중에서도 가장 기초이자 필수적인 부분이다.
다음 글에서는 이 내용을 바탕으로
- 이중 포인터로 2차원 배열 다루기
- 함수 포인터와 콜백 구조
를 이어서 정리해볼 예정이다.
'프로그래밍 > C언어' 카테고리의 다른 글
| 📝 C 언어 함수 포인터와 콜백 함수: 유연한 코드 설계를 위한 핵심 패턴 (0) | 2025.11.21 |
|---|---|
| 이중 포인터로 2차원 배열 다루기 – 개념부터 실전 사용까지 (0) | 2025.11.20 |
| C언어 포인터, 동적할당, 메모리 관리 (0) | 2024.10.15 |
| C언어 구조체란? (3) | 2024.10.14 |
| 자료구조 연결리스트 개념, 장점, 단점, 배열과의 차이 (1) | 2023.06.04 |