모두의 코드
씹어먹는 C 언어 - <20 - 1. 동동동 메모리 동적할당(Dynamic Memory Allocation)>

작성일 : 2010-08-03 이 글은 95507 번 읽혔습니다.

이번 강좌에서는

  • malloc 함수의 이해

  • 1 차원 배열 동적 할당

  • 2 차원 배열 동적 할당

씹어먹는 C 언어

안녕하세요. 여러분. 정말 멀리 달려 온 것 같네요. 벌써 제 20 장을 지나가고 있습니다. 물론 강의 수로 따지면 더 많죠.

아마도 여러분은 프로그램을 만들면서 다음과 같은 문제에 봉착했던 적들이 많았었을 것입니다.

배열의 크기를 자유 자재로 다룰 수 있으면 얼마나 좋을까?

맞습니다. 우리가 배열을 정할 때 그 크기는 언제나 컴파일 시간에 확정 되어 있어야 합니다. 즉 컴파일러가 배열의 크기를 추측할 필요 없이 명확하게 나타나 있어야 된다는 것이지요. 하지만 이는 정말 고역스러운 일이 아닙니다. 예를 들어 우리가 컴퓨터로 부터 학생들의 수학 점수를 입력 받아 평균을 내는 프로그램을 만든다고 해봅시다. 각 학급 마다 학생들의 수가 모두 다르기 때문에 배열의 크기를 명확하게 정할 수 없게 됩니다. 따라서 보통 이 경우 배열을 '충분히 크게' 잡게 되는데 이렇게 된다면 메모리가 낭비되는 경우가 허다하게 발생합니다.

컴퓨터에서 낭비란 곧 비효율적인 프로그램을 의미하는 것이지요. 이렇게 쓸데 없이 낭비되는 자원을 막기 위해 '학생 수' 를 입력 받고 그 학생 수 만큼 배열의 크기를 지정하면 얼마나 좋을까요. 하지만 놀랍게도 이렇게 할 수 있는 방법이 있습니다.

바로 동적 메모리 할당 이라는 방법 입니다. 이 것은 말그대로 동적으로 메모리를 할당 합니다. 여기서 '동적' 이란 말은 딱 정해진 것이 아니라 가변적으로 변할 수 있다는 말이지요. 또한 메모리를 '할당' 한다는 이야기는 역시 우리가 배열을 정의하면 배열에 맞는 메모리의 특정한 공간이 배열을 나타내는 것 처럼 메모리의 특정한 부분을 사용할 수 있게 됩니다.참고적으로 아마 다 아시겠지만 할당되지 않는 메모리는 절대로 사용할 수 없습니다.

도대체 어떻게 그런 일이 가능할까요.

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

int main(int argc, char **argv) {
  int SizeOfArray;
  int *arr;

  printf("만들고 싶은 배열의 원소의 수 : ");
  scanf("%d", &SizeOfArray);

  arr = (int *)malloc(sizeof(int) * SizeOfArray);
  // int arr[SizeOfArray] 와 동일한 작업을 한 크기의 배열 생성

  free(arr);

  return 0;
}

성공적으로 컴파일 했다면

실행 결과

만들고 싶은 배열의 원소의 수 : 5

일단 위 예제를 통해서는 정말 우리가 원하는 크기의 배열이 생겼는지는 모르겠지만 일단 위 소스코드 부터 파헤쳐봅시다.

printf("만들고 싶은 배열의 원소의 수 : ");
scanf("%d", &SizeOfArray);

먼저 우리는 위 과정을 통해서 우리가 원하고자 하는 int 배열의 원소의 개수를 입력 받았습니다. 그리고,

arr = (int *)malloc(sizeof(int) * SizeOfArray);

두둥. 바로 이 녀석이 우리가 원하는 작업을 해주는 역할을 합니다. 이 함수의 이름은malloc 이며 memory allocation 의 약자 입니다.

이 함수는 <stdlib.h> 에 정의되어 있기 때문에 #include <stdlib.h> 를 추가해주어야 합니다.

이 함수는 인자로 전달된 크기의 바이트 수 만큼 메모리 공간을 만듭니다. 즉 메모리 공간을 할당하게 되는 것이지요. 우리가 원소의 개수가 SizeOfArrayint 형 배열을 만들기 위해서는 당연히 (int 의 크기) * (SizeOfArray) 가 되겠지요. 이 때, int 타입의 크기를 정확하게 알기 위해서 sizeof 키워드를 사용하게 됩니다. sizeof 는 이 타입의 크기를 알려줍니다. 따라서 sizeof(int) * SizeOfArray 를 인자로 전달해 주면 됩니다.

이 함수가 리턴하는 것은 자신이 할당한 메모리의 시작 주소를 리턴하게 됩니다. 이 때, 리턴형이 (void *) 형이므로 우리는 이를 (int *) 형으로 형변환 하여 arr 에 넣어주기만 하면 됩니다. 이렇게 보니 마치 malloc 함수가 공원에서 돗자리를 까는 역할을 하는 것과 같네요. 사람이 바글바글한 공원에서 malloc 함수는 '원하는 크기의 돗자리' 를 깔아주고 이 돗자리로 사람들이 올 수 있도록 손을 흔들어주는 역할을 하는 것과 같습니다.

따라서 arr 에는 malloc 이 할당해준 메모리를 이용할 수 있게 됩니다. 즉, arr[SizeOfArray] 만큼을 사용할 수 있게 되죠.

free(arr);

그리고 마지막에free 는 우리가 할당받은 다 쓰고 난 후에 메모리 영역을 다시 컴퓨터에게 돌려주는 역할을 합니다. 이를 해제(free) 한다 그러는데 이 free 를 제대로 하지 않게 된다면 딱히 사용하지도 않는 메모리를 쓸데없이 자리만 차지하게 되겠지요.

이렇게 free 를 제대로 하지 않아 발생되는 문제를 메모리 누수(memory leak) 이라고 합니다. 이는 마치 공원에 돗자리를 깔아놓고 그대로 놓고 집에 가는 것과 똑같은 일입니다. (이런 일이 반복된다면 나중에 다시 왔을 때 공원에는 돗자리를 놓을 수 있는 공간이 하나도 없겠죠?)

malloc 은 어디에 할당할까?

우리가 이전에 17 강에서 메모리 구조에 대해 배울 때 메모리에는 다음과 같은 구조들이 있다는 것을 배웠습니다.

이 때 다른 부분은 모두 설명하였는데 오직 힙(Heap) 에 대해서만 언급을 안했다는 것을 기억 하실 것입니다. 자, 이제 드디어 힙의 정체를 알 수 있는 시간입니다. 스택 이나, 데이타 영역, Read Only Data 부분은 당연하게도 malloc 함수가 결코 건드릴 수 없는 부분 입니다. 이 부분의 크기는 반드시 컴파일 때에 100% 추호의 의심의 여지도 없이 정해져야 합니다.

하지만 힙의 경우 다릅니다. 메모리의 힙 부분은 사용자가 자유롭게 할당하거나 해제할 수 있습니다. 따라서 우리의 malloc 함수도 이 힙을 이용하는 것입니다. 우리가 만들어낸 arr 은 힙에 위치하고 있습니다.

힙은 할당과 해제가 자유로운 만큼 제대로 사용해야 합니다. 만일 힙에 할당은 하였는데 해제를 하지 않았다면 공간이 낭비되겟지요. 다른 메모리 부분의 경우 컴퓨터가 알아서 처리하기 때문에 문제가 발생할 여지가 적지만 힙은 인간이 다루는 만큼 철저히 해야 합니다.

/* 동적 할당의 활용 */
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv) {
  int student;  // 입력 받고자 하는 학생 수
  int i, input;
  int *score;   // 학생 들의 수학점수 변수
  int sum = 0;  // 총점

  printf("학생의 수는? : ");
  scanf("%d", &student);

  score = (int *)malloc(student * sizeof(int));

  for (i = 0; i < student; i++) {
    printf("학생 %d 의 점수 : ", i);
    scanf("%d", &input);

    score[i] = input;
  }

  for (i = 0; i < student; i++) {
    sum += score[i];
  }

  printf("전체 학생 평균 점수 : %d \n", sum / student);
  free(score);
  return 0;
}

성공적으로 컴파일 하였다면

실행 결과

학생의 수는? : 3
학생 0 의 점수 : 100
학생 1 의 점수 : 90
학생 2 의 점수 : 95
전체 학생 평균 점수 : 95 

와 같이 나옵니다.

score = (int *)malloc(student * sizeof(int));

먼저 위 부분을 통해 원소의 개수가 studentint 형 배열을 생성하였죠. 따라서 우리는 scoreint score[student] 로 한 것 마냥 사용할 수 있게 됩니다.

for (i = 0; i < student; i++) {
  printf("학생 %d 의 점수 : ", i);
  scanf("%d", &input);

  score[i] = input;
}

for (i = 0; i < student; i++) {
  sum += score[i];
}

따라서 위와 같이 score 에 원소를 입력 받고 그 원소들을 모두 더해 평균을 구하게 됩니다. 어때요. 간단하지요.

2 차원 배열의 동적 할당

그렇다면 좀더 높은 난이도의 문제에 도전해봅시다. 2 차원 배열을 동적으로 할당할 수 있을까요? 물론 가능합니다. 2 차원 배열을 동적으로 할당하는 방법으로 크게 두 가지 방법을 생각할 수 있습니다.

  • 포인터 배열을 사용해서 2 차원 배열 처럼 동작하는 배열을 만드는 방법

  • 실제로 2 차원 배열 크기의 메모리를 할당한 뒤 2 차원 배열 포인터로 참조하는 방법

로 꼽을 수 있습니다. 먼저 포인터 배열을 사용하는 방법 부터 살펴봅시다.

포인터 배열을 이용해서 2 차원 배열 할당하기

포인터 배열이라 함은 이전에도 이야기 했었지만 배열의 각 원소들이 모두 포인터 인 배열 입니다. 따라서, 각 원소들이 다른 일차원 배열들을 가리킬 수 있습니다. 따라서 우리가 해야할 일은 먼저 포인터 배열을 동적으로 할당한 뒤에 다시 포인터 배열의 각각의 원소들이 가리키는 일차원 배열을 다시 동적으로 할당해 주면, 마치 2 차원 배열을 만든 것과 같은 효과를 낼 수 있습니다.

/* 2 차원 배열의 동적 할당 */
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv) {
  int i;
  int x, y;
  int **arr;  // 우리는 arr[x][y] 를 만들 것이다.

  printf("arr[x][y] 를 만들 것입니다.\n");
  scanf("%d %d", &x, &y);

  arr = (int **)malloc(sizeof(int *) * x);
  // int* 형의 원소를 x 개 가지는 1 차원 배열 생성

  for (i = 0; i < x; i++) {
    arr[i] = (int *)malloc(sizeof(int) * y);
  }

  printf("생성 완료! \n");

  for (i = 0; i < x; i++) {
    free(arr[i]);
  }
  free(arr);

  return 0;
}

성공적으로 컴파일 했다면

실행 결과

arr[x][y] 를 만들 것입니다.
3
5
생성 완료! 

흠. 일단 잘 생성은 된 것 같기는 한데, 한 번 차근 차근 소스를 따라가 봅시다.

int **arr;  // 우리는 arr[x][y] 를 만들 것이다.

일단 int **arr 부터 봅시다. 만일 int array[3]; 이란 배열을 만들었다면 array 의 형은 무엇일까요. 네 맞습니다. int * 이죠. 그렇다면 int * arr[10]; 이란 배열을 만들었다면 arr 의 형은? 네. int **arr 이죠. 따라서 우리는 int **arr; 과 같이 선언하였습니다.

arr = (int **)malloc(sizeof(int *) * x);

따라서 위와 같이 int * 형 배열을 동적 할당 할 수 있었습니다. 위 과정을 거치게 되면 arr 은 다음과 같은 모습일 것입니다.

""

자. 그럼 arr 배열의 각각의 원소들은 int * 형 이므로 다른 int 배열을 가리키기를 갈망하고 있을 것입니다. 우리는 그 욕구를 해소 시켜 주어야 겠죠. 따라서 각각의 원소들에 대해 원하는 메모리 공간을 짝지어 줍시다.

for (i = 0; i < x; i++) {
  arr[i] = (int *)malloc(sizeof(int) * y);
}

각각의 원소들에 대해 메모리 공간을 할당하고 있습니다. arr[i]malloc 이 정의한 또다른 공간을 가리키겠네요.

""

따라서 arr 의 하나의 원소가 크기가 y 인 배열을 가리키고 있는데 arr 의 원소가 x 개 이므로 전체적으로 보았을 때 총 x * y 배열을 가지는 셈입니다. 하지만 이렇게 만들어진 배열은 정확히 말해 2 차원 배열이라 말하기는 힘듧니다. 왜냐하면 배열은 모름지기 메모리에 연속적으로 있어야 하기 때문이죠. 예를 들어 이전 강의의 사진을 잠깐 가져오면

""

와 같이 말이지요. 하지만 우리가 만든 배열은 arr 의 원소들이 가리키는 메모리 공간이 연달아 존재한다고 보장할 수 없습니다. 또한 한 가지 재미있는점은 우리가 만든 '2 차원 배열 처럼 생긴' 포인터 배열은 2 차원 배열과는 달리 함수의 인자로 손쉽게 넘길 수 있습니다. 예를 들면

int array(int **array);

처럼 말이지요. array(arr); 을 하게 되면 우리가 만든 배열을 함수에 넘길 수 있게 됩니다. 이게 가능한 이유는 사실 우리가 만든 배열은 1 차원 배열들이지 2 차원 배열이 아니기 때문입니다. arr 은 단순히 int * 형 원소들을 가지는 1 차원 배열 이지요. 1 차원 배열을 함수의 인자로 넘겨줄 때 에는 크기를 써 주지 않아도 되지 않았습니까. 사실 main 함수의 인자로 전달되는 argv 역시 이와 같은 성격을 띕니다.

그렇다고 해서 2 차원 배열의 성질을 잃어버리는 것은 아닙니다. 이 배열도 2 차원 배열 처럼 arr[3][4] 과 같이 원소에 접근할 수 있습니다 (그렇기 때문에 우리가 만든 이 배열을 2 차원 배열이라 부르는 것입니다). 왜냐하면 arr[3][4]*(*(arr + 3)+4) 로 해석되는데, *(arr + 3) 을 통해 arr 의 네번째 원소에 접근하게 되고 *(arr + 3) 은 자신이 가리키는 int 형 배열의 주소값읠 의미하므로 + 4 를 하면 int 형 배열의 5 번째 원소에 접근하는 것과 같습니다.

아무튼 이와 같은 방법으로 2 차원 배열 (사실은 다르지만 이렇게 부르겠습니다) 를 생성하였습니다. 우리가 이 배열을 힙에 할당하였으면 사용이 끝났으면 역시 되돌려 주어야 하겠죠. 해제하는 순서는 할당하는 순서와 정 반대로 하면 됩니다. 즉, arr[i] 들이 가리키고 있던 int 배열들을 해제한 후, arr 을 해제하면 되겠지요. 만일 arr 을 먼저 해제하면 arr[i] 들이 메모리 상에서 사라지게 되므로 arr[i] 들이 가리키고 있던 int 배열들을 해제할 수 없게 되므로 오류가 나게 됩니다.

/* 2 차원 배열 동적 할당의 활용 */
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv) {
  int i, j, input, sum = 0;
  int subject, students;
  int **arr;
  // 우리는 arr[subject][students] 배열을 만들 것이다.

  printf("과목 수 : ");
  scanf("%d", &subject);

  printf("학생의 수 : ");
  scanf("%d", &students);

  arr = (int **)malloc(sizeof(int *) * subject);

  for (i = 0; i < subject; i++) {
    arr[i] = (int *)malloc(sizeof(int) * students);
  }

  for (i = 0; i < subject; i++) {
    printf("과목 %d 점수 --------- \n", i);

    for (j = 0; j < students; j++) {
      printf("학생 %d 점수 입력 : ", j);
      scanf("%d", &input);

      arr[i][j] = input;
    }
  }

  for (i = 0; i < subject; i++) {
    sum = 0;
    for (j = 0; j < students; j++) {
      sum += arr[i][j];
    }
    printf("과목 %d 평균 점수 : %d \n", i, sum / students);
  }

  for (i = 0; i < subject; i++) {
    free(arr[i]);
  }

  free(arr);

  return 0;
}

성공적으로 컴파일 하였다면

실행 결과

과목 수 : 3
학생의 수 : 2
과목 0 점수 --------- 
학생 0 점수 입력 : 90
학생 1 점수 입력 : 100
과목 1 점수 --------- 
학생 0 점수 입력 : 80
학생 1 점수 입력 : 70 
과목 2 점수 --------- 
학생 0 점수 입력 : 60
학생 1 점수 입력 : 100
과목 0 평균 점수 : 95 
과목 1 평균 점수 : 75 
과목 2 평균 점수 : 80 

와 같이 나옵니다. 대성공이군요!

int **arr;

위 예제에서 우리는 과목별 학생의 점수를 보관하기 위해 이차원 배열을 사용하였습니다. 즉 arr[subject][students] 를 만든 것이지요. 이를 위해

arr = (int **)malloc(sizeof(int *) * subject);

for (i = 0; i < subject; i++) {
  arr[i] = (int *)malloc(sizeof(int) * students);
}

를 통해 arr[subject][students] 를 만들 수 있었습니다. 따라서 이제 과목별 학생의 점수를

for (i = 0; i < subject; i++) {
  printf("과목 %d 점수 --------- \n", i);

  for (j = 0; j < students; j++) {
    printf("학생 %d 점수 입력 : ", j);
    scanf("%d", &input);

    arr[i][j] = input;
  }
}

로 얻었습니다. arr 은 사실 2 차원 배열은 아니지만 2 차원 배열과 똑같이 행동하므로 arr[i][j] 와 같은 문장도 맞게 되지요. arr[i][j]ij 열에 위치한 값이라 생각해도 무방합니다.

for (i = 0; i < subject; i++) {
  sum = 0;
  for (j = 0; j < students; j++) {
    sum += arr[i][j];
  }
  printf("과목 %d 평균 점수 : %d \n", i, sum / students);
}

이제 값을 모두 입력받았다면 각 과목별 평균을 내면 되는데 이는 간단히 위와 같은 for 문으로 해결할 수 있었습니다.

for (i = 0; i < subject; i++) {
  free(arr[i]);
}

free(arr);

마지막으로 할당 받은 메모리의 사용이 끝났기 때문에 해제해야 하는데 이는 이전에 설명했던 예제와 동일하게 하면 됩니다.

/* 할당한 (2 차원 배열 처럼 생긴) 배열 전달하기 */
#include <stdio.h>
#include <stdlib.h>

void get_average(int **arr, int numStudent, int numSubject);

int main(int argc, char **argv) {
  int i, j, input, sum = 0;
  int subject, students;
  int **arr;
  // 우리는 arr[subject][students] 배열을 만들 것이다.

  printf("과목 수 : ");
  scanf("%d", &subject);

  printf("학생의 수 : ");
  scanf("%d", &students);

  arr = (int **)malloc(sizeof(int *) * subject);

  for (i = 0; i < subject; i++) {
    arr[i] = (int *)malloc(sizeof(int) * students);
  }

  for (i = 0; i < subject; i++) {
    printf("과목 %d 점수 --------- \n", i);

    for (j = 0; j < students; j++) {
      printf("학생 %d 점수 입력 : ", j);
      scanf("%d", &input);

      arr[i][j] = input;
    }
  }

  get_average(arr, students, subject);

  for (i = 0; i < subject; i++) {
    free(arr[i]);
  }
  free(arr);

  return 0;
}
void get_average(int **arr, int numStudent, int numSubject) {
  int i, j, sum;

  for (i = 0; i < numSubject; i++) {
    sum = 0;
    for (j = 0; j < numStudent; j++) {
      sum += arr[i][j];
    }
    printf("과목 %d 평균 점수 : %d \n", i, sum / numStudent);
  }
}

성공적으로 컴파일 했다면

실행 결과

과목 수 : 2
학생의 수 : 3
과목 0 점수 --------- 
학생 0 점수 입력 : 100
학생 1 점수 입력 : 90
학생 2 점수 입력 : 80
과목 1 점수 --------- 
학생 0 점수 입력 : 70
학생 1 점수 입력 : 80
학생 2 점수 입력 : 90
과목 0 평균 점수 : 90 
과목 1 평균 점수 : 80 

와 같이 나옵니다. 다른 부분은 모두 똑같으므로 함수만 살펴봅시다.

void get_average(int **arr, int numStudent, int numSubject)

일단 void 형이고 int **arrnumStudent, numSubject 를 인자로 받고 있습니다. 앞에서 설명 했지만 arr 은 2 차원 배열 처럼 행동함에도 불구하고 사실은 단순히 원소가 int * 형인 배열이기 때문에 (1 차원 배열의 경우 단순히 배열의 타입에 * 만 붙이면 된다는 사실은 다 알고계시죠?) 위와 같이 int **arr 로 기존의 2 차원 배열 처럼 열의 개수에 대한 정보가 없어도 됩니다. (2 차원 배열의 경우 int (*arr)[3]과 같이 열에 관한 정보가 있어야 함)

물론 함수 내부에서 총 학생의 명수와 총 과목의 개수를 알아야 하므로 위와 같이 numStudentnumSubject 를 넣어주었지만요.

진짜 2 차원 배열 할당하기

주의 사항

애석하게도 이 방법은 비주얼 스튜디오에서는 작동하지 않습니다. 비주얼 스튜디오의 C 컴파일러가 VLA 를 지원하지 않기 때문입니다. 하지만 GCC 나 Clang 같은 컴파일러에서는 사용 가능한 방법입니다. 참고로 VLA 는 C99 에 표준으로 들어갔습니다만, 비주얼 스튜디오의 C 컴파일러는 C90 을 사용하고 있습니다.

앞서 보았던 방식으로는 진짜 2 차원 배열 처럼 메모리에 연속적으로 존재하는 배열을 만들 수 는 없습니다. 왜냐하면 1 차원 배열들을 쭈르륵 계속 할당하면서 메모리에 여기 저기에 퍼져서 만들어질 것이기 때문이죠.

따라서 메모리에 연속적으로 존재하는 진짜 2 차원 배열을 만들기 위해서는 반드시 malloc 을 통해 해당 크기의 공간을 할당해야 합니다. 예를 들어서

int arr[height][width];

와 같이 생긴 배열을 할당하고자 합시다. 이 경우 필요한 메모리의 크기는

height* width * sizeof(int)

가 되겠죠? 이렇게 메모리를 할당했으면, 이제 해당 메모리를 2 차원 배열이라고 생각해라! 라고 컴파일러에게 알려줘야 합니다. 따라서 이 경우 해당 메모리 주소값으 2 차원 배열을 가리키는 포인터에 전달하면 됩니다.

이전 강좌에서도 이야기 하였지만 2 차원 배열 포인터의 경우 포인터 연산을 수행하기 위해선 반드시 포인터 타입 안에 행 길이 가 들어가야 한다고 하였습니다. 따라서, 아래와 같이 2 차원 배열 포인터 arr 을 정의할 수 있습니다.

int (*arr)[width] = (int (*)[width])malloc(height * width * sizeof(int));

이제 arr 은 컴파일러 입장에서 아 행의 크기가 width 인 2 차원 배열을 가리키는구나! 라고 생각할 수 있겠죠. 한 가지 주의해야 할 점은 arr 을 정의할 때 반드시 width 에 실제 배열의 넓이 값이 들어간 후에 정의해야 합니다. 예를 들어�

int width;
int (*arr)[width] = (int (*)[width])malloc(height * width * sizeof(int));
scanf("%d", &width);

를 하게 되면 arr 이 제대로 2 차원 배열을 참조할 수 없습니다. 반드시

int width;
scanf("%d", &width);
int (*arr)[width] = (int (*)[width])malloc(height * width * sizeof(int));

와 같이 width 에 실제 2 차원 배열의 행 길이가 들어간 후에 배열 포인터를 선언해야겠죠.

주의 사항

arr 을 정의할 때 반드시 width 에 실제 배열의 넓이 값이 들어간 후에 정의합시다.

실제로 작동하는 코드를 살펴보면 다음과 같습니다.

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

int main() {
  int width, height;
  printf("배열 행 크기 : ");
  scanf("%d", &width);
  printf("배열 열 크기 : ");
  scanf("%d", &height);

  int(*arr)[width] = (int(*)[width])malloc(height * width * sizeof(int));
  for (int i = 0; i < height; i++) {
    for (int j = 0; j < width; j++) {
      int data;
      scanf("%d", &data);
      arr[i][j] = data;
    }
  }
  for (int i = 0; i < height; i++) {
    for (int j = 0; j < width; j++) {
      printf("%d ", arr[i][j]);
    }
    printf("\n");
  }

  free(arr);
}

성공적으로 컴파일 하였다면

실행 결과

배열 행 크기 : 3
배열 열 크기 : 2
1 2 3 4 5 6
1 2 3 
4 5 6 

와 같이 나옴을 알 수 있습니다. arr 의 모든 데이터가 메모리에 연속적으로 있으므로 free 역시 arr 에 대해 딱 한 번만 수행하면 됩니다. 간단하죠!

한 가지 주의할 점은 위 배열 포인터를 다른 함수에 인자로 전달하고 싶을 때 입니다.

예를 들어서 배열의 모든 원소들을 출력하는 함수를 만들고 싶다고 해봅시다. 그렇다면 다음과 같이 함수를 작성할 수 도 있을 것입니다.

void print_array(int (*arr)[width], int width, int height) {
  for (int i = 0; i < height; i++) {
    for (int j = 0; j < width; j++) {
      printf("%d ", arr[i][j]);
    }
    printf("\n");
  }
}

그런데 문제가 있습니다. 컴파일러가 배열의 정의인 int (*arr)[width] 을 보고 width 가 뭔지 알 수 없다는 것입니다. 실제로도 아래와 같은 컴파일 오류가 발생합니다.

컴파일 오류

test.c:12:29: error: ‘width’ undeclared here (not in a function)
   12 | void print_array(int (*arr)[width], int width, int height) {

해결책은 간단합니다. 컴파일러가 arr 의 정의를 볼 때 width 의 정체를 알 수 있도록, widtharr 의 정의 앞으로 오도록 순서를 바꿔주면 됩니다. 아래 처럼요.

void print_array(int width, int (*arr)[width], int height) {
  for (int i = 0; i < height; i++) {
    for (int j = 0; j < width; j++) {
      printf("%d ", arr[i][j]);
    }
    printf("\n");
  }
}

실제로 컴파일 해보면 잘 작동함을 알 수 있습니다.

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

void add_one(int width, int (*arr)[width], int height) {
  for (int i = 0; i < height; i++) {
    for (int j = 0; j < width; j++) {
      arr[i][j]++;
    }
  }
}

void print_array(int width, int (*arr)[width], int height) {
  for (int i = 0; i < height; i++) {
    for (int j = 0; j < width; j++) {
      printf("%d ", arr[i][j]);
    }
    printf("\n");
  }
}

int main() {
  int width, height;
  printf("배열 행 크기 : ");
  scanf("%d", &width);
  printf("배열 열 크기 : ");
  scanf("%d", &height);

  int(*arr)[width] = (int(*)[width])malloc(height * width * sizeof(int));
  for (int i = 0; i < height; i++) {
    for (int j = 0; j < width; j++) {
      int data;
      scanf("%d", &data);
      arr[i][j] = data;
    }
  }

  printf("----- Array ----- \n");
  print_array(width, arr, height);
  printf("----- Add one ----- \n");
  add_one(width, arr, height);
  print_array(width, arr, height);

  free(arr);
}

성공적으로 컴파일 하였다면

실행 결과

배열 행 크기 : 3
배열 열 크기 : 2
1 2 3 4 5 6
----- Array ----- 
1 2 3 
4 5 6 
----- Add one ----- 
2 3 4 
5 6 7 

와 같이 잘 처리됨을 알 수 있습니다.

주의 사항

만일 컴파일 오류가 발생한다면 C 컴파일러로 컴파일 되고 있는지 확인해보세요. 비주얼 스튜디오의 경우 파일 확장자를 .c 로 지정해야하고, GCC 를 사용하시는 분들은 g++ 이 아니라 gcc 로 컴파일 해야 합니다.

이를 바탕으로 이전의 점수 계산 코드를 수정해 본다면 아래와 같습니다.

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

void get_average(int num_student, int num_subject, int (*scores)[num_student]);

int main(int argc, char **argv) {
  int subject, students;

  printf("과목 수 : ");
  scanf("%d", &subject);

  printf("학생의 수 : ");
  scanf("%d", &students);

  // students 의 값이 정해진 후에 scores 을 정의해야 한다.
  int(*scores)[students];
  scores = (int(*)[students])malloc(sizeof(int) * subject * students);

  for (int i = 0; i < subject; i++) {
    printf("과목 %d 점수 --------- \n", i);

    for (int j = 0; j < students; j++) {
      printf("학생 %d 점수 입력 : ", j);
      scanf("%d", &scores[i][j]);
    }
  }

  get_average(students, subject, scores);
  free(scores);

  return 0;
}

void get_average(int num_student, int num_subject, int (*scores)[num_student]) {
  int i, j, sum;

  for (i = 0; i < num_subject; i++) {
    sum = 0;
    for (j = 0; j < num_student; j++) {
      sum += arr[i][j];
    }
    printf("과목 %d 평균 점수 : %d \n", i, sum / num_student);
  }
}

성공적으로 컴파일 하였다면

실행 결과

과목 수 : 2
학생의 수 : 3
과목 0 점수 --------- 
학생 0 점수 입력 : 100
학생 1 점수 입력 : 90
학생 2 점수 입력 : 80
과목 1 점수 --------- 
학생 0 점수 입력 : 80
학생 1 점수 입력 : 90
학생 2 점수 입력 : 99
과목 0 평균 점수 : 90 
과목 1 평균 점수 : 89 

와 같이 잘 작동됩니다.

그래서 어떠한 방식을 사용해야 하나?

되도록이면 연속된 공간에 2 차원 배열을 할당하는 후자의 방법을 취하는 것이 좋습니다.

  • malloc 은 상당히 느린 함수들 중에 하나 입니다. 따라서 malloc 의 호출 횟수를 최소한으로 하는 것이 좋습니다. 따라서 배열의 높이 만큼 malloc 을 호출해야 하는 전자의 방법은 height 가 커질 수록 상당히 느려집니다.

  • 전자의 방법으로 메모리의 원소에 접근하기 위해서는 두 단계의 메모리 접근이 필요합니다. 예를 들어서 arr[3][2] 의 경우 먼저 arr[3] 을 읽은 뒤에, 해당 주소에서 다시 [2] 연산을 해야 하죠. 반면에 후자의 경우 컴파일러가 다이렉트로 메모리 arr[3][2] 에 접근할 수 있습니다.

  • 메모리가 연속적으로 있을 경우 접근이 더 빠릅니다.

물론 후자의 방식의 경우 배열 포인터를 사용하기 때문에 배열의 선언이 조금 길어지는 면이 있지만 그 정도의 귀찮음은 감수할 만하다고 생각합니다.

이 강좌를 끝으로 여러분은 C 언어의 대부분을 배웠다고 해도 무방합니다만, 아직 몇 가지 재미있는 것들이 남아있으니 다음 강좌가 나올 때 까지 생각해 볼 문제나 풀어보세요 :)

생각해보기

문제 1

위 성적 프로그램을 개량하여 학생별 평균을 내어 학생의 등수를 출력하는 프로그램을 만들어보세요 (난이도 : 下)

문제 2

동적으로 할당된 배열의 크기를 다시 바꾸는 프로그램을 만들어보세요.즉 p 가 이미 원소가 10 인 동적으로 할당된 배열을 가리키고 있었는데 예상치 못하게 원소 5 개를 더 추가하려면 어떻게 해야 할까요. (난이도 : 中)

강좌를 보다가 조금이라도 궁금한 것이나 이상한 점이 있다면 꼭 댓글을 남겨주시기 바랍니다. 그 외에도 강좌에 관련된 것이라면 어떠한 것도 질문해 주셔도 상관 없습니다. 생각해 볼 문제도 정 모르겠다면 댓글을 달아주세요.

현재 여러분이 보신 강좌는 <씹어먹는 C 언어 - <20 - 1. 동동동 메모리 동적할당(Dynamic Memory Allocation)>> 입니다. 이번 강좌의 모든 예제들의 코드를 보지 않고 짤 수준까지 강좌를 읽어 보시기 전까지 다음 강좌로 넘어가지 말아주세요
댓글이 83 개 있습니다!
프로필 사진 없음
강좌에 관련 없이 궁금한 내용은 여기를 사용해주세요

    댓글을 불러오는 중입니다..