11-3.배열 포인터

11-3-가.배열 포인터

포인터 배열은 바로 앞 절에서 배웠고 이 절에서는 배열 포인터에 대해 알아 본다. 이 둘은 이름이 비슷해서 다소 혼란스러울 수 있는데 이름만 비슷하지 완전히 다른 것이다. 배열 포인터는 사용 빈도가 그리 높지 않으며 실용성도 떨어진다. 문법적 대칭성을 위해 존재하는데 포인터 배열이 있으니 배열 포인터도 당연히 있어야 하기 때문이다. 하지만 포인터를 완전하게 이해하기 위해서는 개념을 잘 알아 두어야 한다. 포인터 배열과 배열 포인터를 비교해 보면 다음과 같다.

 

포인터 배열(Array of Pointer) : 그 원소가 포인터인 배열이다. 각각의 배열 요소인 포인터가 가리키는 대상은 원칙적으로 임의의 타입을 가질 수 있지만 주로 문자형을 가리키는 경우가 많다.

배열 포인터(Pointer to Array) : 배열의 번지를 담는 포인터 변수이다. 포인터가 가리키는 대상은 배열형으로 구성되어 있으며 포인터가 가리키는 배열의 요소는 임의의 타입을 가진다.

 

둘 다 뒤쪽에 강세를 두고 읽으면 실체를 파악하는데 다소 도움이 될 것이다. 포인터 배열은 결국은 배열이고 배열 포인터는 결국은 포인터이다. 지금 한참 배우는 입장에서는 이 둘을 같이 공부하자면 무척 골머리가 아프고 짜증날 것이다. 한단계 더 나가면 배열 포인터 배열이나 포인터 배열 포인터같은 것들도 있을 수 있으며 이런 것들이 실제로 사용된다. 여기에 구조체까지 가세하면 더 복잡해지는데 원리만 파악하면 모두 별 것도 아니다.

배열 포인터는 배열이 여러 개 모여 있는 배열의 배열, 그러니까 2차원 이상의 배열에서만 의미가 있다. 1차원 배열에 대한 배열 포인터라는 것은 없다. 일차원 배열 int ar[5]를 가리키는 포인터가 필요하다면 int *pa; 로 선언하고 pa=ar로 초기화한다. pa로 ar[0], ar[1] 등의 정수형 변수들을 가리킬 수 있는데 이때 pa가 가리키는 대상은 ar 배열의 요소이지 ar 배열 그 자체가 아니기 때문에 pa는 단순한 정수형 포인터 변수에 불과하며 배열 포인터가 아닌 것이다.

1차원 배열은 부분 배열의 개념이 없으므로 배열 포인터를 선언할 수 없으며 선언할 필요도 없다. 최소한 2차원 이상이어야 전체 배열의 요소가 부분 배열이 되며 부분 배열을 가리키는 배열 포인터를 선언할 수 있다. 다차원 배열의 대표격인 2차원 배열의 포인터를 선언하는 방법은 다음과 같다.

 

요소형 (*포인터명)[2 첨자 크기]

 

선언문의 모양이 다소 생소한데 이 선언문에서 괄호는 생략할 수 없으며 반드시 필요하다. 만약 괄호를 생략해 버리면 배열 포인터가 아닌 포인터 배열이 되어 버린다. 배열 포인터 선언시 2차 첨자 크기는 반드시 밝혀야 하는데 이 크기를 알아야 가리킬 배열(곧 대상체)의 전체 크기를 구할 수 있기 때문이다.

포인터는 자신이 가리킬 대상의 크기를 알아야 *연산자로 값을 읽을 수 있고 ++, -- 연산자로 앞뒤 요소로 이동할 수 있다. 그래서 요소의 타입과 2차 첨자 크기에 대한 정보가 필요한데 쉽게 말해 어떤 녀석들이 얼마나 모여 있는지 알아야 하는 것이다. 그러나 1차 첨자 크기는 밝히지 않아도 되며 밝힐 필요도 없다. 왜냐하면 포인터는 자신이 가리키는 번지의 앞뒤에 동일 타입의 데이터가 임의의 개수만큼 있다고 가정하고 이동할 수 있어야 하기 때문이다.

3차 이상은 2차 이후의 첨자만 적고 그 앞에 (*변수명)만 붙이면 된다. 예를 들어 int ar[2][3][4] 배열을 가리키는 배열 포인터는 int (*par)[3][4]로 선언한다. 다음 예제가 배열 포인터를 사용하는 가장 간단한 예제이다.

 

: ArrayPointer

#include <Turboc.h>

 

void main(void)

{

     char arps[5][9]={"고양이","개","오랑우탄","돼지","지렁이"};

     char (*ps)[9];

 

     ps=arps;

     int i;

 

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

          printf("%s\n",*ps++);

     }

}

 

arps는 이차원 문자형 배열로 선언되었으며 각 부분 배열은 동물 이름으로 초기화되어 있다. 가장 긴 동물의 이름이 "오랑우탄"이며 널 문자까지 고려하여 최대 9자까지 저장해야 하므로 2차 첨자는 9로 선언되어 있다. 이 배열의 부분 배열을 가리키는 배열 포인터는 다음과 같이 선언한다.

 

char (*ps)[9];

 

만약 이 선언문에서 괄호를 생략하고 char *ps[9];로 선언한다면 이것은 문자형을 가리키는 포인터 9개를 요소로 가지는 포인터 배열이 되어 버릴 것이다. 이 선언에 의해 ps 배열 포인터는 자신이 가리킬 대상체가 크기 9의 문자형 배열이라는 것을 알 수 있다. 따라서 *ps로 읽으면 크기 9의 문자형 배열(곧 문자열)이 읽혀지고 ps++, ps--는 대상체의 크기인 9바이트만큼 앞 뒤로 이동하게 된다.

ps는 크기 9의 문자형 배열을 대상체로 가지므로 이 변수에 값을 대입할 때도 타입에 맞는 대상체의 번지를 대입해야 한다. 예제에서는 arps 배열 자체를 대입했는데 arps 배열명은 이 배열의 시작 번지를 가리키는 포인터 상수이고 이 번지는 곧 &arps[0]와 같다. arps[0]는 분명히 크기 9의 문자형 배열이므로 이 배열의 번지를 ps에 대입할 수 있는 것이다.

ps가 arps[0]를 가리키고 있는 상태에서 *연산자로 이 배열을 읽으면 배열의 내용이 읽혀질 것이다. 그리고 ++ 연산자로 다음 대상체로 이동하면 arps[1]로 이동하고 다시 한 번 ++ 연산자를 적용하면 arps[2]로 이동한다. 예제에서는 루프를 다섯 번 돌면서 ps가 가리키는 부분 배열을 순서대로 읽어 화면으로 출력하였다.

배열 포인터를 사용하여 부분 배열들을 순서대로 읽는 예제를 만들어 봤는데 사실 이 경우는 굳이 배열 포인터를 사용하지 않아도 된다. 다음과 같이 arps에 [ ] 연산자를 사용해도 부분 배열을 읽을 수 있다.

 

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

     printf("%s\n",arps[i]);

}

 

arps[i]가 결국 *(arps+i)이므로 arps가 이동하나 i를 증가시키나 같은 것이다. int ar[5]; int *pi=ar; 에서 *pi++과 ar[n]이 모두 가능한 것과 마찬가지이다. 그렇다면 배열 포인터가 꼭 필요한 상황은 과연 언제인지 다음 항에서 알아보자.