뭐라도 쓰겠지
25.03.10 / 메모리의 동적 할당 본문
메모리의 정적, 동적 할당을 구분하는 기준은 프로그램 실행 중에 메모리 공간을 할당하는가로 구분할 수 있다. 정적 할당 (Static Allocate)은 컴파일 단계에서 필요한 메모리 공간을 할당하고, 동적 할당 (Dynamic Allocate)은 실행 단계에서 공간을 할당한다.
우선 이걸 이해하려면 메모리의 구분법을 알아봐야 하는데, 아래의 사진을 보자

메모리는 Code, Data, Stack, Heap 4가지 영역으로 나눠진다.
Code 영역은 함수와 상수, 실행한 프로그램의 코드가 저장되는 영역이다. 이들은 컴파일 단계에서 검사를 마치고 저장되기 때문에 정적 메모리에 속한다.
Data 영역은 전역 변수(Global Variables)와 정적 변수(Static Variables)가 저장되는 영역이다. Code 영역과 마찬가지로 컴파일 단계에서 검사를 마치고 메모리가 할당되기 때문에 정적 메모리에 속한다.
Stack 영역은 지역 변수(Local Variables)와 매개 변수(Parameters)가 저장되는 영역이다. 통상적으로 우리가 변수라고 부르는 것들은 선언하면 이 영역에서 공간을 할당해준다. 지역 변수와 매개 변수는 선언된 함수 내에서만 사용 가능한데, 그 구역을 벗어나면 할당된 메모리 공간을 반납하고 사라진다. 그러기 때문에 이 영역은 동적 메모리에 속한다. Stack의 총 용량에는 한계가 있고, 대부분 그리 크지 않은 공간이다. 이 때문에 스택을 초과하는 배열 등을 선언하게 되면 Stack Overflow가 발생하며 컴파일러가 오류를 뱉는다.
Heap 영역은 동적 할당을 받는 영역이다. 동적 할당 받은 공간은 우리가 별도로 해제해주기 전까지는 사라지지 않는다. 그리고 동적 할당은 프로그램 실행 단계에서 발생하기 때문에 이 영역은 동적 메모리에 속한다. 위 3가지가 가지는 작은 용량을 제외한 나머지 모든 공간은 Heap 영역이다. 그렇기 때문에 컴퓨터가 발전한 현재는 가장 큰 용량을 가진다.
그럼 두 가지 할당 방법의 차이는 무엇이 있을까?
우선 정적 할당은 컴파일 단계에서 메모리 공간을 할당 받는다. 그리고 해당 함수, 프로그램이 종료될 때 컴퓨터가 알아서 할당받은 공간을 반환한다.
이런 특징 덕분에 정적 할당은 메모리 누수를 걱정하지 않아도 된다는 장점이 있다. 일단 컴파일해서 메모리를 할당 받으면 실행 단계에서 할당이 해제되지 않고, 함수가 사라지면 컴퓨터가 알아서 해당 메모리 공간을 회수한다.
단점으로는 할당받는 메모리 공간의 크기가 정해져 있어 프로그램이 실행될 때 수정할 수 없다는 점이 있다. int형이면 4바이트, double형이면 8바이트만 딱 받게 된다. 이 때문에 만약 처음 실행할 땐 100의 공간이 필요해 100만큼의 공간을 받았는데, 8만 필요해지게 되면 남은 92는 그저 노는 공간이 되어버리는 것이다. 반대로 110의 공간이 필요해 10만큼의 공간을 더 받으려해도 그러지 못한다.
이번엔 동적 할당을 알아보겠다. 동적 할당은 실행 단계에서 메모리 공간을 할당 받는다. Stack 영역에서 메모리 공간을 빌리는 정적 할당과는 달리 Heap 영역의 메모리 주소를 가리켜 해당 공간을 할당 받는다.
#include <stdio.h>
void main() {
int* pVal = NULL;
pVal = (int*)malloc(sizeof(int));
if (pVal != NULL) { // 유효성 검사
*pVal = 10;
printf("pVal : %d\n", *pVal);
}
if (pVal != NULL) {
free(pVal);
}
}
위 코드는 C언어에서 동적 할당을 받는법과 해제하는 법이다. malloc을 사용해 int의 크기와 동일한 4바이트의 메모리 공간을 사용할 수 있게 된다. 그리고 다 쓰고 나면 free(pVal)을 사용해 다시 Heap 영역에 메모리 공간을 반환한다.
이런 특징 덕분에 동적 할당은 언제나 필요한 만큼의 메모리가 맞춤으로 할당된다는 장점이 있다. 실행 단계에서 메모리 공간을 할당받고 해제하기 때문에 만약 100의 공간을 사용하다 110의 공간이 필요하면 10의 공간을 더 할당하고, 90의 공간만 필요하다면 10의 공간을 해제하면 된다. 낭비되는 메모리가 없다는 것이다.
대신에 사용하지 않는 공간을 사용자가 직접 해제해야 한다는 단점이 있다. 위의 코드에서 보이듯 free()를 사용해 사용이 끝난 공간을 반환해야 한다. 정적 할당은 컴퓨터가 알아서 해주지만 동적 할당은 그렇지 않다는 거다.
만약 사용하지 않는 메모리를 반납하지 않으면 어떻게 될까? 이런 경우 메모리 누수 (Memory Leak)이 발생하기도 한다. 당장 쓰지 않는 공간을 할당받은 채로 계속 있으면 정작 필요한 메모리가 부족해질 수 있다.
예를 들어 좀비가 1000마리쯤 몰려오는 게임을 만들었다 생각하자. 몰려오는 좀비들은 각각 메모리를 차지하는 오브젝트이고, 이게 쌓이게 된다면 메모리 사용량이 점점 커지는 것이다. 그런데 쓰러트린 좀비가 계속 사라지지 않고 남아있다고 생각해보자. 처음엔 1마리, 2마리 이런 적은 수가 몰려와 티가 안날 수도 있다. 그런데 갑자기 100마리, 1000마리, 10000마리가 몰려오면 어떻게 될까? 끝도 없이 쌓인 좀비의 메모리 사용량 때문에 사용 가능한 메모리가 꽉 차버리는 것이다. 가끔 게임을 오래 켜놓으면 처음 켰을때에 비교해 확연히 느려지거나 이런 경우 있지 않은가. 이게 메모리 누수 현상이 일어난 경우이다. 이런 경우를 방지하기 위해 우리는 메모리를 다 쓰고나면 할당을 해제하는 절차가 필요한 것이다.
근데 또 문제가 있는데, 메모리의 동적 할당이 항상 성공하는 게 아니라는 점이다. 만약 메모리가 부족하다거나, 다른 이유로 메모리 할당에 실패하면 동적 할당이 실패한다. 동적 할당이 실패한 메모리를 사용하려 하면 오류가 나고, 우리는 이를 방지하기 위해 맨처음 주소를 NULL로 초기화하고, 그 뒤에 유효성 검사라는 걸 한다. pVal의 주소가 NULL이 아니라면 (=동적 할당에 실패하지 않았다면) pVal이 가리키는 주소에 10이라는 값을 넣을 수 있다.
아래는 C언어의 동적 할당과 관련된 함수의 예시이다.
| 함수 | 구분 | 사용 예시 |
| malloc | 원형 | void *malloc(unsigned int size); |
| 기능 | size 바이트 수만큼 할당하고 시작 위치 반환 | |
| 사용 예시 | int *p = (int *)malloc(sizeof(int)); | |
| calloc | 원형 | void *calloc(unsigned int count, unsigned int size); |
| 기능 | (count * size) 바이트 수만큼 할당하고 0으로 초기화 후 시작 위치 반환 | |
| 사용 예시 | double *p = (double *)calloc(5, sizeof(double)); | |
| realloc | 원형 | void *realloc(void *p, unsigned int size); |
| 기능 | p가 연결된 영역의 크기를 size 바이트의 크기로 조정하고 시작 위치 반환 | |
| 사용 예시 | char *p = (char *)realloc(p, 2 * strlen(str)); | |
| free | 원형 | void free(void *p); |
| 기능 | p가 연결한 영역 반환 |
'프로그래밍 > C' 카테고리의 다른 글
| 25.03.11 / 동적 배열 (0) | 2025.03.11 |
|---|---|
| 25.03.11 / 배열 (0) | 2025.03.11 |
| 25.03.07 / 삼각수를 이용해 중첩 반복문 없이 피라미드 출력하기 (0) | 2025.03.07 |
| 25.03.07-25.03.11 / 중첩 반복문을 사용한 피라미드 (0) | 2025.03.07 |
| 25.03.06 / 비트 플래그를 이용한 퀘스트 클리어 여부 확인 (0) | 2025.03.06 |