본문으로 건너뛰기

C 언어 동적 메모리 할당

정적 할당과 동적 할당

C 언어에서 변수는 크게 정적(static) 또는 동적(dynamic)으로 할당된다.

  • 정적 메모리 할당: int a = 10;과 같이 일반적인 변수 선언 방식이다. 이 방식에서는 프로그램이 실행되기 전, 컴파일 시점에 변수가 사용할 메모리 공간의 크기와 위치가 결정된다.
  • Dynamic Memory Allocation(동적 메모리 할당): 프로그램 실행 중에 필요한 만큼의 메모리를 운영체제로부터 할당받아 사용하는 방식이다. 사용할 메모리 양을 예측하기 어려울 때 유용하며, 메모리를 효율적으로 사용할 수 있다.

동적 메모리 할당 함수

C 언어는 동적 메모리 할당을 위해 stdlib.h 헤더 파일에 정의된 여러 함수를 제공한다.

malloc() 함수

malloc() 함수는 인자로 전달된 바이트 크기만큼의 메모리 블록을 할당한다.

  • 함수 원형: void* malloc(size_t size);
  • 인자: 할당받고 싶은 메모리의 크기를 바이트 단위로 지정한다. sizeof 연산자를 사용하는 것이 일반적이다.
  • 반환값: 할당된 메모리 블록의 시작 주소를 void* 타입으로 반환한다. 할당에 실패하면 NULL을 반환한다. 반환된 void* 포인터는 실제 사용할 포인터 타입으로 반드시 형 변환(casting)해야 한다.
int *pi;
// int 크기만큼의 메모리를 동적으로 할당하고 주소를 pi에 저장
pi = (int*)malloc(sizeof(int));
*pi = 3; // 할당된 공간에 값 저장

calloc() 함수

calloc() 함수는 malloc()과 유사하게 메모리를 할당하지만, 할당된 메모리의 모든 비트를 0으로 초기화한다는 차이점이 있다.

  • 함수 원형: void* calloc(size_t nmemb, size_t size);
  • 인자:
    • nmemb: 할당할 데이터 항목의 개수
    • size: 각 데이터 항목의 크기
int *ary;
// int 3개 크기의 메모리를 할당하고 0으로 초기화
ary = (int*)calloc(3, sizeof(int));

realloc() 함수

realloc() 함수는 이미 할당된 메모리 블록의 크기를 변경할 때 사용한다.

  • 함수 원형: void* realloc(void *ptr, size_t size);
  • 인자:
    • ptr: 크기를 변경할 기존 메모리 블록의 포인터
    • size: 새롭게 할당할 메모리의 전체 크기

기존 블록의 내용은 가능한 한 보존되어 새로운 블록으로 복사된다.

free() 함수

malloc(), calloc(), realloc()으로 동적으로 할당된 메모리는 사용이 끝나면 반드시 free() 함수를 호출하여 시스템에 반환해야 한다. 이를 통해 메모리 누수(memory leak)를 방지할 수 있다.

  • 함수 원형: void free(void *ptr);
  • 인자: 해제할 메모리 블록의 시작 주소를 가리키는 포인터
free(pi); // pi가 가리키는 동적 메모리 해제
pi = NULL; // 포인터를 NULL로 설정하는 것이 안전하다

2차원 배열의 동적 할당

2차원 배열도 동적으로 생성할 수 있다. 이는 행(row)을 가리키는 포인터 배열을 먼저 할당하고, 각 행에 대해 열(column) 공간을 다시 할당하는 2단계 과정으로 이루어진다.

int **table;
int rownum, colnum;

// 사용자로부터 행과 열의 크기를 입력받는다고 가정
scanf("%d %d", &rownum, &colnum);

// 1. 행의 개수만큼 포인터 배열을 할당
table = (int**)calloc(rownum, sizeof(int*));

// 2. 각 행에 대해 열의 개수만큼 메모리를 할당
for (int i = 0; i < rownum; i++) {
table[i] = (int*)calloc(colnum, sizeof(int));
}

// ... table[i][k] 형태로 사용 ...

// ... 사용 후 메모리 해제 (할당의 역순) ...
for (int i = 0; i < rownum; i++) {
free(table[i]); // 각 행 메모리 해제
}
free(table); // 행 포인터 배열 해제

연결 리스트 (Linked List)

자기 참조 구조체

**Linked List(연결 리스트)**는 동적 메모리 할당을 기반으로 하는 대표적인 자료구조이다. 연결 리스트를 구현하기 위해서는 자기 참조 구조체를 사용해야 한다.

자기 참조 구조체란 구조체의 멤버 중 하나가 자기 자신과 동일한 타입의 구조체를 가리키는 포인터 변수를 갖는 구조체이다.

struct selfref {
int n;
struct selfref *next; // 자기 자신을 가리키는 포인터 멤버
};

이때 멤버 next는 반드시 포인터여야 한다. 포인터가 아닌 일반 구조체 변수(struct selfref next;)는 구조체의 크기를 무한히 확장시키므로 허용되지 않는다.

연결 리스트의 개념과 생성

Linked List는 자기 참조 구조체로 만들어진 **Node(노드)**들이 꼬리처럼 연결된 구조이다. 각 노드는 데이터와 다음 노드를 가리키는 포인터로 구성된다. 리스트의 시작은 Head(헤드) 포인터가 가리킨다.

Linked List의 가장 큰 장점은 프로그램 실행 중에 메모리가 허용하는 한, 노드의 수를 동적으로 늘리거나 줄일 수 있다는 점이다.

생성 과정

  1. 노드 구조체 정의 및 포인터 선언

    typedef struct selfref list;
    list *head = NULL; // 헤드 포인터를 NULL로 초기화
    list *newNode = NULL;
  2. 노드 생성 및 데이터 저장 malloc을 이용해 새로운 노드를 위한 메모리를 할당하고 데이터를 저장한다.

    newNode = (list *)malloc(sizeof(list));
    newNode->n = 100;
    newNode->next = NULL; // 새 노드는 아직 아무것도 가리키지 않음
  3. 노드 연결 기존 리스트의 마지막 노드가 새로운 노드를 가리키게 하거나, 헤드 포인터가 새로운 노드를 가리키게 하여 리스트를 확장한다. 예를 들어 리스트의 맨 앞에 노드를 추가하는 방법은 다음과 같다.

    // 새로운 노드를 생성했다고 가정 (newNode)
    newNode->next = head; // 새 노드가 기존의 첫 번째 노드를 가리킴
    head = newNode; // 헤드 포인터가 새로운 노드를 가리키도록 변경

    이 과정을 반복하면 데이터를 순차적으로 연결한 리스트를 만들 수 있다.