본문 바로가기

C언어 자습/혼자공부하는 C언어 (저자 서현우)

메모리 동적 할당 (malloc, free)

프로그래머가 사전에 사용자의 동작을 예상해서 변수나, 배열선언을 통해 메모리 공간을 활용하는것을 배워왔습니다.

STACK 영역을 사용하는 거죠. 운영체제가 알아서 메모리를 관리합니다. 대표적으로 함수 내의 지역변수가 있겠죠. 호출시 메모리를 할당 했다가 호출이 끝나면 자동으로 알아서 해제 합니다.

사용자의 활동범위를 전부 예상하는것이 가능 하다면 STACK 메모리만 활용해서 모든 동작이 문제없이 동작할 것입니다.

그렇다면 좋겠지만 프로그램이 실행중인 와중에 저장공간 할당이 필요할 수도 있습니다.

 

HEAP메모리 영역에는 이렇게 프로그램 실행중에도 저장공간을 할당할 수가 있어요.

할당한 메모리를 다시 사용하기 위해서는 반납해줘야 합니다.

이런 행위를 동적할당이라고 합니다.

 

malloc, free 함수

malloc의 어원을 살펴 보겠습니다.

memory allocation 의 약어입니다.

memory = 메모리

allocation = 할당

단어 뜻만 풀어도 의미가 보이죠?

예제로 살펴 보겠습니다.

#include<stdio.h>
#include<stdlib.h>

int main(void)
{
	int* pi;
	double* pd;

	pi = (int*)malloc(sizeof(int));
	if (pi == NULL)
	{
		printf("#메모리가 부족합니다. \n");
		exit(1);
	}
	pd = (double*)malloc(sizeof(double));
	*pi = 10;
	*pd = 3.4;

	printf("정수형으로 사용: %d\n", *pi);
	printf("실수형으로 사용 : %.1lf\n", *pd);

	free(pi);
	free(pd);

	return 0;
}

 

malloc 이 쓰인 문장을 한번 보겠습니다.

pi = (int*)malloc(sizeof(int));
pd = (double*)malloc(sizeof(double));

익숙한 연산자가 2개 보입니다.

형변한 연산자죠? (int*), (double*)

각각 int, double을 가리키는 포인터로 형변환을 해주겠다는 말입니다. 포인터 형변환 연산자는 void 포인터에서 배웠습니다.void 포인터 (tistory.com)

메모리 주소는 다같은 메모리 주소인데 주소가 참조하고 있는 값을 불러올 떄 어떤 형식으로 읽어야 할 지를 정해주는것이었습니다.

번역해보자면 각각 int, double 형태인 값을 참조하겠다. 라는 이야기죠.

이어서 살펴보겠습니다. malloc함수를 호출하고, Parameter로 sizeof 연산자를 사용해서 정수값을 전달 했군요.

sizeof 연산자는 바이트 수를 정수형태로 반환하지요? (32비트 운영체제 기준)

pi = (int*)malloc(4)

pd = (double*)malloc(8) 이랑 다를바 없는 문장이란 거죠

실제로 코드를 저렇게 바꿔서 작성해도 문제없이 동작하는것을 볼 수가 있습니다.

 

malloc 함수는 값을 반환하는 call by value 함수입니다.주소값을 반환합니다. 전달된 정수형 Parameter 바이트 만큼의 HEAP 영역에 할당한 다음 주소를, 그 시작 주소를 반환하는 것이죠.malloc 함수가 반환하는 주소는 말 그대로 주소만 반환하는 것이기 때문에 자료형이 void* 입니다. 보이드 포인터를 반환하는 것이죠., 따라서 프로그래머가 사용할 용도에 따라 포인터 형변환을 통해서 사용해야 합니다.그것이 (int*), (double*) 형태로 쓰인 것이죠.


동적할당시 주의점

 

malloc 함수의 반환값이 NULL Pointer 인지 반드시 확인하고 사용해야 합니다.메모리 할당 함수는 원하는 크기의 공간을 할당하지 못하면  NULL을 반환하게 되어있습니다.이 NULL 값은 전처리 단계에서 0으로 바뀌기 때문에 0과 같다고 생각해도 됩니다.여기서의 NULL은 포인터의 특별한 상태를 나타내기 위해 사용되기 때문에 간접 참조 연산을 할 수 없습니다.malloc 함수가 NULL  Pointer 를 반환하는 경우 해당 값을 참조 하게되면 프로그램이 비정상 종료되게 됩니다.

 

이 문제는 프로그램이 실행 될 때, 메모리의 상태에 따라 달라집니다. 메모리가 언제 부족하게 될지 알 수가 없기 때문에 동적할당을 계획하고 있다면 반환값을 검사하는 과정이 꼭 필요합니다.

 

이해를 돕기위한 디버깅 과정입니다.

 

32비트 운영체제이고, 중단점을 보면 *pd에 값을 할당하기 직전의 모습이죠.

위가 pi, 아래가 pd 의 값을 주소검색 했을 때 모습입니다.

pi(위)를 보면 이미 값이 할당되어있고,

pd(아래)를 보면 값이 할당 되기 전인데

cd cd cd cd

cd cd cd cd

이런식으로 메모리가 할당되어있는것을 볼 수 있습니다. 아직 값은 없고요.

pi도 값이 할당되기 전이라면

cd cd cd cd cd

이렇게 4바이트가 할당 되어있을 겁니다.


동적할당시 주의점2

사용이 끝난 저장공간은 반드시 재활용 할 수 있도록 반환해야 합니다.

 

STACK 영역에서 사용된 저장공간은 역할을 다 하면 운영체제가 자동으로 회수합니다.하지만 HEAP영역에서는 프로그래머가 지정해주거나 프로그램이 종료 될 때까지 메모리에 계속 남아 있습니다.따라서 함수가 반환되기 전에 프로그래머가 free 함수로 직접 반환해주어야 합니다.반환 하지 않으면 메모리 누수가 생길 수 있습니다.반드시 해제 하는 습관을 들이도록 합시다.

 

 


 

동적할당 영역을 배열처럼 사용하기

많은 저장공간을 HEAP 영역에 한꺼번에 할당하여 배열처럼 사용할 수 있습니다.원하는 공간을 할당 한 후에 포인터를 배열처럼 사용하면 됩니다.배열의 이름 arr은 arr[0]의 주소값과 같다고 했었죠?결국 arr도 포인터이구요.동적으로 할당받은 포인터(주소)를 배열처럼 사용하는 예제. 아래와 같이 살펴 보았습니다.

 

 

 

#include<stdio.h>
#include<stdlib.h>

int main(void)
{
	int* pi;
	int i, sum = 0;

	pi = (int*)malloc(5 * sizeof(int));	//use malloc, get heap memory (5*4)Byte
	if (pi == NULL)	//handling exception
	{
		printf("메모리가 부족합니다.");
		exit(1);
	}
	printf("다섯명의 나이를 입력하세요 : ");	// getting input 5 times
	for (i = 0; i < 5; i++)
	{
		scanf("%d", &pi[i]);	//pointer calculation, assign data to the next address
		sum += pi[i];
	}
	printf("다섯명의 평균 나이 : %.1lf\n", (sum / 5.0));	//printing
	free(pi);	//free memory

	return 0;	
}

9번행을 보겠습니다.

pi = (int*)malloc(5 * sizeof(int));

pi라는 포인터 변수에 우항을 대입하는 과정입니다.

우항은 (int*)malloc(5 * sizeof(int)) 죠?

이것은 heap메모리에 5*4 바이트 만큼 할당 한 후 가장 첫번째 메모리의 주소값을 의미 하고, 그 주소는 int 속성을 가지고 있다는 말입니다.

어렵지 않죠?

 

16번행에서는 pi포인터를 활용해서 포인터연산을 하고 있습니다.

포인터와 배열의 관계에서 포인터 이름arr이 있을 때, arr = arr[0] 이라고 배웠습니다.

arr+1 = arr[1], arr+2 = arr[2] 인 것도 배웠죠 포인터에 정수연산을 하게 되면

그 자료형 크기만큼 건너뛴 후 다음 시작하는 메모리 주소를 의미 합니다.

이해를 돕기 위해 직접 만든 그림

여기서 자료형의 크기는 어떻게 결정 되느냐 하면

void포인터인 malloc을 사용 할 때, 포인터 형변환을 해주죠? (int*)malloc(정수형)

이때 알게 되는 겁니다. 어떤 형태로 쪼개서 배열을 만들지 말이죠.

그렇게 배열 형태 안에 인풋 데이터 값 5개가 들어가게 됨과 동시에 평균값까지 구하는 코드였습니다.