11-3-마.이차 배열 할당

다음 두 항의 주제는 다소 어려운 문법이라 처음 책을 읽는 사람들에게는 혼란스러울 것으로 예상된다. 그래서 이 내용은 지금 당장 읽기보다는 좀 더 실무 경험을 쌓은 후에 다시 보기 바란다. 실제로 현업에서 활용되는 실용성이 있는 내용이라기 보다는 배열과 포인터에 대한 이론적인 연구를 위한 학술적인 내용이라고 할 수 있다. 따라서 금방 이해되지 않는다 하여 고민할 필요까지는 없으며 만약 잘 이해된다면 포인터에 대한 이해가 충분히 깊어졌다고 생각해도 좋다.

동적 할당 기능을 사용하면 실행중에 원하는 크기대로 배열을 할당할 수 있다. 그렇다면 2차원 배열도 동적으로 할당할 수 있을까? 문자형의 2차원 배열을 정적으로 선언하는 문장은 char ar[3][4]가 되는데 이 선언문의 상수 3과 4를 실행 중에 결정하고 싶은 것이다. 이만큼의 메모리를 실행중에 만들려면 malloc(3*4*sizeof(char))를 호출하여 12바이트를 할당하면 된다. 이때 malloc이 리턴하는 포인터를 어떤 타입의 변수로 받아야 하는가가 문제다. 일단 다음과 같이 해 보자.

 

char *p=(char *)malloc(3*4*sizeof(char));

free(p);

 

배열의 최종 요소가 char형이므로 char *로 받았다. 그러나 이렇게 하면 길이 12의 문자형 일차 배열을 할당한 것이지 2차원 배열을 할당한 것은 아니다. 원래 할당하려고 했던 char ar[3][4]는 길이 4의 문자열 3개를 의도한 것인데 이 할당에 의해 만들어진 p는 길이 12의 문자열 하나에 불과하므로 2차원 배열로 사용할 수 없다. 할당한 길이만 같을 뿐이다. 이 배열을 동적으로 할당하려면 배열 포인터로 받아야 한다.

 

: malloc2array

#include <Turboc.h>

 

void main()

{

     int i;

 

     char (*p)[4]=(char (*)[4])malloc(3*4*sizeof(char));

     strcpy(p[0],"dog");

     strcpy(p[1],"cow");

     strcpy(p[2],"cat");

     for (i=0;i<3;i++) puts(p[i]);

     free(p);

}

 

크기 4의 문자형 배열을 가리키는 배열 타입에 대해 크기 3으로 할당했으며 각 부분 배열에 문자열이 잘 들어가는지 확인하기 위해 3자짜리 문자열들을 복사한 후 다시 확인 출력해 보았다. 배열의 2차 첨자 길이는 4이지만 널 문자를 고려해야 하므로 실제로는 3자까지밖에 저장하지 못한다. 이 예제에서 할당한 p는 char ar[3][4] 정적 배열을 동적으로 할당한 것이며 2차원 배열처럼 사용할 수 있다.

그러나 사실 이 예제가 동적으로 할당한 것은 2차원 배열이 아니라 1차원 배열이라고 보는 것이 더 타당하다. 동적 할당이란 실행중에 크기를 결정하고 싶을 때 사용하는 것인데 이 예제에서 할당한 p의 크기는 컴파일중에 결정된 것이기 때문이다. 좀 더 단순한 정수형 일차원 배열을 예로 들면 다음과 같은 할당이 진정한 동적 할당이다.

 

int n=5;

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

free(pi);

 

이 코드는 크기 5의 정수형 1차 배열을 할당하는데 이때 크기를 지정하는 n은 상수가 아니라 변수이다. 사용자가 입력한 값이나 함수의 인수로 전달된 값을 배열 크기로 지정하면 할당할 크기를 외부에서 주어진 값으로 선택할 수 있다. 만약 n이 5로 고정되어 있다면 굳이 pi를 동적할당할 필요없이 int pi[5] 선언문으로 정적 배열로 선언하는 것이 더 편리할 것이다. 예제의 2차 배열도 변수로 폭과 높이를 지정할 수 있는지 점검해 보자. 먼저 다음과 같이 수정해 본다.

 

int n=3;

char (*p)[4]=(char (*)[4])malloc(n*4*sizeof(char));

free(p);

 

일차 첨자의 크기를 변수 n으로 지정했는데 아무 이상없이 잘 컴파일된다. 이제 n만 더 크게 주면 얼마든지 더 큰 2차 배열을 할당할 수 있다. 이번에는 일차 첨자 크기인 4도 변수로 바꿔서 할당해 보자.

 

int n=3,m=4;

char (*p)[m]=(char (*)[m])malloc(n*m*sizeof(char));

free(p);

 

이 코드는 제대로 컴파일되지 않는다. 왜냐하면 배열 포인터의 대상체 배열의 크기값은 상수로만 줄 수 있기 때문이다. p가 어떤 배열을 가리킬 것인가는 실행중에 결정할 수 없으며 컴파일할 때 이미 결정되어 있어야 한다. 모든 포인터는 대상체의 크기를 알아야 하는데 예제의 p는 대상체의 크기가 실행중에 결정되는 변수이기 때문이다. p의 선언문이 컴파일되지 않는 이유는 int ar[n];이 컴파일되지 않는 이유와 동일하다. 위 코드에서 m을 const int로 선언하면 컴파일된다.

이 테스트에서 보다시피 위 예제에서 할당한 p는 2차원 배열이 아니라 크기가 정해진 1차원 배열을 요소로 가지는 1차원 배열, 즉 배열의 배열인 것이다. 두 첨자의 크기를 실행중에 마음대로 변경할 수 없으므로 제대로 2차원 배열을 할당했다고 볼 수 없다. 잘 이해가 가지 않으면 예제를 다음과 같이 수정해 놓고 테스트해 보자.

 

int i;

typedef char c4[4];

 

c4 *p=(c4 *)malloc(3*sizeof(c4));

strcpy(p[0],"dog");

strcpy(p[1],"cow");

strcpy(p[2],"cat");

for (i=0;i<3;i++) puts(p[i]);

free(p);

 

이 코드는 예제의 코드와 완전히 같으며 동작도 동일하다. c4를 크기 4의 문자형 배열로 타입 정의하고 c4의 1차원 배열을 동적으로 할당했다. 이때 malloc의 첫 번째 인수 3은 상수가 아닌 변수로 줄 수 있다. 그러나 c4의 정의문에 있는 4는 타입의 일부이므로 변수로 지정할 수 없으며 컴파일시에 정해져야 한다. 원래 의도했던 바대로 두 첨자를 실행중에 마음대로 결정하도록 하려면 이중 포인터를 사용해야 한다.

 

: malloc2array2

#include <Turboc.h>

 

void main()

{

     int n=3,m=4;

     int i;

 

     char **p;

     p=(char **)malloc(n*sizeof(char *));

     for (i=0;i<n;i++) {

          p[i]=(char *)malloc(m*sizeof(char));

     }

     strcpy(p[0],"dog");

     strcpy(p[1],"cow");

     strcpy(p[2],"cat");

     for (i=0;i<n;i++) puts(p[i]);

     for (i=0;i<n;i++) {

          free(p[i]);

     }

     free(p);

}

 

문자형의 이중 포인터 p를 선언하고 p를 먼저 동적 할당한다. 이때 p 자체의 크기는 변수로 지정할 수 있다. 이 할당에 의해 p는 크기 3의 문자형 포인터 배열이 된다. 그리고 각각의 p요소를 다시 동적할당하되 이때는 이차 첨자 m의 크기만큼 할당했다. 두 첨자 n과 m을 모두 변수로 지정할 수 있으므로 실행중에 2차원 배열의 폭과 높이를 결정할 수 있다. 할당 결과 p가 메모리에 생성된 모양은 다음과 같이 그릴 수 있다.

메모리의 여기저기에 흩어져서 할당되어 있기는 하지만 분명히 실행중에 할당된 이차원 배열이라고 할 수 있다. 정적으로 할당된 char ar[3][4]와 사용 용도가 동일하며 각 요소를 참조하는 방법도 같다. 그러나 완전히 같다고는 할 수 없는데 정적 배열은 부분 배열의 크기가 일정하지만 이런 식으로 이중 포인터를 사용하여 두 번 할당할 경우는 부분 배열의 크기를 각각 다르게 지정할 수도 있다는 차이점이 있다. 즉 직사각형(Rectangular) 배열이 아니라 들쭉 날쭉한(Ragged) 배열이다. 2단계로 할당했으므로 이 배열을 해제할 때도 요소들을 먼저 해제하고 전체 배열을 해제하는 2단계를 거쳐야 한다.

그렇다면 원래의 논의로 돌아가 이차원 배열을 동적으로 할당할 수 있는가라는 명제의 진위 여부를 가려 보자. 문법적인 견지에서 볼 때 이차원 배열을 한 번에 할당하는 것은 불가능하다. 두 첨자의 크기를 실행중에 모두 결정할 수는 없기 때문이다. 그러나 실질적으로는 이차 배열처럼 동작하는 이중 포인터로 동일한 형태의 배열을 만들 수 있으므로 가능하다고 볼 수도 있다.