2단원 — 동적 메모리
지금까지 배열의 크기는 **컴파일 시점에 결정**됐습니다. 하지만 런타임에 입력으로 받은 N의 크기로 배열이 필요하다면? **동적 메모리 할당**의 차례입니다.
이 강의에서 배우는 것
- 1스택과 힙의 차이를 이해한다.
- 2`malloc`, `calloc`, `realloc`, `free`의 역할을 안다.
- 3메모리 누수, 이중 해제, 댕글링 포인터를 피한다.
- 4동적 1차원/2차원 배열을 만든다.
왜 동적 메모리가 필요한가? — 스택과 힙
메모리 영역
┌─────────────────────┐ ◄── 높은 주소
│ 스택 ▼ │ 지역 변수, 함수 호출 정보
│ │ 자동 할당/해제, 빠름, 작음(보통 1~8MB)
├─────────────────────┤
│ ↕ (빈 공간) │
├─────────────────────┤
│ 힙 ▲ │ malloc/free로 직접 관리
│ │ 크기 제한이 거의 없음
├─────────────────────┤
│ 데이터/코드 영역 │
└─────────────────────┘ ◄── 낮은 주소스택만으로는:
- 크기를 **컴파일 시 알아야** 함
- 크기 제한 (큰 배열은 스택 오버플로)
- 함수 종료 시 자동 사라짐 (함수 밖으로 들고 나갈 수 없음)
힙은 위 제약이 없으므로 **N개 입력 받아 N개 슬롯**, **함수가 만들어 반환** 같은 패턴이 가능해집니다.
핵심 개념
1) 4가지 함수 (`<stdlib.h>`)
| 함수 | 의미 |
|---|---|
| `void *malloc(size_t n)` | n 바이트 할당 (초기화 X) |
| `void *calloc(size_t cnt, size_t sz)` | `cnt*sz` 바이트 할당 후 0으로 초기화 |
| `void *realloc(void *p, size_t n)` | 기존 블록을 n 바이트로 재할당 |
| `void free(void *p)` | 해제 |
2) 기본 사용 패턴 — "할당, 검사, 사용, 해제"
int *arr = malloc(n * sizeof(int)); // 1) 할당
if (arr == NULL) { /* 실패 처리 */ } // 2) 검사
for (int i = 0; i < n; i++) arr[i] = i; // 3) 사용
free(arr); // 4) 해제
arr = NULL; // (선택) 댕글링 방지`malloc` 호출 1회 = `free` 호출 1회. **항상 짝**.
3) `realloc`의 안전한 사용
int *tmp = realloc(arr, new_size * sizeof(int));
if (tmp == NULL) {
/* 실패: arr는 여전히 유효, 그대로 사용 가능 */
} else {
arr = tmp;
}결과를 **임시 변수**로 받아야 실패 시 원본을 잃지 않습니다.
4) 동적 2차원 배열
int **mat = malloc(rows * sizeof(int *));
for (int i = 0; i < rows; i++)
mat[i] = malloc(cols * sizeof(int));해제는 **역순**:
for (int i = 0; i < rows; i++) free(mat[i]);
free(mat);예제로 보기
예제 1 — `ex01_malloc.c` : 동적 배열
int *arr = malloc((size_t)n * sizeof(int));
if (arr == NULL) { perror("malloc"); return 1; }**입력**: `5`
배열 크기 입력: 10 20 30 40 50핵심: 입력값 N에 따라 배열 크기가 결정됩니다. 정적 배열로는 불가능.
예제 2 — `ex02_calloc.c` : 0 초기화
**실행 결과**
calloc 직후: 0 0 0 0 0핵심: `malloc`은 쓰레기 값, `calloc`은 0으로 초기화. 0 초기화가 필요하면 calloc.
예제 3 — `ex03_realloc.c` : 동적 확장
10개를 추가하면서 가득 차면 용량을 두 배로 확장.
**실행 결과**
(용량 확장: 8)
(용량 확장: 16)
최종 배열: 100 200 300 400 500 600 700 800 900 1000핵심: 동적 자료구조(가변 길이 벡터, 큐 등)의 핵심 패턴.
예제 4 — `ex04_2d.c` : 동적 2차원 배열
**실행 결과**
1 2 3 4
5 6 7 8
9 10 11 12핵심: `int **`는 "포인터의 포인터". 두 번 할당, 두 번 해제.
다른 시각으로 보기 — "도서관 책 빌리기"
malloc = 도서관에서 책을 빌려 오기 (필요할 때 가서 빌림)
free = 빌린 책을 반납하기 (다 쓰면 돌려줌)
빌리고 안 돌려주면 → 메모리 누수 (leak)
같은 책 두 번 반납 → 이중 해제 (double free)
빌렸던 책을 반납 후 또 봄 → 댕글링 포인터 (use-after-free)도서관(OS)은 책을 무한정 가지고 있지 않으므로 **누수가 쌓이면** 결국 빌릴 책이 떨어집니다.
자주 하는 실수
- **누수(leak)**: `free` 호출을 잊거나 분기에서 빠뜨림.
- **이중 해제**: 같은 포인터를 두 번 free → 정의되지 않은 동작.
- **해제 후 사용**: 댕글링 포인터.
- **크기 계산 실수**: `malloc(n)` 처럼 `* sizeof(int)`를 빠뜨림.
- **`malloc` 결과 NULL 미검사**: 메모리가 부족하면 NULL 반환 → 그대로 쓰면 세그폴트.
- **`realloc` 결과를 같은 포인터로 받기**: 실패 시 원본까지 잃음.
정리
- 스택은 자동·빠름·작음, 힙은 수동·유연·큼.
- `malloc`/`calloc`/`realloc`로 할당, `free`로 해제.
- 할당과 해제는 항상 **1:1로 짝**을 맞춘다.
- `realloc`의 결과는 임시 변수로 받아 실패에 대비한다.
- 누수·이중 해제·댕글링은 메모리 사고의 3대장.
직접 해 보기
cd src
gcc -std=c11 -Wall -o ex01 ex01_malloc.c && echo 5 | ./ex01
gcc -std=c11 -Wall -o ex02 ex02_calloc.c && ./ex02
gcc -std=c11 -Wall -o ex03 ex03_realloc.c && ./ex03
gcc -std=c11 -Wall -o ex04 ex04_2d.c && ./ex04응용:
- `valgrind ./ex01` 로 누수가 없는지 확인 (Linux).
- `ex04`의 행/열을 실행 시간에 입력받도록 바꿔 보세요.
마무리
[메인 README](../../README.md) — 전체 커리큘럼 보기.
💻 예제 (examples)
실제로 컴파일·실행해 결과를 확인할 수 있는 예제입니다.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int n;
printf("배열 크기 입력: ");
scanf("%d", &n);
int *arr = malloc((size_t)n * sizeof(int));
if (arr == NULL) { perror("malloc"); return 1; }
for (int i = 0; i < n; i++) arr[i] = (i + 1) * 10;
for (int i = 0; i < n; i++) printf("%d ", arr[i]);
putchar('\n');
free(arr);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
int main(void) {
size_t n = 5;
int *arr = calloc(n, sizeof(int)); /* 0으로 초기화 */
if (!arr) { perror("calloc"); return 1; }
printf("calloc 직후: ");
for (size_t i = 0; i < n; i++) printf("%d ", arr[i]);
putchar('\n');
free(arr);
return 0;
}
calloc 직후: 0 0 0 0 0#include <stdio.h>
#include <stdlib.h>
int main(void) {
size_t cap = 4;
size_t len = 0;
int *arr = malloc(cap * sizeof(int));
if (!arr) { perror("malloc"); return 1; }
/* 10개를 추가하면서 필요할 때 확장 */
for (int i = 0; i < 10; i++) {
if (len == cap) {
cap *= 2;
int *tmp = realloc(arr, cap * sizeof(int));
if (!tmp) { free(arr); perror("realloc"); return 1; }
arr = tmp;
printf("(용량 확장: %zu)\n", cap);
}
arr[len++] = (i + 1) * 100;
}
printf("최종 배열: ");
for (size_t i = 0; i < len; i++) printf("%d ", arr[i]);
putchar('\n');
free(arr);
return 0;
}
(용량 확장: 8)
(용량 확장: 16)
최종 배열: 100 200 300 400 500 600 700 800 900 1000#include <stdio.h>
#include <stdlib.h>
int main(void) {
int rows = 3, cols = 4;
/* 행 포인터 배열 할당 */
int **mat = malloc((size_t)rows * sizeof(int *));
if (!mat) { perror("malloc"); return 1; }
/* 각 행 할당 */
for (int i = 0; i < rows; i++) {
mat[i] = malloc((size_t)cols * sizeof(int));
if (!mat[i]) { perror("malloc"); return 1; }
for (int j = 0; j < cols; j++) mat[i][j] = i * cols + j + 1;
}
/* 출력 */
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) printf("%4d", mat[i][j]);
putchar('\n');
}
/* 역순 해제 */
for (int i = 0; i < rows; i++) free(mat[i]);
free(mat);
return 0;
}
1 2 3 4
5 6 7 8
9 10 11 12📝 과제 (exercises)
직접 풀어보고, 막힐 때 정답을 펼쳐 비교해보세요.
문제 1 (hw01.c)
목표: 사용자에게 배열 크기 N을 입력받고, N개의 정수를 입력받은 뒤 **최댓값과 최솟값**을 출력하세요. 메모리는 `malloc`/`free`로 관리.
- 파일명: hw01.c
▶정답 코드 펼치기 / 접기
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int n;
printf("N 입력: ");
scanf("%d", &n);
if (n <= 0) { printf("N은 양수여야 합니다.\n"); return 1; }
int *arr = malloc((size_t)n * sizeof(int));
if (!arr) { perror("malloc"); return 1; }
printf("정수 %d개 입력: ", n);
for (int i = 0; i < n; i++) scanf("%d", &arr[i]);
int mx = arr[0], mn = arr[0];
for (int i = 1; i < n; i++) {
if (arr[i] > mx) mx = arr[i];
if (arr[i] < mn) mn = arr[i];
}
printf("최댓값: %d\n최솟값: %d\n", mx, mn);
free(arr);
return 0;
}
문제 2 (hw02.c)
목표: 임의의 N×M 행렬을 동적 할당해 0으로 초기화한 뒤, 대각선만 1로 바꾸어 출력하세요. (단위행렬 비슷한 효과, N=M일 필요는 없음)
- 파일명: hw02.c
▶정답 코드 펼치기 / 접기
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int n, m;
printf("행 N, 열 M: ");
scanf("%d %d", &n, &m);
int **mat = calloc((size_t)n, sizeof(int *));
if (!mat) { perror("calloc"); return 1; }
for (int i = 0; i < n; i++) {
mat[i] = calloc((size_t)m, sizeof(int));
if (!mat[i]) { perror("calloc"); return 1; }
}
int len = (n < m) ? n : m;
for (int i = 0; i < len; i++) mat[i][i] = 1;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) printf("%2d", mat[i][j]);
putchar('\n');
}
for (int i = 0; i < n; i++) free(mat[i]);
free(mat);
return 0;
}
문제 3 (hw03.c)
목표: 사용자에게 문자열을 입력받아 **그 문자열의 복사본**을 동적으로 할당해 반환하는 `char *my_strdup(const char *s)` 함수를 구현하고 사용 예를 보이세요. (`<string.h>`의 `strlen`/`memcpy` 사용 가능)
- 파일명: hw03.c
▶정답 코드 펼치기 / 접기
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *my_strdup(const char *s) {
size_t len = strlen(s);
char *copy = malloc(len + 1);
if (!copy) return NULL;
memcpy(copy, s, len + 1); /* 널 문자 포함 */
return copy;
}
int main(void) {
char buf[256];
printf("문장 입력: ");
if (fgets(buf, sizeof(buf), stdin) == NULL) return 1;
buf[strcspn(buf, "\n")] = '\0';
char *dup = my_strdup(buf);
if (!dup) { perror("malloc"); return 1; }
printf("원본: %s\n", buf);
printf("복사본: %s\n", dup);
free(dup);
return 0;
}