7.2 동적 메모리 할당

Kal·2021년 7월 24일
0

C언어

목록 보기
17/17

1. 동적 메모리 할당 및 해제

1.1 정적 메모리 할당의 한계

컴파일러의 설정을 변경하지 않았다면 프로세스 안에서 지역 변수가 저장되는 기본 스택 메모리 크기는 1Mbyte이다.

따라서 함수를 호출할 때 지역 변수가 할당되는 메모리 공간은 최대 1Mbyte를 넘을 수 없다.

char data[1024*1024] 

! 오류발생

이렇게 1Mbyte를 넘는 배열을 선언하면 스택에 1Mbyte가 할당되기 때문에 오류가 발생한다.

프로그램이 사용할 전체 스택의 크기는 프로그램이 실행되어 함수를 호출할 때까지 가늠하기가 어렵다.

결국, 스택의 실제 크기는 프로그램이 실행할 때가 돼서야 알 수 있기 때문에 컴파일러는 자신이 컴파일한 프로그램이 스택을 얼마나 사용할지 예상할 수 없다.

1.2 동적 메모리 할당이란?

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

스택은 ' 스택 프레임 ' 규칙을 통해 컴파일될 때 사용할 메모리 크기를 결정한다. 하지만 힙(Heap)은 스택 프레임와 같은 형식이 적용되지 않으며 프로그래머가 원하는 시점에 원하는 크기만큼 메모리를 할당할 수 있다.

메모리 사용이 끝나면 언제든지 할당한 메모리 공간을 해체할 수 있다.

이런 형식의 메모리 할당을 ' 동적 메모리 할당 (Dynamic Memory Allocation)이라고 한다.

힙(Heap)은 Mbyte 단위가 아닌 Gbyte 단위까지 할당할 수 있다. 문제 발생 X

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

힙은 스택처럼 관리되는 공간이 아니라서 변수를 선언하는 행위로 메모리를 할당할 수 없다. 그래서 메모리 할당을 지원하는 C 표준 함수인 malloc(memory allocation의 줄임)을 사용해서 메모리를 할당해야 한다.

malloc 함수는 사용자가 size 변수에 지정한 크기만큼 힙 영역에 메모리를 
할당하고 그 할당된 주소를 void* 형식으로 반환해 준다.
함수 원형 : void *malloc(size_t size); /* size_t는 unsigned int와 같음*/
함수 사용 형식 : void *p = malloc(100); /* 100바이트의 메모리를 할당하여
포인터 p에 저장함 */

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

사용자가 malloc 함수로 100바이트 메모리를 할당하더라도 이 메모리를 short형식으로 사용할지, int 형식으로 사용할 지 예상할 수 없다. 따라서 void* 형식으로 주소를 반환하는 것이다.

void*를 사용하면 형 변환(casting)을 해야하는 불편함이 있기 때문에 주소를 받는 시점에 사용할 포인터에 미리 형 변환을 사용하는 것이 좋다.

short *p = (short *)malloc(100);
int *p = (int *)malloc(100);

이렇게 Casting을 미리 해 주면 좋다.

- malloc 함수가 메모리 할당에 실패하는 경우

malloc 함수가 항상 메모리 할당에 성공하는 것은 아니다.

1. 한 번에 너무 큰 크기(2Gbyte 이상)를 명시할 때
2. 계속된 메모리 할당으로 힙에 공간이 부족할 때
이런 경우 malloc 함수는 할당된 메모리 주소 대신 NULL을 반환한다. 따라서 malloc
함수의 메모리 할당 실패를 대비해 반환값이 NULL인지 체크하는 것이 좋다.

short *p = (short *)malloc(100);
if(NULL != P) { /*메모리 할당에 성공함. 이 시점부터 100바이트 메모리 사용 가능/
} else { } /*메모리 할당에 실패함 */

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

스택에 할당된 지역 변수와는 달리 힙에 할당된 메모리는 프로그램이 끝날 때까지 자동으로 해제되지 않는다.

따라서 다음과 같이 free 함수를 이용하여 힙에 할당했던 메모리를 명시적으로 해제해주어야 한다.

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

예를 들어 malloc 함수를 사용하여 할당 받는 메모리의 주소 값을 포인터 변수 p가 가지고 있다고 하자.

그러면 위와 같이 포인터 변수 p가 가지고 있는 주소 값을 free 함수에 매개변수로 넘겨서 해당 주소에 할당된 메모리를 해체하여야 한다.

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

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

(fgets 함수를 사용했다.)

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

동적으로 할당된 메모리는 malloc 함수를 사용할 때부터 free 함수를 사용할 때까지 계속 힙 영역에 할당되어 있다. 따라서 free 함수를 사용하지 않았다면 Test 함수가 호출될 때마다 힙에 추가로 메모리가 할당 된다.

#include <malloc.h>

void Test()
{
   short *p = (short*)malloc(100); /* 힙에 100바이트 할당 */
   /*free(p);*/
   
}
void main()
{
   int i;
   for(i=0;i<100;i++) Test(); /* 메모리가 100바이트씩 100번 동적 할당됨 */
   

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

이런 상태를 메모리가 손실되었다고 한다.

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

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

char *p;
/* p = (char*)malloc(32); 포인터 변수 p에 메모리 할당되지 x */
free(p); /* p는 할당된 메모리의 주소를 가지고 있지 않아 오류 발생 */

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

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

int data = 5;
int *p = &data; /* p는 지역 변수 data의 주소를 가지게 됨 (정적) */
free(p); /* p는 힙에 할당된 주소가 아니기 때문에 (스택) 실행 오류 발생*/

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

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

int *p = (int *)malloc(12); /* 12바이트 메모리를 힙에 할당 */
free(p); /* 할당 메모리 해제 */
free(p); /* 중복 해제, 실행 오류 */

2. 동적 메모리 할당의 장단점

힙에 동적으로 사용하는 메모리는 스택에 비해 큰 크기의 메모리를 할당할 수 있으며, 할당하고 해제하는 시점도 프로그래머가 직접 정할 수 있다.

또한 할당되는 메모리 크기도 프로그램 실행 도중 변경할 수 있다. 따라서 할당되는 메모리 크기가 변경되어도 소스 코드를 다시 컴파일하지 않아도 된다.

그러나 힙에 동적으로 메모리를 할당하고 해제하는 작업을 프로그래머가 직접 관리해야 하기 때문에 코드가 복잡해지며 작은 메모리를 할당해서 사용할 때는 오히려 비효율적이다.

정적 할당은 스택에 1바이트만 할당된다. 
동적 할당은 스택 대신 힙에 1바이트를 할당하며, 포인터 (4바이트)가 기본 추가
할당되기 때문에 총 5바이트가 필요하다.

3. 동적 메모리 사용하기

3.1 배열과 비슷한 형식으로 동적 메모리 사용하기

1, 2바이트처럼 크기가 작은 데이터 여러 개를 동적으로 할당해서 사용하는 것은 번거롭고 불편할 수 있다.

동적 할당도 메모리를 배열처럼 그룹으로 묶어서 많이 사용한다.

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

결국 이런 형식으로 메모리를 동적 할당하면 int 형으로 그룹지어진 메모리를 사용하는 것과 같기 때문에

int data[3];

과 같이 배열과 같은 목적으로 사용할 수 있다.

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

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

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

malloc(12)라고만 적는 것보다 malloc(sizeof(int)*3)이라고 적으면 12바이트를 4바이트(int)단위로 3번 나누어 사용해야겠다는 의도까지 쉽게 파악할 수 있다.

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

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

결국 short형 변수 6개로 이루어진 배열과 같이 사용할 수 있다.

short data[6];

포인터와 동적 할당 문법을 사용하면 배열과 같은 목적으로 사용할 수 있는 메모리를 구성할 수 있다.

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

배열을 사용하면 메모리가 스택에 정적으로 할당되기 때문에 항목의 개수를 상수로만 할당할 수 있다.

int data_size = 3;
int data[data_size]; /* 배열의 요소 개수는 항상 상수여야함 */
/* 오류 발생 */

즉 배열의 크기는 상수로만 적을 수 있기 때문에 자신이 사용할 데이터의 최대 개수에 반드시 신경을 써야 한다.

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

그러나 여기서 사용자가 10개의 숫자를 입력해야 한다고 요청하면 #define을 이용해서 다시 변경하고 코드를 수정해야 한다.

이렇게 정적 메모리 할당을 사용하면 코드를 직접적으로 바꿔야 하는 상황도 생기고 너무 크게 메모리를 배정해도 낭비되는 메모리들 때문에 비효율 적이다.

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

이러한 낭비를 막기 위해 동적 메모리 할당을 사용한다. 배열과 달리 malloc은 함수이기 때문에 할당할 크기를 적을 때 상수뿐만 아니라 변수도 사용할 수 있다.

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

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

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

profile
프로그래밍 독학

0개의 댓글