[C] 동적 할당 (2), 가변길이 배열

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

[정글 4-8주차] C언어

목록 보기
10/26
post-thumbnail

동적 할당은 입력 데이터의 크기가 미리 정해져 있지 않은 상황에서 매우 유용하게 활용할 수 있습니다.

힙 영역

  • 보통은 C 프로그램 하나가 사용하는 영역은, 아래와 같이 나뉨
    • 주소가 낮은 순 -> 높은 순으로
    • 코드 영역: 실행 파일의 기계어 코드가 저장되는 영역
    • 데이터 영역: 전역 변수, 정적(static) 변수가 저장되는 영역
    • 힙 영역: 동적 할당된 저장공간의 영역 (malloc, calloc, realloc)
    • 스택 영역: 함수 내 지역 변수, 매개변수 등이 저장되는 영역
  • 힙에 할당한 저장 공간은, 함수가 종료되어도 프로그램이 종료될 때까지 메모리에 존재
    • 따라서 할당받은 주소만 알면, 어떤 함수에서든 사용 가능
    • 하지만 메모리 누수를 막기 위해, 다 쓰고 나선 free로 회수해야 함!!

동적 할당을 사용하는 이유?

동적 할당을 사용한 문자열 처리

char temp[80];  // 입력 문자열을 임시로 저장
char *str[4];   // char* 포인터 4개로 구성된 배열

for (int i = 0; i < 4; i++){
    printf("문자열 입력: ");

    // temp 배열에 문자열 저장
    fgets(temp, sizeof(temp), stdin);
    // 입력 문자열에서 첫 "\n"의 위치를 찾아, 널문자로 바꿈
    temp[strcspn(temp, "\n")] = '\0';

    // 문자열의 길이만큼 메모리 할당
    // 널 문자 고려해 1 추가
    str[i] = (char *)malloc(strlen(temp) + 1);
    strcpy(str[i], temp);
}

// 문자열 출력
for (int i = 0; i < 4; i++){
    printf("%s ", str[i]);
}

// 동적 할당한 메모리 해제
for (int i = 0; i < 4; i++) {
    free(str[i]);
}

// [입력]
// 문자열 입력: I love
// 문자열 입력: coffee
// 문자열 입력: and
// 문자열 입력: tea

// [출력]
// I love coffee and tea

  • 문자열을 80 크기의 char 배열 temp에 입력받음
  • 이후 strlen(temp) + 1 길이에 맞게 동적 할당 후, strcpy로 시작 주소를 포인터 배열 str의 원소에 저장
    • strlen(temp)은 배열 temp에 저장된 문자열의 길이를 반환
    • 1을 더해준 이유: 널 문자를 포함해야 함
  • 문자열 길이만큼만 메모리를 할당하기 때문에, 메모리를 효율적으로 사용할 수 있음

가변 길이 배열

  • C99 버전 이후로는 배열을 선언할 때, 프로그램을 실행한 후 결정된 변수 크기로 배열을 선언할 수 있음
    • int arr[N], int arr[N][M]과 같은 식으로
    • 가변 길이 배열은 int arr[N] = {0} 식으로 초기화 불가능, 직접 값을 대입해야 함
  • 동적 할당같아 보이지만, 동적 할당 아님.
  • 실제로 일반적인 지역변수로 취급되어 가변 크기 배열은 힙 영역이 아니라 스택 영역에 저장되며, free()로 할당 해제를 할 필요도 없음
#include <stdio.h>

int main(void){
    int N, M;
    printf("행의 수: ");
    scanf("%d", &N);
    printf("열의 수: ");
    scanf("%d", &M);

    int arr[N][M];
    // 가변 길이 배열은 초기화할 수 없고, 선언만 가능

    for (int i = 0; i < N; i++){
        for (int j = 0; j < M; j++){
            arr[i][j] = N * i + j;
        }
    }

    for (int i = 0; i < N; i++){
        for (int j = 0; j < M; j++){
            printf("%4d", arr[i][j]);
        }
        printf("\n");
    }
}

// [입력]
// 행의 수: 4
// 열의 수: 5

// 출력
//    0   1   2   3   4
//    4   5   6   7   8
//    8   9  10  11  12
//   12  13  14  15  16
  • 메모리를 일일이 free 해제시켜주지 않아도 되는 장점이 있지만
    • 스택 영역은 힙 영역에 비해 작기 때문에, 큰 배열의 경우 스택 오버플로우 발생 가능
    • 대량의 데이터를 처리해야 하는 경우 동적 할당 사용이 권장됨
  • 그리고 일부 컴파일러에선 호환성 문제가 있을 수 있음

연습문제

문제 1

4행 5열의 행렬 값을 저장할 2차원 배열을 동적 할당하고, 작성한 값을 반환하는 코드를 작성하세요.

  • 각 행의 5개의 값을 저장할 공간이 필요함
    • 이건 malloc(sizeof(int) * 5)로 할당해야겠지?
    • 결괏값인 시작 주소는 int *형으로 형 변환해야겠지?
  • 다음엔 각 행(부분배열)을 저장할 공간이 필요함
    • 실제로는 각 부분배열의 시작 주소, 즉 int *만 알고 있으면 됨
    • 이건 malloc(sizeof(int *) * 4)로 할당해야겠지?
    • 결괏값인 시작 주소는 int **형이겠지?
  • 결과적으로는 위 주소를 저장할 int **형 포인터가 필요함
#include <stdio.h>
#include <stdlib.h>

int main(void){
    // 이중 포인터 p 선언
    // int * 원소 4개의 배열을 저장할 수 있는 공간 마련
    // 메모리 크기는 4 * sizeof(int *)
    int **p = malloc(sizeof(int *) * 4);

    // 널포인터 여부 체크
    if (p == NULL) exit(1);

    for (int i = 0; i < 4; i++){
        // int 원소 5개의 배열을 저장할 수 잇는 공간 마련
        // 메모리 크기는 5 * sizeof(int)
        p[i] = malloc(sizeof(int) * 5);

        // 널포인터 여부 체크
        if (p[i] == NULL) exit(1);
        for (int j = 0; j < 5; j++){
            int num = 5 * i + j;
            p[i][j] = num;
        }
    }

    // 출력하기
    for (int i = 0; i < 4; i++){
        for (int j = 0; j < 5; j++){
            printf("%3d", p[i][j]);
        }
        printf("\n");
    }

    // 반환하기
    for (int i = 0; i < 4; i++){
        free(p[i]);
    }
    free(p);
}

// [출력]
//   0  1  2  3  4
//   5  6  7  8  9
//  10 11 12 13 14
//  15 16 17 18 19
  • 여기선 입력 크기가 4행 5열로 정해졌지만
  • 실제론 행의 크기 / 열의 크기가 정해지지 않았을 때, 입력받은 후 상수 4, 5만 해당 값으로 바꿔주면 문제없음

문제 2

키보드로 양수를 입력한 후에 입력한 수 이전 수까지 모든 소수를 출력합니다.
2부터 한 줄에 5개씩 출력하며, 소수가 아닌 수는 X를 출력합니다.
입력한 수에 따라 적절한 크기의 배열을 동적 할당해 사용합니다.

  • 에라토스테네스의 체 알고리즘을 사용
  • 입력한 수를 target으로 둘 때, target개의 int 원소를 저장할 공간 동적 할당 후 포인터 p에 주소 대입
  • p[i]0이면 소수이고, p[i]1이면 소수가 아니라고 해석
    • 편의상 calloc으로 모든 값을 0으로 초기화
  • 이후엔 기존의 에라토스테네스의 체 알고리즘과 동일
#include <stdio.h>
#include <stdlib.h>
#include <math.h>   // sqrt 함수

int main(void){
    int target;
    printf("양수 입력 : ");
    scanf("%d", &target);

    // (target)개의 원소를 저장할 공간 동적 할당 후, 0으로 초기화
    // p[i] == 0: 소수
    // p[i] == 1: 소수 아님
    int *p = (int *)calloc(target, sizeof(int));

    // 널 포인터인지 확인
    if (p == NULL) exit(1);

    // 에라토스테네스의 체
    // 제곱근을 구하는 sqrt는 double 타입을 받고 반환. 이에 맞게 형변환
    int final_num = (int)sqrt((double)target);
    for (int i = 2; i <= final_num; i++){
        // 지금 수가 소수인 경우
        if (p[i] == 0){
            // 지금 수의 배수는 소수가 아님
            for (int j = i * i; j < target; j += i){
                p[j] = 1;
            }
        }
    }

    // 2부터 시작해서 소수면 수를, 소수가 아니면 X를 출력
    int count = 0;
    for (int i = 2; i < target; i++){
        if (p[i] == 0){
            printf("%4d", i);
        } else {
            printf("%4c", 'X');
        }
        count++;
        if (count % 5 == 0){
            printf("\n");
        }
    }

    // 메모리 반환
    free(p);
}

// [입력]
// 양수 입력 : 30

// [출력]
//    2   3   X   5   X
//    7   X   X   X  11
//    X  13   X   X   X
//   17   X  19   X   X
//    X  23   X   X   X
//    X   X  29

보충

  • 이 문제에서 malloc, calloc을 사용하면 편한 이유?
  • 입력 값인 target에 따라서 소수인지 아닌지 판별해야 하는 수의 수가 달라지기 때문입니다.
  • 고정된 길이의 배열로는 이렇게 입력이 달라지는 경우를 처리하기 어렵습니다.
  • malloc을 사용하면 입력받은 target 값에 따라 필요한 만큼 공간을 할당할 수 있겠죠.
profile
뭔가 만드는 걸 좋아하는 개발자 지망생입니다. 프로야구단 LG 트윈스를 응원하고 있습니다.

0개의 댓글