일반적으로 C언어에서 변수나 배열을 선언하면, 해당 코드블록이 끝나서 범위를 벗어날때 까지 메모리를 차지하게 됩니다. 하지만 이렇게 되면 메모리 공간의 낭비가 발생할 수 있죠. 필요할 때만 메모리에 변수를 저장하기 위한 공간을 할당해서 사용하는 것이 해결책이 될 수 있고, 이를 동적 메모리 할당 (dynamic memory allocation)이라고도 부릅니다.
동적 메모리 할당을 위해서는 먼저 포인터라고 하는 변수가 필요한데요. 메모리 상에서의 주소를 저장하기 위한 변수인 포인터의 개념이 생소하게 느껴지는 분들에게는 다음 포스팅이 큰 도움이 되리라 생각합니다.
malloc, free 함수
동적 메모리 할당을 위해서 malloc 과 free 함수를 사용할 수 있고, 이들을 호출하려면 stdlib.h 헤더 파일을 포함시켜야 합니다. 이 함수들은 포인터 변수를 다루고 있으며, 할당된 공간의 맨 첫부분의 메모리 주소가 됩니다. 예를 들어서 정수형 변수 10개를 위한 공간을 할당받아서 사용하고 싶다면 다음과 같은 방식을 사용할 수 있습니다.
int *ptr_array;
ptr_array =
(int *)malloc(10 * sizeof(int));
/* ptr_array 에 할당된 변수 사용
* 예시 : ptr_array[i] = i */
for (int i = 0; i < 10; i++) {
ptr_array[i] = i;
}
free(ptr_array);
메모리 할당을 위한 malloc 함수는 할당받고자 하는 메모리 공간의 크기를 인자로 받고 있는데요. 여기서는 10개의 정수형 변수가 들어갈 만큼의 공간을 필요로 하기 때문에, 정수형 변수의 크기를 알기 위해 sizeof 함수를 호출하고 10을 곱했습니다. 그렇게 해서 메모리 할당이 제대로 되면, malloc 함수는 할당된 공간의 메모리 주소를 리턴합니다. 위에 나온 예시에서는 그 주소가 정수형 포인터 변수인 ptr_array에 저장된 것을 볼 수 있죠. 만약 물리적 메모리가 부족하다거나 하는 등의 이유로 메모리 할당이 제대로 되지 않았다면, malloc 함수는 가리키는 대상이 없는 널 (NULL)포인터를 리턴합니다.
이제 malloc 함수로부터 리턴 받은 메모리 주소를 통해, 할당된 공간의 변수들의 값을 바꾸거나 이용할 수 있습니다. 접근하고자 하는 변수의 번호 또는 인덱스를 대괄호 ([ ], square bracket)안에다가 넣고 포인터 변수 이름 뒤에다가 붙이면 되는데요. 이는 사실 배열에 저장된 변수에 접근하는 방식과 동일한 것입니다. 위의 예시에서처럼 포인터 변수의 이름이 ptr_array라면, i-번째 변수의 값은 ptr_array[i] 가 됩니다. N개의 변수가 들어갈 수 있는 공간을 할당받았다면 i는 0 부터 N-1 까지의 값을 가질 수 있습니다. 만약 이 범위를 벗어나서 할당받지도 않은 공간에 접근하려 들면, segmentation fault 또는 런타임 오류가 뜨게 되겠죠.
한 가지 더 짚고 넘어갈 점이라면, malloc 함수는 void 형 포인터를 리턴한다는 점인데요. 그렇기 때문에 저장하고자 하는 변수의 자료형에 걸맞게 형변환을 해서 문제의 소지를 줄일 필요가 있습니다. 만약 정수형 변수라면 (int *), 문자 변수라면 (char *) 등을 malloc 함수 앞에 붙여서, 리턴이 되자마자 형변환을 해 주면 되겠습니다.
마지막으로, 할당받은 메모리 공간을 더 사용할 필요가 없어졌다면 free 함수를 호출하여 이들을 풀어줘야 합니다. 이를 제때 해제하지 않고, 이런 것들이 쌓이다 보면 소위 말하는 메모리 누수 (memory leak)가 되겠죠.
어떤 함수가 행렬이나 텐서 등을 변수로 받아야 할 때가 있는데, 다중배열만 가지고는 이걸 구현하기가 쉽지 않기 때문에 동적 메모리 할당이 해결책이 될 수 있습니다. 위에 나온 방법을 쓰면, 행의 갯수가 nrow 이고 열의 갯수가 ncolumn 인 실수 행렬을 받는 함수를
void function(double **ptr,
int nrow, int ncolumn) {
// ... 함수의 내용 ...
}
같은 형식으로 쉽게 구현이 가능하니까 말이죠.
realloc 함수
이름에서 유추할 수 있듯이 realloc 함수는 이미 할당받았던 메모리에 저장된 변수들의 값들을 유지하면서, 다른 크기의 공간을 재할당 받는 데 사용됩니다. 이 함수의 프로토타입을 살펴보면 다음과 같습니다.
/* ptr : 기존에 할당받았던 메모리 공간의 주소
* s : 할당받고자 하는 메모리 공간의 크기
* 새로 할당된 공간의 메모리 주소를 리턴 */
void* realloc(void *ptr, size_t s);
예전에 malloc 함수 등을 통해서 할당받은 메모리 공간의 주소와 새롭게 할당받고자 하는 공간의 크기를 매개변수로 받고 있습니다. malloc 함수와 비슷하게 새로 할당된 공간의 메모리 주소를 리턴합니다. 참고로 NULL 포인터를 인자로 주게 되면, realloc 함수는 malloc 함수와 동일한 방식으로 작동하는데요. 이말인즉슨 메모리에 새롭게 공간을 할당하고 그 주소를 리턴한다는 뜻입니다.
예시를 통해 realloc 함수를 좀 더 자세히 살펴봅시다.
#include<stdio.h>
#include<stdlib.h>
int main(int argc, char *argv[]) {
int size_now;
int *ptr_array_old;
int *ptr_array_new;
size_now = 10;
ptr_array_old = NULL;
ptr_array_new =
(int *)realloc(ptr_array_old,
size_now * sizeof(int));
fprintf(stdout,
" 1 : size = %d\n", size_now);
fprintf(stdout,
" ptr_array_new =");
for (int i = 0; i < size_now; i++) {
ptr_array_new[i] = i + 1;
fprintf(stdout,
" %d", ptr_array_new[i]);
}
fprintf(stdout, "\n");
size_now = 15;
ptr_array_old = ptr_array_new;
ptr_array_new =
(int *)realloc(ptr_array_old,
size_now * sizeof(int));
for (int i = 10; i < size_now; i++) {
ptr_array_new[i] = i + 1;
}
fprintf(stdout,
" 2 : size = %d\n", size_now);
fprintf(stdout,
" ptr_array_new =");
for (int i = 0; i < size_now; i++) {
fprintf(stdout,
" %d", ptr_array_new[i]);
}
fprintf(stdout, "\n");
size_now = 5;
ptr_array_old = ptr_array_new;
ptr_array_new =
(int *)realloc(ptr_array_old,
size_now * sizeof(int));
fprintf(stdout,
" 3 : size = %d\n", size_now);
fprintf(stdout,
" ptr_array_new =");
for (int i = 0; i < size_now; i++) {
fprintf(stdout,
" %d", ptr_array_new[i]);
}
fprintf(stdout, "\n");
free(ptr_array_new);
return 0;
}
첫번째 단계에서는 정수형 변수가 10개 들어갈 수 있는 크기의 공간을 할당받았는데, 이 때는 realloc 함수의 인자로 NULL 포인터가 들어갔으므로 새로운 공간을 할당받게 됩니다. 그리고 10개의 정수형 변수의 값을 정하고 출력하게 되죠.
두번째 단계에서는 할당받은 공간의 크기를 15로 증가시켰는데요. 기존에 할당받았던 공간에 자리잡고 있던 10개 변수의 값들은 그대로 유지되었습니다. 반면에 새로 확보된 공간에 있는 5개의 변수들에는 새로 값을 정의해 줘야 합니다. 그렇지 않으면 소위 쓰레기 값들이 들어가게 되고, 이는 잘못하면 버그로 이어질 수 있습니다.
세번째 단계에서는 할당받은 공간의 크기를 5로 줄였습니다. 그 결과 맨 처음 5개 변수들의 값은 유지된 반면에, 나머지는 다 사라지게 되었죠. 마지막으로는 free 함수를 호출하여 할당받은 메모리 공간을 해제합니다.
위의 예제 프로그램을 실행시켜 보면 다음과 같은 결과를 얻을 수 있을 것입니다.
C++ 프로그램의 경우, new 와 delete 키워드들을 이용하면 메모리를 동적으로 관리하는 것이 가능합니다. 이 방법에 대한 자세한 사항은 다음 포스팅에 소개되어 있습니다.
여기서는 변수의 포인터를 언급했지만, 포인터의 포인터를 가지고 동적인 메모리 할당을 하면 포인터의 배열을 만드는 것도 가능합니다. 이는 함수의 포인터에 대해서도 마찬가지이므로, 수치해석에서 유용하게 활용할 수 있죠. 대표적인 예시는 다음 포스팅에 소개되어 있습니다.
같이 알고 있으면 좋은 C/C++ 팁들
명령행 인자
함수의 포인터를 인자로 사용하기
포트란과의 하이브리드
라이브러리 만들기