{ }codelper

배우기 › C

언어마다 다른 것 · 35

동적 메모리

실행 중에 정한 크기만큼 메모리를 빌리고, 다 쓰면 꼭 돌려줘요.

이 단원은 C++ · C에서 배워요.
Python·JavaScript·TypeScript·Java·C#·크레이콘은 가비지 컬렉터가 더는 쓰지 않는 객체를 알아서 치워 줘서, 메모리를 빌리고 돌려주는 코드를 쓰지 않아요.

입력을 받아 봐야 크기를 알 수 있을 때는 실행 중에 힙(heap)에서 메모리를 빌려요.
아래 예제는 n을 읽고 n칸을 빌려 수를 담은 뒤, 거꾸로 찍고 합을 구하고 돌려줘요.

#include <stdio.h>#include <stdlib.h> int main(void) {    int n;    if (scanf("%d", &n) != 1 || n <= 0) return 0;     int *a = malloc(n * sizeof *a);  /* 힙에서 n칸을 빌려요 */    if (a == NULL) return 1;         /* 못 빌리면 NULL */     long long sum = 0;    for (int i = 0; i < n; i++) {    /* i <= n 이면 넘쳐요 */        scanf("%d", &a[i]);        sum += a[i];    }    for (int i = n - 1; i >= 0; i--)        printf("%d%c", a[i], i > 0 ? ' ' : '\n');    printf("%lld\n", sum);     free(a);   /* 빌린 만큼 한 번 돌려줘요 */    a = NULL;  /* 해제한 뒤에 실수로 쓰지 않게 */    return 0;}
stdin
5
3 1 4 1 5
stdout
5 1 4 1 3
14

핵심

  • 지금까지 배열은 int arr[100]처럼 크기를 미리 정했어요.
    힙에서는 실행 중에 정한 크기만큼 빌릴 수 있어요.
  • 지역 변수(스택)는 함수가 끝나면 저절로 사라져요.
    힙에서 빌린 메모리는 직접 돌려줄 때까지 남아요.
  • 그래서 함수가 끝난 뒤에도 쓸 결과를 돌려줄 수 있어요.
    대신 돌려주는 일도 내 몫이에요.
  • C++는 메모리를 객체의 수명에 묶어요(RAII).
    변수가 범위를 벗어나면 소멸자가 돌려줘서, 예외가 나거나 중간에 return해도 해제가 빠지지 않아요.
  • gcc -fsanitize=address로 컴파일하면 누수, 해제 후 쓰기, 넘침을 실행할 때 잡아 줘요.

언어별로 쓰는 법

C++보통 std::vector<int> a(n)(0으로 채운 n칸).
객체 하나나 크기가 바뀌지 않는 배열은 std::make_unique<int[]>(n)으로 만든 std::unique_ptr — new int[n]/delete[] a도 있지만(하나만 빌렸으면 delete, 짝이 틀리면 정의되지 않은 동작) 요즘은 거의 직접 쓰지 않아요.
int *a = malloc(...)은 void *가 저절로 바뀌지 않아서 C++에서는 컴파일 오류예요
C<stdlib.h>의 malloc(n * sizeof *a) — 모자라면 NULL, 칸에는 쓰레기 값.
calloc(n, sizeof *a)는 0으로 채우고, realloc(a, m * sizeof *a)는 자리를 옮길 수도 있어서 돌려받은 새 포인터를 써요.
다 쓰면 free(a)

자주 하는 실수

free를 잊었어요

for (int k = 0; k < 1000; k++) {
    int *a = malloc(n * sizeof *a);
    /* … free(a)가 없어요 */
}

→ 프로그램은 멀쩡히 돌지만 쓰는 메모리가 계속 늘어나요(메모리 누수)

빌린 곳마다 free 하나.
C++는 std::vector에 맡겨요.

해제한 뒤에 쓰거나 두 번 해제했어요

free(a);
printf("%d\n", a[0]);
free(a);

→ 해제 후 쓰기(use after free)는 정의되지 않은 동작인데, 옛 값이 그대로 보이기도 해서 더 찾기 어려워요. 두 번 해제(double free)하면 glibc는 보통 free(): double free detected in tcache 2를 찍고 멈춰요

free(a); 다음에 a = NULL;.
실수로 쓰면 대개 바로 죽고, free(NULL)은 아무 일도 하지 않아요.

i <= n으로 빌린 칸을 넘었어요

int *a = malloc(n * sizeof *a);
for (int i = 0; i <= n; i++)
    a[i] = 0;

→ 빌린 n칸 바로 다음인 a[n]에 써서 남의 메모리를 망가뜨려요(버퍼 넘침). 그 자리에서는 아무 일 없다가 한참 뒤 엉뚱한 곳에서 죽기도 해요

i < n으로 쓰고, -fsanitize=address로 확인해요.

언어마다 다른 점
빌린 것은 정확히 한 번 돌려줘요.
malloc·calloc·realloc 하나에 free 하나, new에는 delete, new[]에는 delete[]이고, C++에서는 이 짝 맞추기를 std::vector와 std::unique_ptr에 맡기는 게 가장 안전해요.

연습 문제