본문 바로가기

Other Programming/C C++

메모리 할당 및 해제 / 재 할당

반응형
출처 : winapi.co.kr

void *malloc(size_t size );

void free(void *memblock );




먼저 malloc(엠얼록이라고 읽는다) 함수부터 알아보자. 인수로 필요한 메모리양을 바이트 단위로 전달하면 요청한만큼 할당한다. size_t는 메모리의 양을 나타내는 단위인데 _t로 끝나는 사용자 정의 타입은 표준에 의해 반드시 정의하도록 되어 있으므로 기본 타입과 거의 대등한 자격을 가진다. 플랫폼에 따라 다르게 정의되어 있는데 대부분의 32비트 컴파일러들은 size_t를 unsigned의 부호없는 정수형으로 정의한다. 따라서 이 함수로 할당할 수 있는 이론적 최대 용량은 4G 바이트라고 할 수 있다.

10바이트가 필요하면 malloc(10)이라고 호출하고 1000바이트가 필요하면 malloc(1000)이라고 호출하면 된다. 실행중에 할당하는 것이므로 malloc(Num)과 같이 변수도 사용할 수 있다. malloc은 응용 프로그램이 필요로하는 양만큼 운영체제에게 할당을 요청하며 운영체제는 사용되지 않는 빈 영역(힙)을 찾아 요청한만큼 메모리를 할당하여 그 시작 번지를 리턴한다. 응용 프로그램이 할당한 메모리를 어떤 목적에 사용할지는 알 수 없으므로 malloc은 void *형을 리턴하며 받는 쪽에서는 원하는 타입으로 캐스팅해야 한다.

free 함수는 동적으로 할당한 메모리를 해제한다. 응용 프로그램은 메모리를 다 사용한 후에 반드시 free 함수를 호출하여 메모리를 해제해야 한다. 그래야 이 영역이 다른 프로그램을 위해 재활용될 수 있다. 다음 코드는 정수형 변수 10개를 담을 수 있는 메모리를 할당하는 예이다.

 
int *ar;

ar=(int *)malloc(10*sizeof(int));

// ar 사용

free(ar);


동적으로 할당된 메모리를 사용하려면 그 시작 번지를 기억해야 하므로 포인터 변수가 필요하다. 이 경우 정수형 변수를 위한 메모리를 할당하는 것이므로 정수형 포인터 ar을 선언했다. malloc을 호출할 때는 필요한 메모리양을 바이트 단위로 전달하는데 정수형 변수 10개를 담을 수 있는 메모리의 총 크기는 10*sizeof(int), 즉 40바이트여야 한다. int가 항상 4바이트라는 보장이 없으므로 여기에도 반드시 sizeof 연산자로 크기를 계산해야 한다.

malloc은 인수로 전달된 크기만큼의 메모리를 할당하고 그 시작 번지를 리턴하되 리턴 타입이 void *형이므로 이 포인터를 변수에 대입할 때는 반드시 원하는 타입으로 캐스팅할 필요가 있다. 물론 void *형 변수로 받을 수도 있는데 이렇게 하면 받을 때는 편하지만 쓸 때마다 캐스팅해야 하므로 더 불편하다. 정수형 포인터에 대입해야 하므로 (int *)로 캐스팅했다. 이렇게 할당하면 ar 번지 이후 40바이트를 응용 프로그램이 배타적으로 소유하게 된다.

즉 포인터 ar이 가리키는 번지는 마치 ar[10] 정수형 배열과 같아지며 메모리내에서의 실제 모양과 용도, 적용되는 문법도 배열과 동일하다. ar 번지 이후의 40바이트는 다른 목적에 사용되지 않으므로 마치 정적 할당된 배열처럼 이 메모리를 활용할 수 있다. 물론 다 사용하고 난 후에는 반드시 free 함수로 메모리를 해제해야 한다. 그럼 동적 할당의 실제 예를 보도록 하자.




배열을 공부할 때 만들어 본 적이 있는 성적 처리 프로그램이다. 차이점이라면 학생 수가 미리 고정되어 있지 않고 실행 직후에 입력받은 학생수만큼의 성적을 처리할 수 있다는 점이다. 1명이든 100명이든 또는 10만명이든 메모리 한계까지 성적을 처리할 수 있다. 실행 결과는 다음과 같다.

 

학생수를 입력하세요 : 3

1번 학생의 성적을 입력하세요 : 55

2번 학생의 성적을 입력하세요 : 77

3번 학생의 성적을 입력하세요 : 88

 

총점은 220점이고 평균은 73점입니다.



arScore[i] 연산식으로 i번째 요소를 자유롭게 읽고 쓸 수 있으며 해제하기 전까지 이 메모리는 다른 응용 프로그램이 건드릴 수 없는 독점적인 공간이 되는 것이다. 모든 작업이 끝나면 free 함수로 할당된 메모리를 해제해야 한다

동적 할당이란 사실 굉장히 간단한 것이다. 필요한만큼 malloc으로 할당해서 쓰다가 다 쓰고 나면 free로 해제하기만 하면 된다.



다음은 malloc과 free 함수에 대한 참고 사항이다. malloc 함수는 할당에 실패하면 에러의 표시로 NULL을 리턴하며 그래서 이 함수를 호출할 때는 위 예제처럼 malloc이 리턴한 번지를 반드시 점검하는 것이 원칙이다. 메모리가 부족한 상황은 언제든지 발생할 수 있고 만약 이 점검을 하지 않으면 0번지를 액세스할 위험이 있다. 제대로 만든 프로그램은 어떠한 극한 상황에서도 최소한 죽지는 말아야 한다.

그러나 32비트 환경은 메모리가 충분할 뿐만 아니라 운영체제의 메모리 관리 기법이 정교해져서 작은 메모리를 할당할 때는 에러 점검을 생략해도 큰 무리가 없다. 얼마 정도가 작은지에 대한 명확한 기준은 없지만 일반적으로 메가 단위 이상을 할당할 때는 꼭 점검해야 하며 수십~수백 바이트 정도는 굳이 점검하지 않아도 상관없다. 메모리 부족 사태는 이제 응용 프로그램만의 책임이 아니다. 만약 malloc(100) 호출이 실패하는 상황이 발생한다면 응용 프로그램이 다운되기 전에 운영체제가 먼저 이 상황을 처리하도록 되어 있다. 에러 점검 코드를 일일이 작성한다면 안전성이나 이식성면에서 더 좋기는 하겠지만 그만큼 실행 속도가 느려지고 크기도 늘어나는 반대 급부가 있고 운영체제의 무리한 메모리 확장 시도로 인해 다운되는 것보다 더 치명적인 상태가 될 위험도 있다.

다음은 할당된 메모리를 해제하지 않았을 때의 문제점에 대해 알아보자. malloc으로 할당만 하고 free를 하지 않으면 메모리 관리 원칙상 이 메모리는 시스템을 재부팅하기 전에는 다른 응용 프로그램이 사용하지 못한다. 그래서 16비트 환경에서 해제를 하지 않으면 시스템 메모리가 감소되며 할당된 채로 남아 있는 영역에 의해 메모리가 조각난다. 이렇게 되면 다음 프로그램이 실행될 충분히 큰 공간이 없어 시스템 다운으로 이어지기도 한다.

그러나 32비트 운영체제는 메모리를 할당한 프로그램이 종료되면 해제하지 않은 메모리를 알아서 회수하도록 되어 있으므로 16비트에서처럼 큰 문제가 되지는 않는다. 그렇다고 해서 할당만 해 놓고 해제하지 않는 것이 좋다거나 그래도 괜찮다는 얘기는 절대로 아니다. 다만 16비트 환경보다는 문제가 덜하다는 것뿐이지 할당 후 해제하는 것은 프로그래밍의 대원칙이다. malloc후에 항상 free하는 것을 잊지 말아야 하며 malloc 코드를 칠 때 아래 쪽에 free를 먼저 입력해 놓고 다음 작업을 하는 습관을 들이는 것이 좋다.




출처 : winapi.co.kr




 
#include 

void main()
{
	int *arScore;
	int i, stNum;
	int sum;

	printf("학생수를 입력하세요 : ");
	scanf("%d" , &stNum);

	arScore = (int *)malloc(stNum*sizeof(int));
	if(arScore == NULL)
	{
		printf("메모리가 부족합니다.\n");
		exit(0);
	}

	for(i=0; i < stNum ; i++)
	{
		printf("%d번 학생의 성적을 입력하세요 : " , i+1);
		scanf("%d", &arScore[i]);
	}

	sum = 0;
	for(i=0;  i< stNum ; i++)
	{
		sum+=arScore[i];
	}

	sum=0;
	for(i=0;i < stNum;i++)
	{
		sum+=arScore[i];
	}

	printf("\n총점은 $d점이고 평균은 %d점입니다\n", sum, sum/stNum);
	free(arScore);

		
}

.


재 할당

void *calloc( size_t num, size_t size );

 

첫 번째 인수 num은 할당할 요소의 개수이고 size는 요소의 크기이다. malloc은 필요한 메모리를 바이트 단위 하나로만 전달받지만 calloc은 두 개의 값으로 나누어 전달받는다는 점이 다르다. malloc이 "몇 바이트 할당해 주세요"라고 요청하는 것에 비해 calloc은 "몇 바이트짜리 몇 개 할당해 주세요"라고 요청하는 것이다. 그래서 다음 두 호출문은 동일하다.

 

ar=(int *)malloc(10*sizeof(int));

ar=(int *)calloc(10,sizeof(int));



구조체같은 큰 데이터의 배열을 할당할 때는 calloc으로 할당하는 것이 더 보기에 좋고 코드를 읽기에도 좋다. calloc도 실제 할당하는 양은 size*num으로 계산하므로 사실 calloc(1,10*sizeof(int))나 calloc(10*sizeof(int),1)이나 결과는 동일하다. 다만 필요한 메모리양을 단위와 개수로 나누어 좀 더 논리적으로 표현한다는 점만 다르다.

calloc이 malloc과 또 다른 차이점은 메모리 할당 후 전부 0으로 초기화한다는 것이다. malloc은 메모리 할당만 하므로 할당된 메모리에는 쓰레기값이 들어 있지만 calloc으로 할당하면 할당 후 모든 메모리를 0으로 채운다. 할당 후에 배열을 바로 초기화해야 한다면 malloc 호출 후 memset을 사용할 수도 있지만 이 방법보다는 calloc 함수로 할당하는 것이 더 편리하다

다음 함수는 이미 할당된 메모리의 크기를 바꾸어 재할당한다. 최초 할당한 크기보다 더 큰 메모리가 필요할 때는 이 함수로 크기를 조정할 수 있다. 원래 크기보다 더 작게 축소 재할당하는 것도 가능하기는 하지만 보통은 확대 재할당하는 경우가 많다


void *realloc( void *memblock, size_t size );

 

첫 번째 인수로 malloc이나 calloc으로 할당한 메모리의 시작 번지를 주고 두 번째 인수로 재할당할 크기를 전달한다. 만약 첫 번째 인수가 NULL일 경우, 즉 할당되어 있지 않을 경우는 새로 메모리를 할당하므로 realloc의 동작은 malloc과 같아진다. 두 번째 인수 size가 0일 경우는 할당을 취소하라는 얘기이므로 free와 같아진다. 재할당 후에 새로 할당된 메모리의 번지를 리턴하는데 이 번지는 원래 번지와 같을 수도 있고 아닐 수도 있다. 일반적으로 축소 재할당했을 때는 같은 번지이며 확대 재할당했을 때는 다른 번지로 이동될 확률이 높다.

예를 들어 ar 배열이 20바이트만큼 할당되어 있는 상태에서 40바이트로 확대 재할당한다고 해 보자. 만약 ar뒤쪽의 메모리가 비어 있다면 ar을 40바이트로 늘리기만 하면 되므로 ar은 그 자리에서 길이만 늘어난다. 그러나 ar다음의 메모리가 비어 있지 않다면 ar의 위치가 바뀌게 될 것이다.


ar 다음에 다른 변수가 이미 메모리를 차지하고 있으면 이 상태에서 ar 배열의 길이를 늘릴 수 없기 때문에 어쩔 수 없이 ar이 확대된 크기만큼의 여유 메모리가 있는 쪽으로 이사가야 한다. 이때 realloc 함수는 ar을 이동시키면서 기존 ar에 들어 있던 모든 내용을 그대로 복사하므로 재할당에 의해 위치는 바뀌더라도 내용은 그대로 유지된다. 다음 예제는 realloc 함수의 간단한 사용예이다.




#include 

void main()
{
     int *ar;
     ar=(int *)malloc(5*sizeof(int));

     ar[4]=1234;
     ar=(int *)realloc(ar,10*sizeof(int));

     ar[9]=5678;
     printf("ar[4]=%d, ar[9]=%d\n",ar[4],ar[9]);

     free(ar);
}

 



최초 ar 배열을 20바이트 크기로 할당했으므로 ar은 ar[0]~ar[4]까지의 요소를 가지게 될 것이다. 이 상태에서 어떤 이유로 ar이 40바이트로 확장되어야 할 필요가 생겼다면 realloc 함수로 ar의 크기를 늘려 재할당한다. 크기를 확장하면 ar의 번지가 바뀔 수는 있지만 원래 배열에 들어 있던 모든 값은 그대로 유지된다.

동적 할당이 필요한 이유는 컴파일할 시점에 필요한 메모리양을 모를 때가 있기 때문이다. 재할당이 필요한 이유는 실행중에라도 필요한 메모리양을 가늠할 수 없을 때가 있기 때문이다. 그리 흔하지는 않지만 재할당이 꼭 필요한 경우가 있다. 어떤 경우에 재할당이 필요한지 구체적인 예제를 보이기는 어렵고 예만 들어 보도록 하자.

네트워크를 통해 파일을 전송하는 프로그램을 작성한다고 해보자. 네트워크의 반대편에서 보내는 파일을 받아야 하는데 이 파일의 크기는 다 받아 보기 전에는 알 수 없는 상황이다. 이럴 때는 최초 적당한 크기로 버퍼를 할당한다. 가령 1M 정도만 할당한 채로 네트워크로 들어오는 패킷을 이 버퍼에 누적시킨다. 그러다가 받은 패킷 총량이 1M가 되면 다시 1M 더 늘려 2M로 재할당한다. 이런 식으로 패킷을 다 받을 때까지 계속 재할당하면 된다. 이런 상황은 생각보다 훨씬 더 자주 발생하는데 압축을 해제한다거나 DB 쿼리를 실행할 때도 재할당이 필요하다.

참고로 다음 확장 함수를 사용하면 malloc, calloc으로 할당한 메모리의 크기를 실행중에 조사할 수 있다. 할당한 메모리가 충분한지를 조사하고 싶을 때 이 함수가 유용하게 사용된다. 표준 함수는 아니지만 비주얼 C++, Dev-C++ 등 웬만한 컴파일러들은 이 함수를 제공하므로 이식성에 대해서는 걱정하지 않아도 된다.

 

size_t  _msize(void *memblock);

 

메모리를 동적으로 할당하는 여러 함수들에 대해 알아보았는데 C 언어의 기본적인 메모리 할당, 해제 함수는 malloc, free이다. 이에 비해 C++은 malloc, free 대신 사용할 수 있는 new, delete라는 할당 연산자를 따로 제공하는데 이 연산자들은 단순히 메모리를 할당하기만 하는 것이 아니라 객체의 생성자와 파괴자를 호출하기도 한다. 이 연산자에 대해서는 C++ 편에서 다시 공부하기로 하자.

물론 C++에서도 객체를 생성할 때가 아니라면 malloc, free 함수를 여전히 사용할 수 있으며 new, delete보다 오히려 더 간편하다. 메모리 할당 함수는 이 외에도 운영체제와 라이브러리별로 다양하게 제공된다. 예를 들어 윈도우즈에서는 GlobalAlloc, VirtualAlloc이라는 별도의 할당 함수가 있고 COM에서는 IMalloc이라는 메모리 관리 전용 인터페이스를 제공하기도 한다. 이런 할당 방법에 대해서는 관련 과목을 공부할 때 다시 살펴보되 할당의 원칙과 방법은 malloc, free의 경우와 거의 동일하다.