[C] 동적 할당 (1)

방법이있지·2025년 6월 13일
post-thumbnail

C의 malloc 함수는 대체 언제 어떻게 써야 하는 건지 알아봅시다.

동적 할당

  • C에서 변수나 배열 선언을 하면, 실행 시점에 메모리가 자동으로 할당됨
    • e.g., int num; -> 4바이트 공간 확보!
    • e.g., int arr[3]; -> 4바이트 * 3 = 12바이트 확보!
  • 단 프로그램 실행 도중에, 필요한 시점에 직접 저장 공간을 할당해야 할 수도 있음
  • 이를 메모리 동적 할당이라 함

메모리 동적 할당 및 반환하기

  • malloc: 실행 중 메모리를 동적 할당
    • memory allocation이라서 엠 얼록처럼 읽는다고 함
    • 하지만 어림도 없지. 난 말록이라고 읽겠음.
  • free: 동적 할당한 메모리를 반환
  • <stdlib.h> 헤더를 인클루드하고 사용해야 함
#include <stdio.h>
#include <stdlib.h>

int main(void){
    // 포인터를 선언만 하고 초기화하지 않음
    int *pi;
    double *pd;

    // 4바이트 할당 후 포인터 연결
    pi = (int *)malloc(sizeof(int));

    // 동적할당에 실패하면 널 포인터 반환
    if (pi == NULL){
        printf("메모리가 부족합니다.\n");
        exit(1);
    }

    // 8바이트 할당 후 포인터 연결
    pd = (double *)malloc(sizeof(double));

    *pi = 10;
    *pd = 3.4;

    printf("*pi: %d\n", *pi);
    printf("*pd: %.1lf\n", *pd);

    // 사용 후 영역 반환
    free(pi);
    free(pd);

    return 0;
}

malloc - 공간 할당

  • malloc(할당할 저장 공간의 크기)와 같이 사용
  • 확보된 저장 공간의 주소를 반환
    • 이때 주소는 (void *)형이므로, 용도에 맞는 포인터로 형 변환해야 함
  • e.g., pi = (int *)malloc(sizeof(int))
    • sizeof(int) -> 4바이트의 공간을 할당
    • 주소 반환 후 (int *)로 형변환해 (int *)형 포인터 pi에 대입
  • int *pi = (int *)malloc(sizeof(int)) 처럼 초기화와 동시에 할당도 가능

🫤 지금까진 이렇게 안 하고 pi = &a로 주소를 대입했었잖아요! malloc은 왜 필요한 거에요?

  • 포인터 p를 선언만 하고 초기화하지 않으면, 기본적으로 쓰레기 값을 가지고 있습니다. 즉 유효한 메모리 위치를 가리키지 않을 수 있습니다.
  • 따라서 포인터를 사용할 땐 항상 유효한 메모리 주소를 대입해 주어야 합니다.
    • 그렇게 안 해주면 그 유명한 Segmentation Fault가 뜹니다.
  • pi = &a 식으로 기존 변수의 주소를 대입하면, a를 선언하면서 이미 할당된 메모리의 주소가 대입됩니다. 즉 malloc 없이 포인터를 사용할 수 있습니다.
  • 하지만 위 예제에선 상수 10, 3.4를 저장할 공간이 따로 존재하지 않습니다.
    • 이 값들을 저장할 공간이 필요하기 때문에, malloc으로 메모리를 확보하고 그 주소를 포인터에 저장하는 겁니다.

널 포인터

  • malloc은 요청한 크기의 공간 할당에 실패하면 NULL을 반환
    • NULL은 전처리 단계에서 0으로 바뀜
  • malloc이 반환한 NULL을 포인터에 대입하고 참조하면, Segmentation Fault 발생
  • 따라서, mallocNULL을 반환했는지 확인하고 사용해야 함
    • if (pi == NULL) exit(1);와 같이 예외 처리 필요

🫤 Segmentation Fault는 뭘 뜻하는 건가요?

  • 세그폴트 (줄여서 이렇게 부를게요)는 허용되지 않은 메모리 구간에 접근했을 때 발생하는 오류입니다.
int* pi;
*pi = 10;   // 초기화되지 않은 포인터에 접근할 시
  • 초기화되지 않은 포인터에는 쓰레기 값(즉 임의의 주소)이 저장되어 있는데, 보통 해당 주소는 OS가 보호하는 영역입니다. 접근하려고 하면 곧바로 세그폴트를 때립니다.
  • NULL 포인터에 접근하려고 할 시, 대부분 OS는 0번지 주소에 대한 접근을 차단하므로 곧바로 세그폴트를 때립니다.

free - 공간 반환

  • int a, int arr[4]와 같이 지역 변수를 선언하며 자동으로 할당된 메모리는, 함수가 종료되면 곧바로 해제됨
  • 하지만 동적으로 할당한 저장 공간은, 함수가 반환되어도 메모리에 남아 있음
  • free(포인터)를 이용해 직접 메모리를 회수해야 함
    • 회수하지 않을 시, 메모리 누수로 인해 프로그램이 의도치 않게 종료될 수 있음

연속된 변수를 저장할 공간 할당하기

  • 형태가 같은 변수들을 연속으로 저장할 땐, 크기가 큰 저장 공간을 한꺼번에 동적 할당하는 것이 효율적
  • 할당한 저장 공간의 시작 주소만 저장하면, 포인터 연산으로 각 변수에 접근할 수 있음 (배열과 유사하게)
  • e.g., int형 변수 5개를 저장해야 할 때
int *pi = (int *)malloc(sizeof(int) * 5);    // 4 * 5 = 20바이트 할당
int sum = 0;    // 배열 값의 합을 저장할 변수

if (pi == NULL){
    printf("메모리가 부족합니다\n");
    exit(1);
}

printf("5개의 값을 입력하세요: ");

// 20바이트를 4바이트씩 끊어서, 5개의 int 변수 저장
for (int i = 0; i < 5; i++){
    scanf("%d", pi + i);    // 주소 값은 4바이트 크기만큼 증가
    sum += *(pi + i);       // *(pi + i) 말고 pi[i] 쓰셔도 됨
}

printf("평균값: %.1lf\n", (sum / 5.0));
free(pi);   // 메모리 공간 반환
  • int 5개를 저장 -> 45=204 * 5 = 20바이트 저장공간 할당
  • pi + i 포인터 연산으로, 주소 값을 int형의 크기인 4만큼 증가시켜 사용

⚠️ 이 경우 free를 할 때 무조건 한꺼번에 free(pi)를 해 줘야 합니다.

  • int 5개를 저장할 공간이 할당된 만큼 free(&pi[0]), free(&pi[1])... 이렇게 따로 해도 될 거라고 착각할 수 있지만
  • freemalloc 또는 관련 함수(calloc, realloc)로 받은 정확한 시작 주소만 받을 수 있습니다.
  • free(&pi[0])은 어쨌든 pi와 같은 주소를 가지는 만큼 잘 작동된다 쳐도, free(&pi[1])부터 오류가 뜨거나 예기치 못한 동작이 발생할 수 있습니다. 그니까 하지 마요.

calloc: 0으로 초기화

  • calloc: 메모리를 동적으로 할당한 뒤, 할당한 공간을 0으로 초기화
    • calloc(배열 요소의 개수, 각 요소 하나의 크기)
    • 전체 저장공간의 크기를 매개변수로 받는 malloc과 다름에 유의.
    • e.g., int형 변수 5개를 저장할 공간을 할당 시, malloc(sizeof(int) * 5)가 아닌 calloc(5, sizeof(int))와 같이 호출
  • malloc과 마찬가지로 시작 위치를 void *로 반환, 형 변환해서 사용
// 5개의 int를 저장할 공간 할당 후, 값을 0으로 초기화
int *pi = calloc(5, sizeof(int));

for (int i = 0; i < 5; i++){
    printf("%4d", *(pi + i));
}
//    0   0   0   0   0

realloc: 저장공간 크기 조정

  • realloc: 이미 할당된 저장공간의 크기를 조절
    • realloc(저장공간의 포인터, 조정 이후 저장공간의 크기)
  • malloc과 마찬가지로 시작 위치를 void *로 반환, 형 변환해서 사용
int count = 0;  // 입력된 수의 개수
int num;        // 수를 입력받을 변수
int size = 5;   // 현재 저장공간에 저장할 수 있는 int 요소의 수
int *pi = malloc(size * sizeof(int));   // 일단 5개의 int 저장 가능

while (1){
    printf("저장할 양수를 입력하세요: ");
    scanf("%d", &num);      // num에 수 입력받기
    if (num <= 0) break;    // 0 이하 값이면 종료

    if (count == size){
        size += 5;          // 저장공간이 꽉 찬 경우 재할당
        pi = (int *)realloc(pi, size * sizeof(int));
    }

    pi[count] = num;
    count += 1;
}

for (int i = 0; i < count; i++){
    printf("%3d", pi[i]);
}
free(pi); // 동적 할당된 공간 반환

// [출력]
// 저장할 양수를 입력하세요: 7
// 저장할 양수를 입력하세요: 6
// 저장할 양수를 입력하세요: 5
// 저장할 양수를 입력하세요: 4
// 저장할 양수를 입력하세요: 3
// 저장할 양수를 입력하세요: 2
// 저장할 양수를 입력하세요: 1
// 저장할 양수를 입력하세요: 0
//   7  6  5  4  3  2  1
  • 우선 int 변수 5개를 저장할 20바이트만 할당
    • size로 현재 저장할 수 있는 최대 변수 수를 관리
  • 저장공간이 부족할 때 size를 5 증가시키고, size * sizeof(int) 만큼 메모리 재할당
  • 위 코드랑 반대로 저장공간을 줄일 수도 있는데, 이 경우 기존 데이터는 잘려 나감
profile
뭔가 만드는 걸 좋아하는 개발자 지망생입니다. 프로야구단 LG 트윈스를 응원하고 있습니다.

0개의 댓글