11-3-다.배열 인수 표기법

배열에 대해 어떤 반복적인 처리가 필요하다고 해 보자. 일정한 코드가 계속적으로 반복된다면 함수로 분리해야 하는데 이때 작업 대상이 되는 배열을 함수로 전달할 필요가 있을 것이다. 다음 예제는 배열의 내용을 화면으로 출력하는 함수 OutArray를 정의한다.

 

: ArrayPara1

#include <Turboc.h>

 

void OutArray(int ar[5])

{

     int i;

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

          printf("%d번째 = %d\n",i,ar[i]);

     }

}

 

void main(void)

{

     int ar[]={1,2,3,4,5};

 

     OutArray(ar);

}

 

main에서 크기 5의 정수형 배열 ar을 선언했으며 이 배열을 OutArray로 전달하였다. 실행해 보면 ar 배열의 내용이 제대로 출력되는 것을 확인할 수 있다. 이 예제에서 OutArray의 인수 목록에 int ar[5]라고 되어 있으므로 이 함수가 크기 5의 정수형 배열을 전달받는 것처럼 보이지만 사실은 그렇지 않다.

C는 함수의 인수로 배열을 전달하는 방법은 제공하지 않으며 오로지 포인터만 전달할 수 있다. OutArray의 인수 목록에 있는 int ar[5]는 정수형 포인터 ar을 의미하는 것이지 정수형 배열을 의미하는 것이 아니다. 다만 정수형 포인터 타입의 인수를 배열처럼 표기하는 것을 허용할 뿐이다. 그래서 다음 세 함수 표기는 완전히 동일하다.

 

void OutArray(int ar[])

void OutArray(int *ar)

void OutArray(int ar[5])

 

int ar[]이라고 표기하든 int *ar이라고 표기하든 컴파일러는 둘 다 정수형 포인터로 해석한다. 함수의 인수 목록에서 int ar[]이라는 표기는 int *ar과 완전히 동일하며 배열이 아니라 포인터이다. 표기만 같을 뿐이지 ar은 항상 포인터이며 함수 내에서 *연산자로 가리키는 대상을 읽을 수 있고 ++, -- 연산자로 앞 뒤로 이동할 수도 있다. 예제에서 ar[i]를 *ar++로 바꿔도 똑같이 동작하는데 증가할 수 있다는 것은 곧 상수가 아니라는 얘기다.

int ar[]의 [ ] 괄호안에 배열의 크기는 생략할 수도 있고 상수값을 적을 수도 있되 컴파일러는 여기에 표기된 상수값은 완전히 무시한다. 그래서 배열을 넘기고 싶을 때 실제로는 포인터가 넘어가더라도 그 배열의 형태를 그대로 적을 수 있다. 심지어 int ar[1000] 같은 터무니없는 값을 써도 아무런 문제가 없다. 어차피 전달되는 것은 포인터이지 배열이 아니기 때문에 컴파일러는 크기 따위에 관심을 가질리가 만무하며 함수는 시작 번지만을 전달받기 때문에 배열 크기를 알 방법이 전혀 없다.

만약 함수내에서 배열의 크기를 꼭 알아야 한다면 배열의 시작 번지와 함께 별도의 인수로 배열 크기를 전달해야 한다. 아니면 배열의 요소 중 0이나 -1같이 끝을 나타내는 미리 약속된 특이값을 쓸 수도 있는데 주로 문자형 배열이 이 방법을 사용한다. 다음 예제의 GetArSum함수는 배열의 시작 번지와 크기를 전달하면 배열 요소의 총 합을 구한다.

 

: ArrayPara2

#include <Turboc.h>

 

int GetArSum(int ar[], int size)

{

     int i,sum=0;

 

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

          sum += ar[i];

     }

     return sum;

}

 

void main(void)

{

     int ar[]={1,2,3,4,5};

     int ar2[]={6,7,8,9,10,11};

 

     printf("총합 = %d\n",GetArSum(ar,sizeof(ar)/sizeof(ar[0])));

     printf("총합 = %d\n",GetArSum(ar2,sizeof(ar2)/sizeof(ar2[0])));

}

 

이 함수가 받아들이는 인수는 정수형의 포인터 ar과 배열의 크기값 size이다. 정수형 포인터의 시작과 길이를 주면 시작 번지 이후 길이만큼의 요소에 대한 합을 구해 리턴한다. 그래서 이 함수는 임의 크기의 정수형 배열에 대한 크기값을 계산할 수 있다. int ar[]이라는 표기는 곧 정수형 포인터를 의미하므로 GetArSum 함수의 본체를 다음과 같이 작성해도 동일하다.

 

int GetArSum(int *ar, int size)

 

같은 원리로 앞항에서 작성한 GetTotalForWeek 함수의 pa 인수도 다음과 같이 두 가지 방법으로 표기할 수 있다.

 

int GetTotalForWeek(int (*pa)[7])

int GetTotalForWeek(int pa[][7])

 

첫 번째 첨자 크기는 비워 두고 두 번째 첨자에만 크기를 밝히면 이 인수가 크기 7의 배열 포인터라는 것을 알 수 있다. int pa[3][7] 등과 같이 첫 번째 첨자 자리에 크기를 밝힐 수도 있는데 써 봐야 무시된다. 포인터 인수를 배열 형태로 표기하는 예는 main 함수의 인수에서도 볼 수 있는데 argv 인수에 대해 다음 두 가지 표기가 모두 가능하다.

 

void main(int argc,char *argv[]);

void main(int argc,char **argv);

 

정리하자면 함수의 인수 목록에서 포인터형 인수에 대해서는 int *ar 형식 외에 int ar[]이라는 표기도 허용되며 두 표기법은 완전히 동일하게 해석된다. 어디까지나 함수의 인수 목록에서만 그럴 뿐이며 일반적인 선언문에서는 int ar[]이라는 표기로 포인터를 선언할 수 없다. 그렇다면 C 문법은 왜 이런 두 가지 표기법을 허용하며 두 표기법이 문법외적으로 어떤 차이점이 있을까?

 

int *ar : 이렇게 표기하는 것은 이 인수가 포인터라는 것을 강조한다. 호출원에서 &i나 pi 등을 넘길 때는 이런 표기법을 쓰는 것이 좋으며 함수를 쓰는 사람은 이 표기를 보고 이 인수가 정수형 변수의 번지를 전달받는다고 생각할 것이다.

int ar[] : 이렇게 표기하는 것은 이 인수가 배열로부터 온 포인터라는 것을 강조한다. 호출원에서 arScore나 arValue같은 배열명으로부터 평가된 포인터 상수를 넘길 때 이런 표기법을 쓰는 것이 좋으며 이렇게 표기된 인수는 배열이라는 것을 쉽게 알 수 있다.

 

두 표기법은 문법적으로는 완전히 동일하지만 함수를 읽는 사람에게 인수의 의미에 대한 약간의 정보를 제공할 수 있다. 그래서 C는 이런 두 가지 표기법을 제공하는데 이로 인해 C 초보자들은 많은 혼돈을 느끼는 것 또한 사실이다. 차라리 배열이더라도 무조건 *로만 표기하도록 문법을 정리했다면 이런 혼란은 없었을 것이다.

만약 이런 것을 신경쓰고 싶지 않다면 앞으로 배열이든 정수형 변수의 번지든 함수로 전달할 때는 * 표기법만 사용하면 된다. 단, 남의 소스를 볼 때는 [ ] 표기법으로 포인터를 넘기는 경우가 있기 때문에 두 표기법이 동일하다는 것은 꼭 알아 두어야 한다.