{ }codelper

배우기 › C++

언어마다 다른 것 · 35

동적 메모리

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

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

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

#include <iostream>#include <vector> int main() {    int n;    if (!(std::cin >> n) || n <= 0) return 0;     std::vector<int> a(n);  // n칸을 빌리고, 범위를 벗어나면 저절로 돌려줘요    long long sum = 0;    for (int& x : a) {        std::cin >> x;        sum += x;    }    for (int i = n - 1; i >= 0; --i)        std::cout << a[i] << (i > 0 ? ' ' : '\n');    std::cout << sum << '\n';}  // 여기서 a의 소멸자가 메모리를 해제해요 (delete가 필요 없어요)
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에 맡기는 게 가장 안전해요.

연습 문제