언어마다 다른 것 · 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가 필요 없어요)#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;}5 3 1 4 1 5
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에 맡기는 게 가장 안전해요.