C 언어 | 고급 기능 | 동적 메모리 할당 - malloc(), free()
실행할 때까지 필요한 기억 용량을 확인할 수 없는 데이터 처리하는 경우, 컴파일 할때가 아닌 실행할 때에 시스템에서 새로운 메모리 영역을 확보해야 한다. C 언어에서 표준 함수로 malloc() 함수를 사용하여 메모리 공간을 확보하고, free() 함수으로 해제할 수 있다.
실행 시에 메모리를 할당
일반적으로 변수에 할당된 메모리 크기는 정적이었다. 예를 들어, 문자열을 포함하는 배열을 준비하는 경우에 배열 선언시에 그 크기를 지정하였다. 그러나 배열의 선언은 크기를 정수로만 지정할 수 있으며, 변수는 지정할 수 없다. 이것은 컴파일시에 프로그램이 필요로 하는 메모리 크기가 판명해야 한다는 것을 나타낸다.
그러나 이것으로는 하드웨어의 성능을 최대한 살릴 수 없다. 그 컴퓨터가 기가 바이트 이상의 메모리를 탑재하고 있었다고 해도, 컴파일시에 지정된 배열의 바이트 수가 64 킬로바이트이면 남아있는 여유 메모리를 사용할 수 없다. 물론, 응용 프로그램이 더 많은 메모리를 필요로 하지 않으면 문제가 되지 않지만 편집 소프트웨어 등은 상한이 정해지지 않는 문제가 있다.
예를 들어, 텍스트 편집기의 경우, 컴파일시에 사용자가 입력하는 데이터의 양을 결정할 수 없다. 그러나 지금까지의 방법으로는 응용 프로그램이 상한을 정하지 않으면 안되기 때문에, 편집 가능한 문자 수를 응용 프로그램이 강제하는 것이다. 이것은 좋은 소프트웨어라고 부를 수 없다. 사용자는 사용하는 컴퓨터의 성능을 충분히 발휘할 수 있는 소프트웨어를 사용하고 싶을 것이다.
그래서 런타임 시에 동적으로 메모리를 할당하는 것이 요구된다. 동적으로 메모리를 할당하는 것은 프로그램 실행 중에 동적으로 할당하는 영역을 변화시키는 것이다. 이것이 있으면 필요한 메모리를 할당할 수 있다. 원래 메모리를 소프트웨어에 제공하는 역할은 시스템이 하는 것이며, 메모리 확보 및 제어를 수행하려면 시스템이 제공하는 API를 호출할 필요가 있다. 그러나 이러한 방법은 복잡하면서도 시스템에 의존하는 코드를 작성해야 된다.
그래서 C 언어는 표준 라이브러리 메모리 할당 함수를 제공한다. 동적 메모리 할당은 stdlib.h 헤더 파일에 선언되어 있는 malloc() 함수를 사용한다. 이 함수는 지정된 크기의 공간을 확보하고 void 형 포인터를 돌려준다.
malloc() 함수
void* malloc( size_t size );
size 매개 변수는 할당하는 크기를 바이트 단위로 지정한다. malloc() 함수는 지정된 크기의 영역을 확보하고, 그 기억 영역에 대한 포인터를 반환한다. 이 void 형 포인터를 캐스팅하여 할당된 메모리 영역을 사용할 수 있다. 이렇게 동적으로 할당된 메모리 영역을 힙 공간이라고 한다.
만약 요청된 크기를 할당하면 힙 영역이 존재하지 않는 경우, malloc() 함수는 NULL을 반환한다. 메모리는 한계가 있고, 메모리가 과도하게 소비되는 경우는 동적으로 할당할 수 없기 때문에 malloc()에서 받은 포인터는 NULL 여부를 조사한 후 사용하는 것이 좋다.
코드1
#include <stdio.h>
#include <stdlib.h>
#define ALPHABET_COUNT 26
int main() {
int iCount;
char *str = (char *)malloc(ALPHABET_COUNT + 1);
for(iCount = 0 ; iCount < ALPHABET_COUNT ; iCount++)
str[iCount] = 0x41 + iCount;
str[iCount] = 0;
printf("%s\n" , str);
return 0;
}
코드1은 malloc() 함수를 사용하여 27 바이트의 저장 공간을 확보하고, 그 주소를 char 형의 포인터 str에 할당한다. 이는 str 포인터는 27의 요소를 가지는 char 형의 배열을 가리키는 것으로 해석할 수 있다. 프로그램에는 저장 공간이 할당되어 있는지를 확인하기 위해 배열에 값을 할당하고 문자열로 그것을 표시하고 있다. 0x41에서 값을 단순 증가시키고 대입하고 있기 때문에, ASCII 코드 A부터 순서대로 문자열이 표시된다.
물론, malloc() 함수으로 할당한 이상의 영역에 액세스할 수 없다. 코드1의 경우는 str 포인터에서 27 이상의 주소에 액세스하는 것을 허용하지 않는다. char 형 이외의 형식을 지정하는 경우 sizeof 연산자를 사용하여 바이트 수를 얻는 것이 좋다. int 형의 배열에 대한 포인터를 malloc() 함수를 사용하여 얻을 경우는 sizeof(int)에 요소 수를 곱하면 필요한 바이트 수를 얻을 수 있다. 예를 들어 4개의 요소를 가진 배열을 만들려면 sizeof(int) * 4
를 계산하여 필요한 바이트 수를 얻을 수 있다.
하지만 malloc()으로 할당한 메모리는 보통의 변수에는 발생하지 않는 특별한 문제가 있다. 일반 로컬 변수이면, 함수에서 제어를 벗어날 때 메모리가 해제되지만 malloc() 함수에서 할당한 메모리는 프로그램이 끝날 때까지 자동으로 해제되는 것은 아니다.
코드2
#include <stdio.h>
#include <stdlib.h>
int main() {
while(1) {
int *iPo = (int *)malloc(sizeof(int) * 0x100000);
if (iPo == NULL) {
printf("메모리가 할당되지 않았습니다.\n");
break;
}
printf("iPo = %p\n" , iPo);
}
return 0;
}
코드2는 while 루프에서 malloc() 함수를 이용하여 1048576 개의 요소를 가지는 int 형 배열을 저장하는데 필요한 메모리를 연속적으로 할당한다. 일반 로컬 변수이면 루프가 종료될 때마다 메모리가 초기화되지만 malloc()의 경우 메모리가 해제되지 않는다. malloc() 함수로 할당된 메모리는 컴파일시에 결정되는 변수와는 달리 관리 대상이 아니다. 동적 메모리 관리는 개발자에 달려있는 것이다.
코드2의 경우, malloc()를 반복 호출하여 여러번 메모리를 할당하지만 해제는 될 수는 없다. 그러나 포인터가 손실되어 있기 때문에 모두 사용할 수 없는 메모리 영역이 만들어 버린다. 이러한 메모리 유출을 메모리 누수(memory leck)라고 하고, 고급 프로그래머도 귀찮은 버그 중 하나이다.
메모리 누수는 무엇이 문제가 무엇인지는 코드2를 실행하면 알 수 있다. 메모리를 해제하지 않고 메모리를 계속 할당하여 사용할 수 물리 메모리의 용량이 점점 계속 줄고 있다. 실행하는 컴퓨터의 메모리가 적으면 메모리 잔량이 없어 malloc ()는 NULL을 반환해 버리는 것이다. 이것은 은행이 상환이 불가능한 융자처에 대출을 계속해 주어 부실 채권이 급증 원리와 비슷하다.
시스템에서 빌린 메모리 영역은 해제하는 형태로 언젠가 반환해야 한다. 그렇지 않으면 다른 프로그램과 시스템을 정상적으로 운영하는데 필요한 메모리가 손실될 수 있다. 단기간만 실행하는 응용 프로그램의 소량의 메모리 누수라면 문제가 없지만, 장기간 실행하는 서버 등의 프로그램 메모리 누수가 있는 경우 치명적이다.
그러나 불행히도 시스템은 빚쟁이처럼 “메모리를 돌려줘!“라고 경고 해주지 않는다. 따라서 할당된 메모리는 free() 함수를 사용하여 명시적으로 해제해야 한다. 메모리가 불필요하게 되면 free() 함수에 할당된 메모리에 대한 포인터를 전달한다.
free() 함수
void free( void * memblock );
memblock에는 해제하기 이전 할당된 메모리에 대한 포인터를 지정한다. 이 함수에는 malloc() 등의 메모리 할당에 대한 함수가 반환한 포인터 이외의 값이나, 이미 해제된 포인터를 전달해서는 안된다.
코드3
#include <stdio.h>
#include <stdlib.h>
int main() {
int *iPo;
while(1) {
int *iPo = (int *)malloc(sizeof(int) * 0x100000);
if (iPo == NULL) {
printf("메모리가 할당되지 않았습니다.\n");
break;
}
printf("iPo = %p\n" , iPo);
free(iPo);
}
return 0;
}
코드3은 루프의 마지막에서 free() 함수를 호출하여 할당된 메모리를 해제하고 있다. malloc() 함수와 free() 함수가 서로 대응하고 있기 때문에, 메모리 누수가 발생하지 않는다.
그러나 실전의 개발에는 이러한 의미있는 관계가 되어주는 것은 거의 없다. 메모리의 할당과 해제가 여러 함수에서 이뤄지는 등 복잡한 구조가 될 수 있다. 개발자는 어떤 타이밍에서 free() 함수를 호출 여부를 충분히 검토하여, 포인터의 소유권을 명확히 해둘 필요가 있다.