본문 바로가기
~ 2024.03/C 언어

[C 언어 기초] 동적 메모리 할당

by Monett 2023. 12. 13.
반응형

K.N.King 의 C Programming - A Modern Approach 를 공부하며 내용을 정리한 글 입니다.
현재 블럭의 내용은 작성자의 의견 혹은 생각이며, 틀린 내용이 있을 수 있습니다. 지적 감사드립니다.

동적 메모리 할당

C의 데이터 구조는 기본적으로 크기가 고정되어 있다.

예를들어, 배열 원소의 개수는 프로그램이 컴파일될 때 고정된다.

 

고정된 크기의 데이터는 문제가 될 수 있다; 프로그램을 작성할 때 크기를 선택할 수 밖에 없기 때문이다.

프로그램을 수정하고 다시 컴파일하는것 외에는 크기를 변경할 수 없다.

 

다행히도, C는 동적 메모리 할당을 제공한다.

동적 메모리 할당이란 프로그램 실행중에 메모리를 할당하는 기능이다.

동적 메모리 할당을 사용하면 필요한 만큼 데이터 구조를 늘리거나, 축소할 수 있다.

 

메모리 할당 함수

동적으로 메모리를 할당하기 위해 우리는 <stdlib.h> 헤더에 정의된 세가지 메모리 할당 함수 중 한 가지를 사용하면 된다.

  • malloc : 메모리를 할당한다. (초기화하지 않는다)
  • calloc : 메모리를 할당하고 초기화한다.
  • realloc : 이전에 할당한 메모리의 크기를 재설정한다.
malloc 은 memory allocate, calloc은 clear allocate 의 준말로 보인다.

 

세 가지 중 가장 많이 쓰이는 것은 malloc이다. 

할당한 메모리를 초기화(clear)할 필요가 없다면 calloc보다 효과적이다.

 

메모리 할당 함수를 호출하면 함수는 어떤 타입의 데이터를 메모리에 저장할 지 알 수 없다.

따라서 함수는 int 나 char 포인터 대신 void* 를 반환한다.

 

void* 의 값은 일반 포인터로, 기본적으로 메모리 주소일 뿐이다.

 

널 포인터 (Null pointer)

메모리 할당 함수가 호출되면, 메모리가 충분하지 않아 우리의 요청이 실패할 가능성이 있다.

만약 요청이 실패하면 함수는 널 포인터를 반환한다.

 

널 포인터는 "아무것도 가리키지 않는 포인터"를 의미한다.

 

널 포인터는 NULL이라는 이름의 매크로로 정의되어 있다. 

우리는 malloc의 반환값을 다음과 같이 확인해볼 수 있다.

p = malloc(10000);

if(p == NULL)
{
	// allocation failed; take appropriate action
}

동적 할당된 문자열

동적 메모리 할당은 문자열과 함께 할때도 유용하다.

 

malloc 함수는 다음과 같은 원형을 갖는다.

void *malloc(size_t size);

 

malloc 함수는 size 만큼의 바이트를 할당하고 그의 포인터를 반환한다.

size_t 타입은 무부호정수(unsigned integer) 타입으로 C 라이브러리에 선언되어있다.

 

n개의 문자를 갖는 문자열을 위해 메모리를 할당하려면 아래와 같이 작성한다.

p = malloc(n + 1);

여기서 p는 char 타입을 가리키는 포인터 변수이다.

 

malloc이 반환하는 일반적인 포인터(generic pointer)는 할당이 실행될 때 char* 형태로 자동으로 변환된다. (형변환이 불필요하다)

 

몇몇 프로그래머들은 아래 코드처럼 형변환하는 것을 선호하기도 한다.

p = (char *) malloc(n + 1);

 

이제 p는 n + 1개의 원소를 가지며 초기화되지 않은 배열을 가리킨다.

 

이 배열을 초기화 하는 방법 중 strcpy를 이용해보자.

strcpy(p, "abc");

 

이제 배열의 첫 네 문자는 a, b, c, \0으로 채워졌다.


동적 할당된 배열

동적 할당된 배열은 동적 할당된 문자열과 동일한 이점을 가진다. (놀랍지 않다. 문자열은 배열이다.)

 

프로그램을 작성할 때 배열에 맞는 사이즈를 추정하는 것은 어렵다.

C는 프로그램 실행 중에 배열에 대한 공간을 할당하고, 첫 번째 원소를 가리키는 포인터를 통해 배열에 접근하도록 하여 이러한 문제를 해결한다.

 

문자열에 사용한 것과 같이 배열에도 malloc 함수를 사용하여 메모리를 할당할 수 있다.

가장 큰 차이점은 배열의 원소가 반드시 1 바이트일 수 없다는 것이다.

따라서 우리는 sizeof 연산자를 사용하여 필요한 크기를 계산하여야 한다.

 

int *a;

a = malloc(n * sizeof(int));

위 코드는 n개의 원소를 갖는 정수형 배열을 동적 할당한다.


calloc 함수

malloc 함수를 배열의 동적 할당에 사용할 수 있지만, calloc 함수가 더 유용할 때도 있다.

 

calloc 함수의 원형은 다음과 같다.

void *calloc(size_t nmemb, size_t size);

 

calloc은 nmemb개의 원소를 갖는 배열을 할당한다.

각각의 원소는 size 만큼의 바이트크기만큼 할당된다.

 

할당에 성공하면 calloc은 모든 비트를 0으로 초기화한다.

 

예를 들어, 

a = calloc(n, sizeof(int));

이 코드는 n개의 원소를 갖고, 정수형의 size(4바이트)가 각각 할당되었고, 0으로 초기화된 배열의 포인터를 반환한다.

 

calloc은 할당 후에 값을 초기화하지만 malloc은 그렇지 않다.

따라서 우리는 배열이 아닌 개체(object)를 위해 메모리를 할당할 때도 calloc을 사용할 수 있다.

첫번째 인수(nmemb)를 1로 호출함으로 우리는 어떤 종류의 데이터에 대한 공간을 할당할 수 있다.

struct point { int x, y; } *p;

p = calloc(1, sizeof(struct point));

 

위 구문이 실행되면 p는 x와 y가 0으로 초기화된 point 구조체를 가리킬 것이다.


realloc 함수

배열을 한번 할당한 후에 너무 작거나 너무 큰 것을 알아챌 수 있다.

 

realloc 함수는 배열의 크기를 우리가 필요한 만큼 조정할 수 있도록 한다.

 

realloc 함수의 원형을 보자.

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

 

매개변수 ptr은 이전에 malloc, calloc, realloc 으로 할당된 메모리 공간을 가리켜야 한다.

매개변수 size은 기존보다 크거나 작은 새 메모리 크기를 전달한다.

(malloc, calloc, realloc으로 할당하지 않은 포인터를 사용한다면 정의되지 않은 동작(undefined behavior)이 발생할 수 있다)

 

C 표준에 realloc에 관한 몇 가지 규칙이 있다.

  • 메모리 블럭을 확장할 때 초기화하지 않는다.
  • 메모리가 요청한 만큼 충분하지 않다면 널 포인터를 반환한다. 기존 메모리에 담긴 데이터는 변하지 않는다.
  • 첫번째 매개변수 (ptr)에 널 포인터를 입력하면 malloc처럼 동작한다.
  • 두번째 매개변수 (size)에 0을 입력하면 메모리 할당을 해제(free)한다.

※ realloc을 반환받으면 기존 포인터를 갱신해야 한다. 메모리 확장 시 기존 블록 뒤에 다른 목적으로 사용되는 블록이 있다면, 기존 블록을 옮긴 후 확장하기 때문이다. 


메모리 할당 해제

malloc을 포함한 메모리 할당 함수는 heap이라고 알려진 저장소를 사용한다.

 

이러한 함수를 너무 자주 사용하여(또는 너무 큰 메모리를 요청하면) 힙을 다 써버리면 함수는 널 포인터를 반환할 것이다.

심지어 메모리를 할당하고 참조하지 않으면 버려지는 공간이 생긴다.

p = malloc(...);
q = malloc(...);

p = q;

위 두 구문을 실행하면 아래와 같은 형태가 된다.

그 후 q가 p에 할당되면 두 포인터 변수는 모두 아래쪽 메모리 블럭을 가리킨다.

첫 번째 블럭을 가리키는 포인터가 없기에 우리는 첫번째 블럭을 다시 사용할 수 없다.

이렇게 다시 접근할 수 없는 메모리 블럭을 가비지(쓰레기, garbage)라고 부른다.

 

쓰레기는 메모리 누수를 유발한다.

 

몇몇 언어들은 이를 자동으로 재활용하는 가비지컬렉터를 제공하지만 C는 그렇지 않다.

 

대신, C는 free 함수 호출을 통해 불필요한 메모리를 해제하여 가비지를 재활용할 책임이 있다.

free 함수

free 함수는 다음과 같은 원형을 갖는다.

void free(void *ptr);

 

free 사용은 간단하다. 

더이상 사용하지 않는 메모리를 가리키는 포인터를 넘겨주기만 하면 된다.

p = malloc(...);
q = malloc(...);

free(p);

p = q;

댕글링 포인터 문제 (Dangling pointer problem)

직역하면 매달린 포인터 문제지만 어색할 수 있어 영어 발음 그대로 표기했다.

 

free를 사용하여 포인터를 해제하면 댕글링 포인터 문제가 발생할 수 있다.

 

만약 할당 해제한 포인터가 아무것도 가리키지 않는다는 것을 잊는다면, 혼란이 발생할 것이다.

char *p = malloc(4);
...
free(p);
...
strcpy(p, "abc"); /*** WRONG ***/

 

여러 개의 포인터가 동일한 메모리 블럭을 가리킬 수 있기 때문에 댕글링 포인터는 발견하기 어려울 수 있다.

메모리 블럭의 할당이 해제되면 이를 가리키는 모든 포인터는 댕글링 상태가 된다.

 

※ 할당 해제된 메모리 블럭에 접근하거나 수정하려고 하면 정의되지 않은 동작(undefined behavior)이 발생할 수 있다. 이를 수정하려고 하면 프로그램 충돌을 포함한 재앙적인 결과(disastrous consequences)를 초래할 수 있다.

반응형