동적할당
C 메모리 할당 요약 보고서
1. 개요
본 문서는 C 언어에서 사용되는 메모리 할당 방식(정적, 자동, 가변 길이 배열, 동적)과 저장 영역(스택, 힙)의 차이를 정리한다. 또한 malloc
/realloc
/free
사용 시 안전 패턴과 예제를 제시하고, 실무에서 자주 발생하는 오류와 점검 항목을 제공한다.
2. 핵심 요약
-
동적 할당의 핵심은 수명과 크기를 실행 중에 결정하고 직접 해제하는 점이다.(heap) 일반적으로 힙에서
malloc
/realloc
/free
로 관리한다. - 스택 배열(자동 저장)은 블록이 끝나면 소멸하며, 크기 변경을 지원하지 않는다.
int a[n];
은 VLA(가변 길이 배열)로 분류되며 스택에 위치한다. 블록을 벗어나면 소멸하고, 주소를 반환하면 안 된다.-
realloc
은 확장과 축소를 모두 지원한다. 재할당 후 주소가 변경될 수 있으므로 포인터 갱신이 필요하다. - 메모리를 부분적으로만 해제하는 기능은 없다. 일부만 반환하려면 처음부터 여러 블록으로 분할하는 설계가 필요하다.
- VLA는 컴파일러 지원이 제한될 수 있으므로 큰 버퍼나 장수명 데이터는 힙 사용을 권장한다.
3. 용어 및 분류
분류 | 예시 | 저장 영역 | 수명(생성/소멸) | 크기 결정 시점 | 크기 변경 | 해제 방식 |
---|---|---|---|---|---|---|
정적 저장기간 | static int g[10]; (전역/정적) |
데이터 영역 | 프로그램 시작~종료 | 컴파일 타임 | 불가 | 자동(프로그램 종료 시) |
자동(스택) 고정배열 | int a[10]; |
스택 | 블록 진입~탈출 | 컴파일 타임 | 불가 | 자동 |
자동(스택) VLA | int a[n]; |
스택 | 블록 진입~탈출 | 런타임(n) | 불가 | 자동 |
동적(힙) | int *p = malloc(n*sizeof *p); |
힙 | malloc ~free |
런타임 | 가능(realloc ) |
free(p) 필요 |
주의: “정적 할당”은 보통 정적 저장기간(전역/
static
)을 의미한다. “컴파일 시 크기가 고정된 배열”과 혼용되는 경우가 있으므로 문맥을 구분해 사용한다.
4. 자주 묻는 질문 요지
- 왜 배열인데도 동적이라고 부르나?
힙에서 런타임에 크기와 수명(해제 시점)을 개발자가 결정하기 때문이다. int a[n];
은 정적인가?
정적이 아니다. VLA이며 스택에 위치한다. 블록 종료 시 소멸하고 크기 변경은 불가하다.scanf
입력 후int a[n];
은 오류인가?
C99 VLA를 지원하는 컴파일러(GCC/Clang 등)에서는 가능하다. MSVC는 미지원이라 컴파일 오류가 발생할 수 있다. 값이 크면 스택 오버플로 위험이 있으므로 범위를 검증한다.- 동적 배열은 확장/축소 가능한가? 일부만 반환 가능한가?
realloc
으로 확장과 축소 모두 가능하다. 다만free
는 전체 블록만 해제한다.
5. 안전 사용 패턴
- 크기 계산 시 정수 오버플로를 우선 확인한다:
n > SIZE_MAX/sizeof *p
검증을 수행한다. malloc
/realloc
의 반환값을 항상 검사한다. 실패 시 적절히 정리하고 종료한다.realloc
수행 후 원 포인터는 더 이상 사용하지 않는다. 임시 포인터(tmp
)를 사용한 후 성공 시 교체한다.- 해제 후(또는 축소 후) 영역에 접근하지 않는다. 이는 정의되지 않은 동작(UB)을 유발한다.
- VLA는 지원 여부가 다르므로 이식성이 요구되면 힙 할당을 우선 고려한다.
6. 코드 예제
6.1 VLA 예제(지원 컴파일러에서만)
#include <stdio.h>
int main(void){
int n;
if (scanf("%d",&n)!=1 || n<=0 || n>1000000){ puts("bad n"); return 1; }
int a[n]; // VLA: 스택에 위치
for(int i=0;i<n;i++) a[i]=i;
printf("%d %d\n", a[0], a[n-1]);
return 0;
}
큰 배열이거나 장수명 데이터에는 힙 사용을 권장한다.
6.2 동적 배열(확장·축소·정확 맞춤)
#include <stdio.h>
#include <stdlib.h>
int main(void){
size_t n=5;
if (n > SIZE_MAX/sizeof(int)) return 1; // 오버플로 가드
int *p = malloc(n*sizeof *p);
if(!p){ perror("malloc"); return 1; }
for(size_t i=0;i<n;i++) p[i]=(int)i;
// 확장
size_t m=10;
if (m <= SIZE_MAX/sizeof(int)) {
int *tmp = realloc(p, m*sizeof *tmp);
if(!tmp){ free(p); perror("realloc"); return 1; }
p = tmp;
for(size_t i=n;i<m;i++) p[i]=(int)i;
n=m;
}
// 축소 (뒤쪽 일부 반납 효과)
size_t k=6;
int *tmp = realloc(p, k*sizeof *tmp);
if(tmp){ p=tmp; n=k; } // 실패해도 p는 여전히 유효
printf("size=%zu last=%d\n", n, p[n-1]);
free(p); // 전체 해제만 가능
return 0;
}
7. 흔한 오류 체크리스트
n<=0
또는 과도하게 큰 값 사용으로 스택/힙 오류 발생- 크기 계산의 정수 오버플로 미검증
malloc
/realloc
결과 미검증realloc
이후 옛 포인터 사용(댕글링, 이중 해제)free
이후 접근(Use-After-Free) 또는 double free- 지역 배열 주소 반환(스택 주소 반환 버그)
- VLA를 미지원 컴파일러에서 사용
8. 선택 가이드
- 짧고 작은 임시 버퍼: 스택 고정 배열
int a[256];
을 우선 고려한다. - 런타임 크기 결정이 필요하나 소규모·단기 사용: 지원 환경에서 VLA 사용을 고려한다.
- 크거나 장수명 또는 크기 변경 필요: 힙 할당(
malloc
)을 사용하고 필요 시realloc
을 적용한다.
9. 초기화와 기타 사항
- 정적 저장기간 변수(전역/
static
)는 0으로 자동 초기화된다. malloc
은 초기화하지 않으므로 쓰레기 값을 가진다.calloc
은 0으로 초기화한다.free(NULL)
은 안전하며 부작용이 없다.
10. 디버깅 및 검증
- 컴파일 옵션:
-Wall -Wextra -Wvla -O2
사용을 권장한다. -
런타임 검증: AddressSanitizer(
-fsanitize=address,undefined
), 리눅스 valgrind가 유용하다. - Windows(MSVC): AddressSanitizer 지원 버전을 사용하거나
_CrtDumpMemoryLeaks()
등을 활용한다.
Leave a comment