C 포인터 마스터: 기초부터 포인터 연산, 배열/함수 포인터, 동적 할당까지
포인터는 C의 핵심이자 가장 강력한 도구입니다. 메모리 주소를 직접 다루며 동적 할당, 배열 조작, 함수 콜백, 자료구조 구현의 기반이 됩니다. 기초부터 실전 패턴까지 체계적으로 정리합니다.
포인터 기초
메모리 주소와 포인터
모든 변수는 메모리 주소 가짐
&var: 변수 주소 구하기 (주소 연산자)
포인터 변수: 주소를 값으로 저장하는 변수
int var = 42;
int *ptr = &var; // ptr은 var의 주소 저장
printf("var 값: %d\n", var); // 42
printf("var 주소: %p\n", (void*)&var); // 0x7ffc...
printf("ptr 값(주소): %p\n", (void*)ptr); // 0x7ffc... (같음)
printf("ptr이 가리키는 값: %d\n", *ptr); // 42 (역참조)
포인터 선언 문법
int *p1, *p2; // 둘 다 int* (각 변수마다 * 붙이기 권장)
int *p3, p4; // p3만 포인터, p4는 int - 혼동 주의!
// 읽는 법: 오른쪽에서 왼쪽으로
int *p; // p는 int를 가리키는 포인터
int **pp; // pp는 int*를 가리키는 포인터 (이중 포인터)
int *arr[10]; // int* 배열 (포인터 배열)
int (*parr)[10]; // int[10] 배열 포인터 (배열 포인터)
int *func(); // int* 반환 함수
int (*fp)(int); // int(int) 함수 포인터
포인터 연산 (Pointer Arithmetic)
포인터 + 정수: ptr + n → n * sizeof(*ptr) 바이트 이동
포인터 - 정수: ptr - n
포인터 - 포인터: ptr2 - ptr1 → 요소 개수 차이 (ptrdiff_t)
비교: ==, !=, <, > 등 (같은 배열 내 요소 간만 정의됨)
증감: ++ptr, ptr++, --ptr, ptr--
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;
printf("%d\n", *p); // 10
printf("%d\n", *(p+2)); // 30
p++; // 다음 요소로
printf("%d\n", *p); // 20
p += 2;
printf("%d\n", *p); // 40
ptrdiff_t diff = &arr[4] - &arr[1]; // 3 (요소 3개 차이)
NULL 포인터와 검사
NULL (<stddef.h>, <stdio.h> 등): 0 값 포인터 상수
초기화: int *p = NULL;
검사: if (p) (NULL 아님), if (!p) (NULL임)
역참조 전 반드시 검사!
동적 메모리 할당 (<stdlib.h>)
| 함수 | 용도 | 반환 | 해제 |
malloc(size) | 미초기화 메모리 할당 | void* (성공) / NULL (실패) | free() |
calloc(n, size) | 0 초기화된 배열 할당 | void* / NULL | free() |
realloc(ptr, size) | 크기 조정 (확장/축소) | 새 주소 / NULL (실패 시 원본 유지) | free() |
free(ptr) | 메모리 해제 | void | - |
aligned_alloc(align, size) (C17) | 정렬된 메모리 할당 | void* / NULL | free() |
안전한 할당 패턴
int *arr = malloc(100 * sizeof(int));
if (!arr) {
fprintf(stderr, "메모리 할당 실패\n");
exit(EXIT_FAILURE);
}
// 사용...
free(arr);
arr = NULL; // 댕글링 포인터 방지
흔한 버그
이중 해제: free(ptr); free(ptr); → UB
댕글링 포인터: 해제 후 포인터 사용
메모리 누수: 해제 안 함
버퍼 오버플로: 할당 크기 초과 쓰기
realloc 실패 시 원본 포인터 손실 방지: tmp = realloc(p, sz); if (tmp) p = tmp; else free(p);
배열과 포인터 심화
포인터 배열 (Pointer Array)
char *names[] = {"Alice", "Bob", "Charlie"}; // 문자열 리터럴 배열
// names[i]는 char* (각 문자열 주소)
int *ptrs[10]; // int 포인터 10개 배열
배열 포인터 (Array Pointer)
int matrix[3][4];
int (*row)[4] = matrix; // int[4] 타입 포인터
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
printf("%d ", row[i][j]); // row[i] ≡ *(row+i) ≡ matrix[i]
}
}
함수 포인터
// 선언
int (*compare)(const void*, const void*);
// 대입
compare = &strcmp; // & 생략 가능
compare = strcmp;
// 호출
int result = compare("a", "b"); // 또는 (*compare)("a", "b")
// typedef로 간소화
typedef int (*CmpFunc)(const void*, const void*);
CmpFunc cmp = strcmp;
실전: qsort 콜백
int cmp_int(const void *a, const void *b) {
int ia = *(const int*)a;
int ib = *(const int*)b;
return (ia > ib) - (ia < ib); // 오버플로 안전 비교
}
int arr[] = {5, 2, 8, 1, 9};
qsort(arr, 5, sizeof(int), cmp_int); // 정렬됨
포인터와 const
| 선언 | 읽기 | 쓰기 | 포인터 변경 | 이름 |
int *p | 가능 | 가능 | 가능 | - |
const int *p / int const *p | 가능 | 불가 | 가능 | 상수 포인터 (Pointer to const) |
int *const p | 가능 | 가능 | 불가 | 포인터 상수 (Const pointer) |
const int *const p | 가능 | 불가 | 불가 | 상수 포인터 상수 |
void 포인터 (Generic Pointer)
타입 없는 주소, 어떤 타입 포인터든 암시적 변환 가능
역참조 불가 (타입 모름), 캐스팅 후 사용
malloc, memcpy, qsort 등 제네릭 함수에 사용
void *raw = malloc(100);
int *arr = raw; // 암시적 변환 OK (C에서만, C++은 캐스팅 필요)
free(raw);
제약/제한 포인터 (C99 restrict)
void copy(int *restrict dst, const int *restrict src, size_t n) {
for (size_t i = 0; i < n; i++) dst[i] = src[i];
// dst와 src 영역 겹치지 않음 보장 → 적극적 최적화 가능
}
모범 사례
포인터 선언 시 *를 변수 이름 옆으로 (int *p)
할당 후 NULL 체크, 해제 후 NULL 대입
함수 매개변수에 const 적극 활용
배열 크기 함께 전달 (ptr, size 쌍)
복잡한 포인터 타입은 typedef로 별칭 정의
정적 분석 도구(Clang Static Analyzer, Coverity) 활용
C 언어
-
C 변수, 상수, 리터럴: 완전 가이드
-
C 저장 클래스 이해: 범위, 생명주기, 성능
-
C 포인터 마스터하기: 실전 중심 전문가 가이드
-
C 언어에서 배열과 포인터의 관계 이해하기
-
C 프로그래밍: 주소와 포인터를 함수에 전달하기 – 실전 가이드
-
C 구조체와 포인터 마스터하기: 실전 가이드
-
파이썬 global 키워드 마스터하기: 사용법과 타이밍
-
C++ 포인터 완전 정복: 개념, 예제, 실전 활용
-
C 변수, 데이터 타입, 상수 – 실전 가이드
-
C 포인터 완벽 가이드: 타입, 사용법, 모범 사례