C언어

12-1) 동적 메모리 할당 및 해제

SleeveStar 2021. 5. 1. 13:43
반응형

정적 메모리 할당의 한계

 

컴파일러의 설정을 변경하지 않았다면 프로세스 안에서 지역 변수가 저장되는 기본 스택 메모리 크기는 1Mbyte이다. 따라서 함수를 호출할 때 지역 변수가 할당되는 메모리 공간은 최대 1Mbyte를 넘을 수 없다.

 

예를 들어 char data[1024 * 1024]; 와 같이 배열을 선언하면 스택에 1Mbyte가 할당되기 때문에 오류가 발생한다.  

 

그렇다면 스택의 크기는 어떻게 계산할까?

 

함수가 호출되는 여부나 횟수를 미리 파악할 수 없기 때문에 프로그램이 사용할 전체 스택의 크기는 단순히 각 함수에 선언한 지역 변수의 크기를 합산해서 예측할 수는 없다. 이 말은 실제로 프로그램이 실행되어 함수가 호출될 때까지 스택 크기를 예측하기 어렵다는 뜻이다.

 

결국 스택의 실제 크기는 프로그램이 실행될 때가 돼서야 할 수 있기 때문에 컴파일러는 자신이 컴파일한 프로그램이 스택을 얼마나 사용할지 예상할 수 없다. 그래서 단일 배열의 크기나 단일 자료형의 크기가 1Mbyte를 넘는 경우에만 오류 메시지를 출력하고 각 함수에 선언한 변수의 크기를 합산해서 1Mbyte를 넘을때는 오류로 처리되지 않는다. 하지만 컴파일에 성공한다고 해도 프로그램을 실행할 때 스택을 사용하는 크기가 1Mbyte를 넘게 되면 다음과 같이 실행 오류가 발생한다.

 

"예외가 있습니다. 0xC00000FD: Stack overflow(매개변수: 0x00000000, 0x00372000)"

 

따라서 프로그래머는 자신의 프로그램이 스택을 1Mbyte이상 사용하지 않도록 주의해야 한다. 그런데 프로그램에서 사용하는 데이터는 당연히 1Mbyte보다 훨씬 큰경우가 많다. 예를 들어 보고 있는 스마트폰이나 모니터에 출력된 한 점의 데이터가 보통 4바이트 크기이다. 이런 픽셀들의 모임인 Full-HD 영상 한장을 저장하려면 1920 X 1080 X 4바이트, 대략 7Mbytes 정도의 크기가 필요하다. 결국 프로세스 안의 기본 스택으로는 Full_HD 영상 한 장 조차 저장하지 못한다는 뜻이다. 따라서 더 큰 메모리를 사용할 수 있는 방법이 필요하다.

 

 

 

 

동적 메모리 할당이란

 

프로세스는 더 큰 메모리를 할당해서 사용할 수 있도록 힙(Heap)이라는 공간을 제공한다.

힙은 스택 세그먼트 영역내에 포함되어 있다.

 

기계어 명령문

문자열 상수 목록 동적 메모리 할당
(heap)
전역 변수
(0으로 초기화)
지역 변수
(stack)
static 전역 변수
(초기화 안 됨)
코드 세그먼트 데이터 세그먼트 스택 세그먼트

스택은 '스택 프레임' 규칙을 통해 코드가 컴파일될 때 사용할 메모리 크기를 결정한다. 하지만 힙은 스택 프레임과 같은 형식이 적용되지 않으며 프로그래머가 원하는 시점에 원하는 크기만큼 메모리를 할당할 수 있다. 그리고 메모리 사용이 끝나면 언제든지 할당한 메모리 공간을 해제할 수 있다. 이런 형식의 메모리 할당을 '동적 메모리 할당'이라고 한다. 또

힙은 Mbyte 단위가 아닌 Gbyte단위까지 할당할 수 있기 때문에 메모리를 할당할 때 크기 문제가 거의 발생하지 않는다.

 

*스택 프레임이란? - 함수를 호출할 때 일어나는 스택의 변화

 

 

 

 

malloc 함수로 동적 메모리 할당하기

 

힙은 스택처럼 관리되는 공간이 아니라서 변수를 선언하는 행위로 메모리를 할당할 수 없다. 그래서 동적 메모리 할당을 지원하는 C 표준 함수인 malloc을 사용해서 메모리를 할당해야 한다. 이 하수는 같이 사용자가 size 변수에 지정한 크기만큼 힙 영역에 메모리를 할당하고 그 할당된 주소를 void* 형식으로 반환해 준다.

 

*malloc은 memory allocation의 약자

 

함수 원형 : void *malloc(size_t size) // size_t는 unsigned int와 같은 자료형

함수 사용 형식 : void *p = malloc(100);

 

메모리 크기를 지정할 때 size_t 자료형을 사용하는데 이 자료형은 unsigned int형과 같으며, 메모리 할당은 항상 양수로만 가능하기 때문에 음수를 고려하지 않겠다는 뜻이다.

 

그런데 사용자가 malloc 함수로 100바이트 메모리를 할당하더라도 이 메모리를 2바이트 단위의 50개 데이터 그룹으로 사용 사용할지, 4바이트 단위의 25개의 그룹으로 사용할지는 예상할 수 없다. 그래서 malloc 함수가 메모리의 사용단위를 결정하지 않고 void* 형식으로 주소를 반환해 주는 것이다. 

 

그런데 void*를 사용하면 사용할 때마다 형 변환(casting)을 해야하는 불편함이 있다. 그래서 다음과 같이 malloc 함수를 사용하여 주소를 받는 시점에 사용할 포이넡에 미리 형 변환을 사용하는 것이 좋다. 예를 들어 힙 영역에 할당된 100바이트의 메모리를 2바이트 단위로 사용하고 싶거나 4바이트 단위로 사용하고 싶다면 malloc 함수를 다음처럼 사용하면 된다.

 

short *p = (short *)malloc(100); //100바이트를 2바이트의 50그룹으로 쪼갬

 

int *p = (int *)malloc(100);  //100바이트를 4 바이트의 25그룹으로 쪼갬

 

 

*malloc 함수가 메모리 할당에 실패하는 경우도 있다.

- 한번에 너무 큰 크기(2Gbytes 이상)를 명시할 때

- 계속된 메모리 할당으로 힙에 공간이 부족할 때

 

이런 경우 malloc 함수는 할당된 메모리 주소 대신에 NULL을 반환한다.

 

 

 

 

free 함수로 할당된 메모리 해제하기

 

스택에 할당한 지역 변수는 함수 호출이 끝나면 스택 프레임에 의해 자동으로 해제된다. 하지만 힙에 할당한 메모리는 프로그램이 끝날 때까지 자동으로 해제되지 않는다. 사용하던 메모리가 해제되지 않으면 힙에 메모리를 할당할 공간이 부족해질 수 있다. 따라서 다음과 같이 free 함수를 사용하여 힙에 할당했던 메모리를 명시적으로 해제해주어야 한다.

 

free(p); // p가 가지고 있는 주소에 할당된 메모리를 해제함

 

예를 들어 malloc 함수를 사용하여 할당 받는 메모리의 주소 값을 포인터 변수 p가 가지고 있다고 하자. 그러면 위와 같이 포인터 변수 p가 가지고 있는 주소 값을 free 함수에 배개변수로 넘겨서 해당 주소에 할당된 메모리를 해제해야한다.

 

malloc 함수와 free 함수의 정보가 malloc.h에 정의되어 있기 때문에 이 함수들을 사용하려면 #include<malloc.h> 전처리기를 코드에 추가해야 한다.

 

 

 

동적 메모리 할당을 사용하여 이름 입력 받고 출력하기

 

 

 

malloc 함수를 사용할 때 주의할 점

 

동적으로 할당한 메모리는 malloc 함수를 사용할 때부터 free 함수를 사용할 때까지 계속 힙 영역에 할당되어있다. 따라서 다음과 같이 동적 메모리 할당을 사용하는 프로그램에서 실수로 메모리할당을 해제하는 free 함수를 사용하지 않았다면 Test 함수가 호출될 때마다 힙레 100바이트씩 추가로 메모리가 할당된다. 그래서 반복문이 완료된 시점에는 100 x 100바이트의 메모리가 힙에 할당되어 버린다.

 

그리고 힙에 할당된 주소를 기억하고 있는 포인터 변수 p는 지역 변수이기 때문에 Test 함수의 종료와 함께 메모리에서 제거된다. 그런데 제거된 포인터 변수 p가 동적 할당된 메모리의 주소 값을 저장하고 있었기 때문에 포인터 변수 p가 제거되면 동적 할당된 메모리의 주소 값을 알 수 있는 방법이 없어서 해당 메모리를 사용할 수 없고 해제할 수도 없다. 이런 상태를 메모리가 손실되었다고 이야기 한다.

 

 

 

할당되지 않은 메모리를 해제하는 경우

 

동적 메모리 할당을 많이 사용하느 프로그램은 메모리 손실이 나지 않도록 free 함수를 빼놓지 않도록 신경을 많이 써야 한다. 그렇다고 해서 할당도 되지 않은 메모리를 해제하면 컴파일은 성공하더라도 실행할 때 오류가 발생한다.

 

char *p;

// p = (char *)malloc(32);

free(p);  // p는 할당된 메모리의 주소를 가지고 있지 않아서 실핼할 때 오류 발생함

 

 

 

 

정적으로 할당된 메모리를 해제하는 경우

 

포인터가 정적으로 할당된 지역 변수의 주소를 가지고 있는데 이 주소를 사용하여 free 함수를 호출해도 실행할 때 오류가 발생한다.

 

int data = 5;

int *p = &data;  // p는 지역 변수 data의 주소를 가지게 됨

free(p); // p는 힙에 할당된 주소가 아니기 때문에 실행할 때 오류 발생함

 

 

 

 

할당된 메모리를 두 번 해제하는 경우

 

malloc 함수를 사용해 정상적으로 할당한 주소를 free 함수로 해제하고 나서 실수로 한 번 더 해제하는 경우에도 프로그램을 실행할 때 오류가 발생한다.

 

int *p = (int *)malloc(12); //12바이트 메모리를 힙에 정상적으로 할당함

free(p);  //할당했던 메모리를 정상적으로 해제함

free(p);  //이미 해제한 주소를 다시 해제하기 때문에 실행할 때 오류 발생함

 

 

 

 

동적 메모리 할당의 장단점

 

힙에 동적으로 할당하는 메모리는 스택에 비해 큰 크기의 메모리를 할당할 수 있으며 메모리를 할당하고 하제하는 시점도 프로그래머가 직접 정할 수 있다. 그리고 할당되는 메모리 크기도 프로그램 실행중에 변경할 수 있다. 그래서 할당되는 메모리 크기가 변경되어도 소스 코드를 다시 컴파일 하지 않아도 된다. 하지만 힙에 동적으로 메모리를 할당하고 해제하는 작업을 프로그래머가 직접 관리해야 하기 때문에 코드가 복잡해지며 작은 메모리를 할당해서 사용할 때는 오히려 비효율적일 수도 있다.

스택에 메모리 정적 할당 힙에 메모리 동적 할당
char data; //스택에 1바이트 사용함
data = 5;
//p는 지역 변수라서 스택에 정적 할당함
char *p;
//1바이트 메모리를 힙에 동적 할당함
p= (char *)malloc(1);
*p = 5;
free(p); // 동적 할당한 메모리 해제

 

정적 할당은 스택에 1바이트만 할당된다. 하지만 동적 할당은 스택 대신 힙에 1바이트를 할당하며, 할당받은 힙의 메모리 주소를 저장하기 위해 스택에 4바이트를 할당한다. 따라서 총 5바이트가 필요하다. 이런 상황에 굳이 동적할당을 고집하는것은 좋지 않다.

 

 

 

동적 메모리 사용하기

 

1바이트 2바이트처럼 크기가 작은 데이터 여러 개를 동적으로 할당해서 사용하는 것은 번거롭고 불편할 수 있다. 그래서 동적 할당도 메모리를 배열처럼 그룹으로 묶어서 많이 사용한다.

 

ex) int *p = (int *)malloc(12);  //12바이트를 할당하며 int형은 4바이트이기 때문에 세 그룹으로 나누어 사용하는 것.

int p[3]과 비슷하다

 

포인터 문법은 포인터 변수에 저장되어 있는 주소로 연산할 수 있다. 그래서 처음 4바이트는 *p 형식으로 사용하고, 그 다음 4바이트는 *(p + 1)형식으로 사용할 수 있으며, 마지막 4바이트는  *(p + 2) 형식으로 사용할 수 있다.

 

결국 이런 형식으로 메모리를 동적으로 할당하면 int형으로 그룹 지어진 메모리를 사용하는 것과 같기 때문에 아래쪽처럼 선언한 배열과 같은 목적으로 사용할 수 있다.

 

int data[3];

 

 

 

 

 

동적 메모리를 할당하는 또 다른 방법

 

동적 메모리를 할당할 때 앞에서 본 것처럼 malloc(12)라고 호출하면 할당할 전체 메모리의 크기를 명시하는 형태이다. 그런데 sizeof 연산자를 사용하면 메모리 사용 단위까지 적을 수 있다.

 

int *p = (int *)malloc(sizeof(int) * 3);   //sizeof(int) * 3 == 12

 

malloc(12)라고만 적어 놓으면 12바이트를 동적 할당한다는 정도로 이해할 수 있다. malloc(sizeof(int)*3)이라고 적으면 12바이트를 할당하면서 대상 메모리를 4바이트 단위로 나누어서 사용하려는 의도까지 좀 더 쉽게 파악할 수 있다.

그리고 같은 12바이트를 사용하더라도 다음과 같이 사용하면 메모리를 사용하는 방법이 달라진다.

 

 

short *p = (short *)malloc(sizeof(short) * 6); //sizeof(short) * 6 == 12

 

위 형태로 사용하면 포인터 p는 주소에 접근하여 2바이트(short) 단위로 메모리를 읽고 쓰기 때문에 12바이트를 6개의 항목으로 나누어 사용하게 된다.

 

이 구조도 변수 6개로 이루어진 배열과 같은 목적으로 사용할수 있다. 결국 포인터와 동적 할당 문법을 사용하면 배열과 같은 목적으로 사용할 수 있는 메모리를 구성할 수 있다.

 

short data[6];

 

 

 

정적 메모리 할당을 사용했을 때 발생할 수 있는 문제점

 

배열을 사용하면 메모리가 스택에 정적으로 할당되기 때문에 항목의 개수를 상수로만 할당할 수 있다. 따라서 다음과 같이 선언하면 오류가 발생한다.

 

int data_size = 3;

int data[data_size];  //배열의 요소 개수는 상수로만 명시할 수 있기 때문에 오류 발생함

 

즉, 배열의 크기는 상수로만 적을 수 있기 때문에 자신이 사용할 데이터의 최대 개수에 반드시 신경을 써야 한다. 예를 들어 친구 관리 프로그램을 만드는데 배열을 사용하고 이 배열의 한 요소에 한 명의 친구 정보가 저장된다고 하자. 그러면 친구가 몇 명인지에 따라서 배열의 크기가 달라져야 한다. 친구가 별로 없는 프로그램 사용자는 배열 크기가 10으로도 충분하겠지만 친구가 300명인 사용자도 있을 수 있다. 그러면 프로그래머는 어쩔 수 없이 최대 크기인 300으로 배열 크기를 정해야 한다. 하지만 이 프로그램을 친구가 10명인 사람이 사용하면 290개의 배열 요소를 낭비하는 셈이다.

 

이 문제는 결국 배열이 정적 메모리 할당 방식을 사용하기 때문에 배열의 크기를 상수로만 받아서 생기는 것이다. 

 

 

 

정적 메모리 할당을 사용하여 숫자를 입력 받아 합산하기

 

 

사용자가 10개의 숫자를 입력해야 한다고 요청하면 이 프로그램은 배열의 크기를 변경해야 하기 때문에 MAX_COUNT 값을 바꾸기 위해 프로그램 코드를 수정해야 한다. 즉 #define MAX_COUNT     10 과 같이 사용자가 요청할 때마다 최대 횟수를 변경해서 다시 컴파일 하고 재배포한다면 유지보수에 부담을 줄 수 있기 때문에 변경할 가능성이 없을 만큼 큰 숫자를 명시하기도 한다.

 

 

 

malloc 함수는 메모리 할당 크기를 변수로 지정할 수 있다

 

int data_size = 12;

int *p = (int *)malloc(data_size)  //12바이트의 메모리가 동적 할당됨

 

이렇게 메모리 할당 크기를 변수로도 사용할 수 있기 때문에 사용할 데이터의 개수를 제한할 필요가 없다. 

 

 

 

 

동적 메모리 할당을 사용하여 숫자를 입력받아 합산하기

 

 

배열 문법이 사용하기 편한 것은 사실이다 하지만 편한 문법의 공통점은 제약이 많다는 것이다. 위와 같이 처리하면 사용자가 직접 숫자의 개수를 지정할 수 있기 때문에 배열을 사용할 때 처럼 코드를 수정할 필요가 없다.

 

 

 

반응형