동적할당

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은 초기화하지 않으므로 쓰레기 값을 가진다. calloc0으로 초기화한다.
  • free(NULL)은 안전하며 부작용이 없다.

10. 디버깅 및 검증

  • 컴파일 옵션: -Wall -Wextra -Wvla -O2 사용을 권장한다.
  • 런타임 검증: AddressSanitizer(-fsanitize=address,undefined), 리눅스 valgrind가 유용하다.

  • Windows(MSVC): AddressSanitizer 지원 버전을 사용하거나 _CrtDumpMemoryLeaks() 등을 활용한다.

Categories:

Updated:

Leave a comment