C언어 포인터 & malloc 완벽 가이드 — 포인터, 주소, 스레드, 메모리 관리
요약: 포인터(
*), 주소(&), 이중 포인터(**), 함수 포인터,malloc/free의 핵심과 시험장에서 자주 실수하는 포인트들을 한 장으로 정리한 치트시트입니다. ✅
🏠 비유로 이해하기 — 집(House)과 주소(Address) 🔑
- 변수 (
int a = 10;): ‘a’라는 이름의 집이 있고, 그 안에 10이라는 사람이 살고 있습니다. &(주소 연산자): 그 집의 도로명 주소입니다. (예:0x1234abcd)*(포인터): 그 주소가 적힌 쪽지입니다. 혹은 그 주소를 보고 찾아가는 행동(역참조)입니다.
1) & (주소 연산자) : “주소가 어디야?”
변수 앞에 &를 붙이면, 그 변수가 메모리 상에서 어디에 저장되어 있는지 알려줍니다.
1
2
int number = 50;
// &number -> number가 저장된 메모리 번지수 (예: 0x1234abcd)
실전 팁: pthread_create(&threads[i], ...) 같은 곳에서 주소를 넘기는 이유는 함수가 “여기에 값을 채워주세요”라고 하기 때문입니다.
2) * (아스테리스크) : 두 얼굴
*는 선언과 사용 위치에 따라 의미가 달라집니다.
선언 시: “나는 주소를 담는 그릇이야”
1
int *ptr; // ptr은 '정수의 주소'를 저장하는 포인터 변수
사용 시(역참조): “그 주소로 찾아가!”
1
*ptr = 100; // ptr이 가리키는 주소로 찾아가서 100을 넣음
구조체 포인터 접근은 ->로 간단히 쓸 수 있습니다. 예: order->plasma_swords ((*order).plasma_swords의 축약형)
3) 함수 포인터 해석법 — “안에서 밖으로” 읽기
예: void *(*start_routine)(void *)
(*start_routine):start_routine은 포인터다.(void *): 이 포인터는void *하나를 인자로 받는 함수의 주소를 담고 있다.- 왼쪽의
void *: 그 함수는void *타입을 반환한다.
한 문장 요약: “start_routine은 void *을 받아 void *을 반환하는 함수의 주소를 담는 변수다.” (pthread에서 흔히 사용)
4) 이중 포인터(**) — 지도 안의 지도
- 비유: 보물 상자(
a) ← 지도(p) ← 금고(pp) - 예:
1 2 3 4
int a = 10; int *p = &a; int **pp = &p; printf("%d", **pp); // 10
pthread_join처럼 함수 내부에서 호출자 쪽의 포인터 값을 바꿔야 할 때 **를 사용합니다. 예를 들어 int pthread_join(pthread_t thread, void **retval)에서 retval은 스레드가 반환한 포인터를 호출자 쪽 변수에 저장하기 위해 “포인터의 주소”를 넘기는 용도로 사용됩니다.
5) 화살표 연산자(->)
p->member 는 (*p).member의 축약형입니다. 포인터로 구조체 멤버에 접근할 때 편리합니다.
6) 포인터 덧셈과 타입
- 포인터 연산은 “한 걸음”이 자료형 크기만큼 이동합니다.
- 예:
int *p; p = p + 1;→ 주소가sizeof(int)만큼 증가합니다 (보통 4바이트).
7) void * (보이드 포인터)와 캐스팅
void *은 내용물 타입이 알려져 있지 않은 만능 상자입니다. 역참조(*)나 포인터 산술(+1)은 불가능합니다.- 사용 전에 반드시 캐스팅하세요:
1
struct order *my_order = (struct order *)arg;
8) const의 위치 읽는 법
const int *p:*p(내용물)이 const — 내용물 변경 금지 (*p = 3금지;p = &b가능)int * const p:p(포인터 자체)가 const — 주소 변경 금지 (p = &b금지;*p = 3가능)
읽을 때는 *를 기준으로 왼쪽/오른쪽 의미를 확인하세요.
9) volatile 간단 정리
하드웨어 레지스터나 외부에서 값이 바뀔 수 있는 변수에 사용합니다. 컴파일러 최적화를 막고 매번 메모리에서 읽도록 강제합니다.
1
volatile int *status_reg = (volatile int *)0xFFFF0000;
📘 malloc 완벽 가이드 (힙 메모리 관리)
malloc이란?
- 힙에서 메모리를 동적으로 할당합니다.
- 반환형:
void *(성공 시 주소, 실패 시NULL) - 헤더:
<stdlib.h>
사용 패턴
1
2
3
4
5
int *arr = malloc(sizeof(int) * 10); // C에서는 (int *) 캐스팅 불필요
if (arr == NULL) { /* 오류 처리 */ }
// 사용
free(arr);
arr = NULL; // 댕글링 포인터 방지
스레드에서의 malloc / free (실전 팁)
스레드별로 독립적인 힙 메모리를 할당해야 데이터가 덮어써지지 않습니다. 각 스레드가 할당한 메모리는 그 스레드(또는 할당자를 명확히 정한 쪽)가 책임지고 해제해야 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 스레드 함수 예시
void *process_order(void *arg) {
struct order *o = arg; // 적절히 캐스팅되어 넘어온다고 가정
// 처리
free(o); // 처리 직후 반납
return NULL;
}
// 스레드 생성 예시
pthread_t tid;
struct order *o = malloc(sizeof *o);
// ... 초기화 ...
pthread_create(&tid, NULL, process_order, o); // &tid: tid의 주소, o: 인자
// 스레드 결과(옵션): pthread_join와 void **retval
void *retval;
pthread_join(tid, &retval);
// 만약 스레드가 malloc한 주소를 반환했다면
struct order *res = retval; // 캐스팅하여 사용
malloc 캐스팅 팁
- C에서는
malloc의 반환을 명시적으로 캐스팅할 필요가 없습니다.(int *)malloc(...)는 생략해도 됩니다. - 주의: 헤더
<stdlib.h>를 포함하지 않으면 컴파일러가 경고 또는 오류를 낼 수 있으니 항상 포함하세요. - C++에서는 반드시 캐스팅이 필요합니다.
권장 스타일 (Best Practices)
malloc(sizeof *p)형태로 쓰면 타입 수정 시 실수를 줄일 수 있습니다. 예:int *arr = malloc(sizeof *arr * n);- 항상
NULL체크를 하고, 할당 실패 시 적절히 처리하세요. - 할당한 메모리는 더 이상 필요해지면 즉시
free()하세요. 필요시ptr = NULL;로 댕글링 포인터를 방지합니다. - 가능한 한 초기화가 필요한 경우
calloc을 고려하세요.
핵심 주의사항
malloc한 번에free한 번 — 메모리 누수 방지- 항상
sizeof를 사용하세요 (malloc(sizeof(int) * n)) NULL체크는 필수
calloc / realloc
calloc(n, size): 0으로 초기화된 메모리realloc(ptr, new_size): 기존 메모리 크기 조절
시험장에서 자주 하는 실수 체크리스트 ✅
- 포인터 초기화 없이 사용했다. (
int *p; *p = 10;) → 초기화 또는NULL확인 malloc크기 실수 (malloc(10)대신malloc(sizeof(int) * 10))- 문자열 복사는
strcpy또는 적절한 버퍼 사용 필요 pthread_create에 주소(&)를 넘겼는지 확인pthread_join시void **retval사용 맥락 이해
시험용 요약 치트시트
| 기호 | 의미 | 한마디 |
|---|---|---|
& | 주소 연산자 | “너 어디 살아?” |
int *p | 포인터 선언 | “주소 담을 그릇” |
*p | 역참조 | “그 주소로 찾아가” |
p->a | 구조체 포인터 멤버 접근 | “문 열어봐” |
void * | 만능 포인터 | “내용물 모름(캐스팅 필요)” |