Jasontreks Blog

DM 보내기


Send

동적 할당

C언어의 화룡점정이라고 할 수 있는 동적 메모리 관리는 거의 모든 형태의 자료 구조에 반드시 필요한 기본 개념이다. 어떠한 데이터가 필요한 순간에만 메모리 공간을 확보하고, 필요가 없어지면 그 공간을 해제하는 것이 기본 원리인데, 이러한 작업을 개발자가 직접 제어할 수 있다는것은 C언어가 가진 매우 큰 메리트임과 동시에 높은 난이도를 느끼게 하는 진입 장벽이기도 하다.

동적 할당이 아닌 것

우선 동적으로 메모리를 할당하는 과정을 설명하기 이전에, '동적이지 않은 할당'이 무엇인지를 확실하게 짚고 넘어가야 한다. 동적이지 않다는 것은 컴파일 단계에서 그 크기가 미리 정해진다는것을 의미한다. 다음과 같이 일반적인 형태로 선언된 변수들이 이에 해당한다.

int main(void)
{
	int i = 0; // 정수 1개를 위한 크기가 미리 결정
	int iArr[10] = {0,}; // 정수 10개를 위한 크기가 미리 결정
	
	return 0;
}

동적으로 크기가 할당되려면 아마 다음과 같은 그림이 되어야 할 것이다.

int main(void)
{
	int size;
	scanf("%d", &size); // 사용자가 원하는 크기를 입력

	int intArr[size] = {0,}; // 입력한 크기만큼 동적으로 할당
	
	return 0;
}

하지만 이 코드는 틀렸다. 일반적인 선언 문법으로는 동적 할당이 불가능하기 때문이다. 따라서 동적 할당을 이용하기 위해서는 기본적으로 malloc 함수를 이해해야 한다. 이 함수는 stdlib.h 헤더에 있다.

malloc(memory allocate)

  • 인수: 크기
  • 반환값: 포인터

원리는 매우 간단하다. 우리가 원하는 크기를 인수로 넘기면 함수는 그 크기에 해당하는 메모리 공간의 주소를 포인터 변수로 반환한다. 다만 이 함수의 사용법은 여기서 끝이 아니다. 다음 두 작업이 추가로 필요하다.

  1. 크기는 (자료형의 바이트 수 * 요소의 개수)로 넘겨야 한다. 보통 sizeof 연산자와 곱셈 연산자를 사용한다. 단일 변수의 경우 요소 개수는 생략해도 좋지만, 배열을 할당할 경우 타입 크기와 배열의 길이를 곱해 넘겨야 한다.
  2. malloc의 반환값은 void* 타입이다. 이것을 형 변환을 통해 원하는 타입의 포인터로 바꿔야 한다. 그래야 그 타입으로서 데이터에 접근하는 것이 가능해진다.

예를 들어 1개의 int형 자료에 대한 동적 할당은 다음과 같다.

#include <stdlib.h>

int main(void)
{
	int* i = (int*)malloc(sizeof(int) * 1); // 할당
	free(i); // 해제(매우 중요)
	return 0;
}

할당된 메모리를 더이상 사용하지 않을 때 free 함수를 호출하여 해제하는 것은 매우 중요하다.

calloc(contiguous allocate)

calloc 함수는 malloc을 좀더 직관적이고 안정적으로 수정한 함수이다. 요소의 개수를 타입의 크기에 곱하는 것이 아닌 인수에 추가로 전달한다는 점, 할당 후 모든 요소가 0으로 초기화된다는 점에서 malloc과 차이가 있다.

#include <stdlib.h>

int main(void)
{
	int* i = (int*)calloc(1, sizeof(int)); // i의 실제 값은 0
	free(i);
	return 0;
}

reaclloc(re allocate)

realloc 함수는 동적으로 할당된 메모리의 크기를 변경하는 함수이다.

  • 인수1: 할당된 메모리에 대한 포인터
  • 인수2: 새로운 크기
  • 반환값: 크기가 변경된 메모리 주소

이 때 기존의 데이터는 유지하게 되는데, 크기를 줄일 경우 마지막 인덱스부터 줄어든 크기만큼의 데이터는 날아가게 된다.

#include <stdlib.h>

int main(void)
{
	int* i = (int*)calloc(10, sizeof(int)); // int형에 대한 길이 10 배열을 동적 할당
	i = (int*)realloc(i, sizeof(int) * 20); // 동적으로 할당된 배열을 길이 20으로 증가
	i = (int*)realloc(i, sizeof(int) * 5);  // 길이 5로 축소 (뒤쪽 15개 값 사라짐)
	free(i);
	return 0;
}
	

원하는 길이의 배열 만들기

앞서 배운 동적 할당을 이용해 컴파일 이후에도 원하는 길이의 배열을 만드는 프로그램을 작성할 수 있게 되었다.

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

int main(void)
{
	int size;
	scanf("%d", &size);

	int* arr = (int*)calloc(size, sizeof(int));
	free(arr);

	return 0;
}

동적으로 할당한 배열의 위험성 일반적으로 선언한 배열의 경우, 배열의 길이를 초과하는 메모리 주소에 접근하려고 할 때 컴파일 에러가 발생한다. 그러나 동적으로 할당한 배열은 할당된 범위를 초과하는 메모리 주소에 접근하는 것을 컴파일러가 막지 못한다. 이것을 오버런(Overrun) 현상이라고 한다. 일반 배열 변수는 선언에 포함된 미리 정해진 길이를 컴파일러가 확인하고 불미스러운 접근을 제어할 수 있지만, 동적으로 할당된 배열은 그 길이를 컴파일 단계에서 확인이 불가능하기 때문에 이러한 현상이 일어나게 된다. 따라서 런타임에도 속성값과 조건문을 통해 메모리 접근을 안전하게 제어할 수 있도록 자료구조를 설계하는 작업은 매우 중요하다.