[운영체제] 동적 메모리 할당
![[운영체제] 동적 메모리 할당](https://firebasestorage.googleapis.com/v0/b/cruz-lab.firebasestorage.app/o/images%2Fheroes%2Fhero-1766482449295.webp?alt=media&token=0df62fba-1387-4740-b8c9-1f76d1a8c29a)
C언어를 처음 배우고 나서 우리는 변수를 선언할 때 일반적으로
int a;
int arr[10];
이런식으로 크기를 미리 정해줬다.
그리고 이런 변수들은 프로그램이 시작될 때 이미 스택(Stack)이라는 메모리 영역에 자리가 예약되어있고,
프로그램 실행 중에 이 크기를 바꿀 수 없다.
하지만 만약 프로그램을 실행하는 사용자가 몇 개의 데이터를 입력할지 미리 알 수 없다면?
배열의 크기는 얼마로 잡아야 할까?
100개? 1000개?
너무 적게 잡으면 프로그램이 제대로 동작하지 않을 테고, 그렇다고 너무 크게 잡으면 메모리 낭비가 심해질 것이다.
이러한 고민을 해결해 주는 기술이 바로 “동적 메모리 할당(Dynamic Memory Allocation)”이다.
컴파일 타임 vs 런타임: 메모리 할당 시점의 차이
우리가 일반적으로 크기를 명시해서 변수를 선언하면 컴파일 타임(Compile Time)에 그 크기가 결정된다.
즉, 컴파일러가 스택(Stack)영역에 메모리를 할당하고 해제하는 코드를 알아서 만들어주므로 빠르고 간편하지만, 크기가 코드에 고정되어 있기에 유연성이 떨어진다.
void myFunction() {
// 컴파일러는 이 함수가 호출되면 int 100개 만큼의 공간(400바이트)을
// 스택에 할당하라는 코드를 생성한다.
int score_list[100];
// ...
} // 함수가 끝나면 스택에 할당된 메모리는 자동으로 사라진다.
반면 동적 메모리 할당은 런타임(Run Time), 즉 프로그램이 실행되는 도중에 필요한 만큼의 메모리를 요청해서 할당받고, 다 쓰면 반납하는 방식을 말한다.
실행 당시의 상황에 따라 정확히 필요한 만큼만 메모리를 빌려 쓰는 것이다.
void processUserData() {
int user_count;
// 런타임에 사용자로부터 데이터 개수를 입력받는다.
printf("처리할 사용자 수를 입력하세요: ");
scanf("%d", &user_count);
// 컴파일러는 user_count가 몇이 될지 전혀 모른다.
// 실행 도중 user_count 값에 따라 힙(Heap) 영역에 메모리를 요청한다.
User* user_list = (User*)malloc(sizeof(User) * user_count);
// ...
// 힙에 할당된 메모리는 자동으로 사라지지 않으므로,
// 프로그래머가 직접 해제(free)해야 한다.
free(user_list);
}
이 때 동적으로 할당된 메모리가 머무는 공간을 바로 힙(Heap)이라고 부른다.
힙은 매우 유연하고 효율적인 공간이지만,
malloc으로 요청하고 free로 반납하는 모든 과정을 개발자가 직접 책임지고 관리해야한다.
⇒ 관리를 소홀히 할 경우 메모리 누수와 같은 심각한 버그가 발생할 수 있다.
힙을 관리하는 핵심 도구들
힙 메모리를 자유자재로 다루려면 <stdlib.h> 헤더 파일에 선언된 함수들과 친해져야 한다.
1. malloc (memory allocate) - "일단 아무 자리나 빨리 주세요!"
-
역할:
지정된 크기(바이트)만큼의 메모리 블록을 할당
-
특징:
-
빠른 속도: 메모리를 초기화하는 과정이 없으므로 가장 빠르다.
-
초기화 없음: 할당된 메모리 공간에는 이전에 다른 프로그램이 사용했던 쓰레기값(Garbage Value)이 그대로 남아있을 수 있다.
-
-
사용법:
함수 원형:
void* malloc(size_t size);// int 5개를 저장할 공간을 할당 (int가 4바이트라면 총 20바이트) // 이 공간에는 무슨 값이 들어있을지 아무도 모른다. int* arr = (int*)malloc(sizeof(int) * 5); -
언제 쓸까?: 할당받은 공간에 바로 새로운 값을 덮어쓸 예정이라, 굳이 0으로 초기화할 필요가 없을 때 사용
2. free (release) - "다 썼습니다!"
-
역할:
malloc이나 다른 함수들로 할당받았던 메모리 공간을 다시 운영체제에 반납 -
중요성:
free를 호출하지 않으면, 프로그램이 끝날 때까지 그 메모리는 '사용 중'인 상태로 남아 아무도 쓸 수 없게 된다.이걸 메모리 누수라고 부르며, 프로그램이 오래 실행될수록 점점 더 많은 메모리를 차지하다가 결국 시스템을 마비시킬 수도 있는 심각한 버그이므로 반드시 짝을 맞춰 사용해야 한다!
-
사용법:
함수 원형:
void free(void* ptr);ptr: 할당받았던 메모리의 시작 주소를 가리키는 포인터
3. calloc (contiguous allocate) - "깨끗하게 치워진 자리로 주세요!"
-
역할:
지정된 개수와 크기의 메모리 블록을 할당하고, 그 내용을 모두 0으로 초기화
-
특징:
-
안전성: 모든 비트가 0으로 초기화되므로, 예기치 않은 쓰레기값으로 인한 버그를 원천적으로 방지 가능
-
속도는 조금 느림:
malloc에 비해 초기화 과정이 추가되어 약간의 성능 저하가 있을 수 있음
-
-
사용법:
함수 원형:
void* calloc(size_t num, size_t size);// int 5개를 저장할 공간을 할당하고, 모든 공간을 0으로 초기화한다. // arr[0]~arr[4]는 모두 0이다. int* arr = (int*)calloc(5, sizeof(int)); -
언제 쓸까?: 할당과 동시에 메모리가 0으로 초기화되어야 하는 상황, 특히 포인터나 구조체 배열을 다룰 때 유용
4. realloc (re-allocate) - “자리가 좁은데, 좀 넓혀주세요!”
-
역할: 이미 할당된 메모리 블록의 크기를 변경
-
특징:
-
유연성: 메모리 크기를 동적으로 늘리거나 줄일 수 있음
-
데이터 보존: 크기를 늘릴 때, 기존에 있던 데이터는 그대로 보존 ⇒ 만약 더 넓은 새 자리로 이사 가면, 기존 데이터는 새 자리로 복사
-
-
주의사항:
realloc이 기존 공간을 확장하지 못하고 완전히 새로운 곳에 메모리를 할당하는 경우, 원래 포인터는 더 이상 유효하지 않은 주소(댕글링 포인터)가 될 수 있다.따라서 항상
realloc의 반환값을 새로운 포인터 변수에 받아 사용하는 것이 안전! -
사용법:
함수 원형:
void* realloc(void *ptr, size_t new_size);// int 5개짜리 공간을 먼저 할당 int* arr = (int*)malloc(sizeof(int) * 5); // ... 사용 중 공간이 부족해져서 10개짜리 공간으로 확장 ... // arr에 있던 기존 데이터는 그대로 유지된다. int* new_arr = (int*)realloc(arr, sizeof(int) * 10); // realloc 성공 여부 확인 if (new_arr != NULL) { arr = new_arr; // 성공 시 포인터 업데이트 } -
언제 쓸까?: 프로그램 실행 중 필요한 메모리 크기가 계속 변할 때 사용
| 구분 | malloc | calloc | realloc |
|---|---|---|---|
| 인자 | (총 바이트) | (개수, 개별 크기) | (기존 포인터, 새 크기) |
| 초기화 | ❌ (쓰레기값) | ⭕️ (모두 0으로) | ❌ (기존 데이터 유지) |
| 주요 용도 | 가장 일반적인 동적 할당 | 안전한 초기값이 필요할 때 | 이미 할당된 공간의 크기 변경 |
어떤 함수를 쓰든, 할당에 실패하면 NULL을 반환한다는 점과, 다 쓴 메모리는 반드시 free()로 해제해야 한다는 점을 절대 잊지말자!
코드 예시
#include <stdio.h>
#include <stdlib.h> // malloc, free 함수를 위해 반드시 필요!
int main() {
int n;
int* arr; // 할당받은 메모리의 주소를 저장할 포인터 변수
// 1. 사용자에게 필요한 메모리 크기(배열의 크기)를 입력받음
printf("몇 개의 정수를 저장하시겠습니까?: ");
scanf("%d", &n);
// 2. 메모리 할당 요청 (malloc)
// int 타입 n개 만큼의 크기를 바이트 단위로 계산하여 요청
// (int의 크기) * (원하는 개수)
arr = (int*)malloc(sizeof(int) * n);
// 3. 할당 성공 여부 확인 (매우 중요!)
if (arr == NULL) {
printf("메모리 할당에 실패했습니다.\n");
return 1; // 비정상 종료
}
// 4. 할당된 메모리 사용 (일반 배열처럼 사용 가능)
for (int i = 0; i < n; i++) {
arr[i] = i + 1;
printf("%d ", arr[i]);
}
printf("\n");
// 5. 메모리 반납 (free)
// 할당과 반납은 1:1 대응!
free(arr);
// (참고) 메모리 해제 후 포인터 접근은 위험! (Dangling Pointer)
// arr = NULL; // 안전을 위해 NULL로 초기화하는 습관도 좋음
return 0;
}