10-4-다.동적 문자열 배열

이미 알아보았듯이 배열을 선언할 때 그 크기는 반드시 상수로 지정해야 한다. 그래서 다음과 같이 선언할 수는 없다.

 

int len=원하는 값;

char name[len];

 

여기서 len은 함수의 인수로 전달되었거나 또는 사용자로부터 입력된 값, 즉 실행중에 결정되는 값이라고 생각하자. 컴파일러는 배열을 선언할 시점에 크기를 알아야 하므로 변수로 배열 크기를 지정할 수는 없으며 실행중에 가변적인 크기의 배열을 생성하려면 동적 메모리 할당 함수인 malloc 함수를 사용해야 한다. 앞에서 이미 실습해 본 내용인데 이를 좀 더 일반화하여 동적 할당을 통해 가변 크기의 배열을 만드는 공식을 유도해 보자. 임의의 타입 T형의 요소 n개를 가지는 배열을 실행중에 생성하는 코드는 다음과 같이 정리할 수 있다.

 

T *p;

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

 

총 할당 크기는 개수 n에 T형 타입의 크기를 곱한 바이트 수이며 malloc 함수로 할당되는 메모리는 이름이 없기 때문에 시작 번지를 반드시 포인터 변수로 대입받아야 사용할 수 있다. 그래서 T형 포인터 p를 선언하고 malloc이 리턴하는 번지를 대입하되 (T *) 타입으로 캐스팅했다. 동적 할당의 결과 p는 T형 요소 n개를 가지는 배열처럼 사용할 수 있다. 이 공식에 따라 크기 len의 문자형 배열을 생성하는 코드는 다음과 같이 작성된다.

 

int len;

scanf("%d",&len);

char *name;

name=(char *)malloc(len*sizeof(char));

// name 사용

free(name);

 

len의 값은 실행중에 주어지며 malloc은 이 크기만큼의 문자형 배열을 할당한다. len이 10이건 1000이건 상관없이 말이다. 이 할당에 의해 name은 len의 크기를 가지는 문자형 배열이 되며 이는 곧 len-1개의 문자를 저장할 수 있는 문자열이 된다는 뜻이다. 이 코드를 작성하는 방법을 이해했다면 똑같은 방법대로 좀 더 차원이 높은 코드도 작성할 수 있게 된다.

, 이제 요구를 좀 더 확장하여 이런 문자열(=문자형 배열)이 여러 개 필요하며 그 개수도 실행중에만 알 수 있다고 하자. 즉 가변 길이(len)의 문자형 배열을 가변 개수(num)만큼 생성해야 하는데 방법은 앞에서 name 배열을 만든 것과 동일하다. 결과 코드를 보이면 다음과 같다.

 

: DynStrArray

#include <Turboc.h>

 

void main()

{

     int len=10,num=5,i;

     char **name;

 

     name=(char **)malloc(num*sizeof(char *));

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

          name[i]=(char *)malloc(len*sizeof(char));

     }

 

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

          sprintf(name[i],"string %d",i);

          puts(name[i]);

     }

 

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

          free(name[i]);

     }

     free(name);

}

 

len은 문자열의 길이이며 num은 이런 문자열의 개수인데 둘 다 변수이므로 실행중에 사용자나 외부에서 주어지는 값이다. 예제에서는 편의상 선언문에서 10과 5로 초기화했지만 이 값은 언제든지 바뀔 수 있다. 문제를 요약하자면 len 길이의 char *형을 요소로 가지는 num 크기의 배열을 동적으로 할당하는 것이다. 요소의 타입은 char *이고 크기는 num이므로 공식대로 작성해 보면 다음과 같은 코드를 얻을 수 있다.

 

char **p;

p=(char **)malloc(num*sizeof(char *))

 

만약 이 문장이 이해되지 않는다면 typedef char *PC 선언으로 char *형을 PC 타입으로 정의한 후 공식에 따라 PC형 배열을 동적으로 할당하는 코드를 작성해 보아라. 그리고 이렇게 작성된 코드에서 PC를 다시 char *로 대체하면 바로 위의 문장이 나온다. char형의 배열을 할당할 때 char *형 변수가 필요한 것처럼 char *형의 배열을 동적 할당할 때는 char **형, 즉 이중 포인터 변수가 필요한 것이다.

이 할당에 의해 name은 num개의 char *형으로 구성된 배열이 되는데 malloc은 할당만 하지 초기화는 하지 않으므로 name 배열은 쓰레기 값을 가지고 있을 것이다. 이제 0~num까지 루프를 돌며 name의 각 요소에 대해 다시 메모리를 할당하여 len 길이의 문자형 배열을 만들면 name은 2차원 문자형 배열이 되며 개념적으로 1차원 문자열 배열이라고 할 수 있다. 할당이 완료된 후의 메모리 모양을 그려 보면 다음과 같다.

정적으로 선언한 변수는 name뿐이며 나머지는 모두 동적으로 할당된 것들이다. name이 가리키는 곳에는 크기 num의 char * 배열이 있고 이 배열의 요소들이 가리키는 곳에는 또 len 길이의 char 배열이 있다. 역으로 말하자면 char 배열은 char *에 의해 참조되며 이런 char *들의 집합을 name이 가리키고 있으니 name의 타입은 char **가 되어야 하는 것이다.

각 문자열들이 메모리상의 어디에 할당될지는 알 수 없지만 name을 통해 이 문자열의 시작 번지를 가지는 배열을 찾을 수 있고 이 배열로부터 각 문자열에 접근한다. 이 경우 name은 동적으로 할당된 문자열에 접근할 수 있는 유일한 경로이며 그 자체는 개념적으로 문자열 배열의 자격을 가진다. 과연 name이 복수 개의 문자열을 잘 저장하는지 확인해 보기 위해 루프를 돌며 "string n"이라는 문자열을 저장해 보고 출력도 해 보았다. 물론 잘 된다.

 

string 0

string 1

string 2

string 3

string 4

 

동적으로 할당한 배열을 해제할 때는 name 배열의 각 요소가 가리키는 메모리 블록을 순서대로 해제하고 마지막으로 name이 가리키는 배열을 해제한다. 순서를 바꿔 해제하면 하위 블록의 번지를 잃어버리므로 일부를 해제할 수 없게 될 것이다. name 그 자체는 지역적으로 선언된 포인터 변수이므로 해제할 필요가 없다.