11-1.첨자 연산

11-1-가.배열의 내부적 처리

동일한 타입의 변수 집합인 배열은 정보를 저장하는 가장 기본적인 자료 구조로서 실용성이 높다. 포인터는 조금 어렵기는 하지만 C언어를 다른 언어와 구분하는 가장 큰 특징이다. 이 둘은 아주 긴밀한 연관을 맺고 있으며 상호 보완적이면서 또한 일정 부분에 있어서는 대체도 가능하다. 포인터를 사용하면 배열을 그야말로 떡 주무르듯이 마음대로 주무를 수 있고 배열은 포인터를 만날 때 진정한 가치를 발휘한다. 배열의 정보 저장 능력과 포인터의 정보 가공 능력이 결합되면 복잡한 현실 문제들을 해결하는 데 아주 강력한 무기가 된다.

이 장에서는 이런 긴밀한 연관을 맺고 있는 배열과 포인터를 같이 살펴 본다. 배열도 쉽지 않고 포인터도 어려운데 이 둘을 같이 공부하자면 머리가 과열되지 않을까 걱정스러울 것이다. 배열과 포인터 여기에 구조체까지 합세하면 표현식도 복잡해지고 말도 꼬여 어지간히 혼란스러울 것 같지만 오히려 그 반대이다. 배열과 포인터는 닮은 점이 있고 또 틀린 점이 있는데 이런 유사점과 차이점을 분명하게 살펴보고 실습을 통해 눈으로 직접 확인해 봄으로써 오히려 배열과 포인터의 개념을 더 깔끔하게 정리할 수 있는 기회로 삼아야 한다.

언어의 문법이란 지극히 논리적이고 질서가 있는데 이런 규칙들이 정리되면 문법이 오히려 재미있어지고 응용력이 생긴다. 이 장의 내용을 다 이해하고 나면 배열과 포인터 각각에 대해 확실한 이해를 할 수 있음은 물론이고 이 둘을 자유자재로 활용할 수 있는 진정한 자유를 얻게 될 것이다. 9장에서 우리는 배열의 정의에 대해 공부했고 첨자가 0부터 시작되며 초기화되지 않고 끝 점검을 하지 않는다는 배열의 특징에 대해서도 알아보았다. 이제 좀 더 깊은 곳으로 들어가 C언어가 배열을 내부적으로 어떻게 다루는지를 알아보자. C언어의 배열은 다음과 같은 두 가지 특징을 가진다.

 

C는 내부적으로 1차원 배열만 지원한다. 2차원 이상의 다차원 배열은 1차원 배열의 확장에 불과하다. C에는 2차원 배열이라는 것이 없다.

배열을 구성하는 배열 요소의 타입에는 전혀 제한이 없다. T형 변수를 선언할 수 있으면 T형 배열도 언제나 선언할 수 있다. 배열도 유도형 타입의 일종이며 따라서 배열 그 자체가 배열의 요소가 될 수 있다.

 

C가 다차원 배열을 지원하지 않는다는 얘기는 C의 모든 배열은 내부적으로 1차원이라는 뜻이다. 하지만 배열 요소로 또 다른 배열을 사용할 수 있으므로 즉, 배열끼리 중첩이 가능하기 때문에 외부적으로는 다차원 배열도 지원하는 셈이다. 배열끼리 중첩되어 있을 때 다른 배열에 포함된 배열을 부분 배열(SubArray)이라고 하며 부분 배열을 배열 요소로 가지는 배열을 전체 배열(또는 모배열)이라고 한다.

다차원 배열의 가장 간단한 예인 int ar[3][4] 이차원 배열의 예를 들어 보자. 이 배열을 그림으로 그려 보면 다음과 같이 메모리에 생성될 것이다.

ar 전체 배열은 ar[0] 부분 배열과 ar[1], ar[2] 부분 배열 세 개를 요소로 가지는 1차원 배열이며 ar[0] 부분 배열은 ar[0][0]~ar[0][3]까지의 정수형 변수를 배열 요소로 가지는 1차원 배열이다. ar[0] 부분 배열이 배열이 될 수 있는 이유는 이 배열의 요소가 전부 정수형으로 타입이 같기 때문이다. 마찬가지로 ar 전체 배열의 경우도 배열 요소인 ar[0], ar[1], ar[2]의 타입이 모두 동일하므로(그것이 배열이든 어쨌든) 분명히 배열이라고 할 수 있다. ar 배열을 다른 형태로 다시 그려 보면 다음과 같이 그릴 수 있다.

ar배열은 세 개의 배열 요소를 가지는데 각 배열 요소인 ar[0], ar[1], ar[2]는 정수형 부분 배열이다. ar[0] 부분 배열은 다섯 개의 배열 요소를 가지는데 각 배열 요소는 정수형 변수이다. 결국 전체 배열 ar속에 12개의 정수형 변수가 모여 있는 셈이지만 ar 배열에 직접 속해 있는 요소는 세 개의 부분 배열이며 따라서 ar 배열은 1차원 배열이다. 이번에는 한단계 더 확장해서 char arr[3][2][4]로 선언된 3차원 배열의 경우를 생각해 보자.

 

-전체 배열 arr은 arr[0], arr[1], arr[2] 세 개의 부분 배열을 요소로 가지는 크기 3의 1차 배열이며

-부분 배열 arr[0]는 arr[0][0], arr[0][1] 두 개의 부분 배열을 요소로 가지는 크기 2의 1차 배열이며

-부분 배열 arr[0][0]는 arr[0][0][0]~arr[0][0][3] 네 개의 문자형 변수를 요소로 가지는 크기 4의 1차 배열이며

-최종 배열 요소인 arr[0][0][0]는 문자형이므로 더 이상 배열이 아닌 단순 변수이다.

 

3차원 배열은 배열의 배열의 배열인 셈이다. 그렇다면 도대체 2차원 배열과 배열의 배열을 왜 굳이 구분하려고 하는가? C가 내부적으로 2차원 배열을 1차원 배열로 취급하든 말든 선언할 때나 사용할 때 두 개의 첨자를 사용하면 되므로 밖에서 볼 때는 완전한 2차원 배열이다. 배열의 배열이라는 이상한 용어를 만들어 가면서 C가 다차원 배열을 지원하지 않는다는 주장은 다소 억지스러워 보이기도 한다.

배열을 단순히 사용하기만 한다면 과연 2차원 배열로 보든 배열의 배열로 보든 전혀 차이점이 없다. 그러나 배열이 내부적으로 어떻게 처리되는가에 따라서 큰 차이점이 발생하는데 바로 첨자 연산 방법과 부분 배열의 자격 문제가 달라진다. 다차원 배열에서는 부분 배열만 단독으로 사용할 수 없지만 배열의 배열에서는 부분 배열 단독으로도 배열로 인정된다는 큰 차이점이 있다. 다음 예제는 부분 배열이 온전하게 배열로 대접받는다는 것을 보여준다.

 

: SubArray

#include <Turboc.h>

void arDump(void *array, int length);

 

void main(void)

{

     unsigned char ari[2][3]={{1,2,3},{4,5,6}};

 

     arDump(ari,sizeof(ari));

     arDump(ari[0],sizeof(ari[0]));

     arDump(ari[1],sizeof(ari[1]));

}

 

void arDump(void *array, int length)

{

     int i;

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

          printf("%02X ",*((unsigned char *)array+i));

     }

     printf("\n");

}

 

arDump는 앞장에서 작성한 배열 덤프 함수인데 배열과 길이를 인수로 전달하면 배열의 내용을 16진수로 출력한다. 실행 결과는 다음과 같다.

 

01 02 03 04 05 06

01 02 03

04 05 06

 

ari는 문자형의 2차원 배열로 선언되었는데 arDump 함수로 ari를 전달하나 ari[0], ari[1]을 전달하나 모두 정상적으로 동작한다. 이 예에서 보다시피 ari[0]라는 부분 배열이 배열명으로 인정되며 부분 배열 혼자만 떼어내서 사용하는 것이 가능하다. ari[0]는 배열의 이름이기 때문에 부분 배열의 시작 번지를 가리키는 포인터 상수이며 따라서 arDump 함수로 전달할 수 있다.

베이직같은 고급언어는 다차원 배열만 지원하기 때문에 부분 배열이라는 개념이 없으며 ari[0]라는 표현식 자체가 허용되지 않는다. 오로지 전체 배열을 통해 배열의 최하위 요소에 접근할 수 있을 뿐이다. 반면 C는 부분 배열만 단독으로 사용할 수 있다. 다음은 좀 더 간단한 예제이다.

 

: SubArray2

#include <Turboc.h>

 

void main(void)

{

     char ar[]="한국을 빛낸 사람들";

     char ars[2][3][10]={

          {"이순신","강감찬","김유신"},

          {"유관순","을지문덕","신사임당"}

     };

 

     printf("ar 배열 = %s\n",ar);

     printf("ars[1][1]=%s\n",ars[1][1]);

}

 

ar은 1차원 문자형 배열이므로 ar 배열명은 문자열이 들어있는 시작 위치를 가리키는 포인터 상수이며 따라서 printf의 %s 서식과 대응된다. ar이라는 배열의 이름이 곧 문자열이 되는 것이다. 문자형 3차 배열로 선언된 ars는 문자형 1차 배열의 배열의 배열이라고 할 수 있다. ars[1][1]은 ars 전체 배열의 두 번째 배열 요소인 ars[1] 부분 배열의 두 번째 배열 요소이며 크기 10의 문자형 1차 부분 배열이다.

전체 배열에 속해 있는 부분 배열이지만 그 자체로 독립성을 가지고 있다. ars[1][1] 부분 배열이 하나의 배열이므로 ars[1][1]이라는 명칭은 배열의 이름이 되며 정의에 의해 배열명은 포인터 상수이다. ars[1][1]이 문자형 포인터이므로 printf의 %s 서식과 대응될 수 있으며 puts 함수의 인수로 사용될 수도 있고 문자열이 필요한 모든 곳에 사용할 수 있다.