11-3-바.&ar

int ar[5]라는 선언문은 크기 5의 정수형 배열을 선언하는 문장이다. 이렇게 선언된 ar은 배열의 시작 번지를 가리키는 포인터 상수라는 것은 앞에서 이미 다 알아보았다. ar 자체가 포인터이므로 이 번지를 얻고 싶을 때는 별도의 연산자없이 ar이라고만 표현하면 된다. 예를 들어 배열의 선두 번지를 정수형 포인터에 대입하고 싶다면 int *pi=ar; 로 대입한다.

그렇다면 ar에 &연산자를 붙인 &ar은 과연 어떤 의미가 있을까. ar이 포인터 상수인데 상수는 번지를 가지지 않으므로 &연산자를 쓸 수 없다. 실제로 클래식 C의 문법에서는 &ar이라는 표현식을 허용하지 않는다. 그러나 ANSI C 이후부터는 배열이 &의 피연산자가 될 때 포인터 상수가 아니라 배열 그 자체를 가리키는 것으로 변경되었으므로 &ar이라는 표현식이 가능해졌다. 그렇다면 ar과 &ar이 어떻게 다른지 연구해 보자. 일단 다음 코드로 두 형식의 포인터가 실제 어떤 번지를 가지는지 출력해 보자.

 

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

printf("%p\n",ar);

printf("%p\n",&ar);

 

배열 ar이 어디에 생성될 것인가는 실행시마다 다르므로 관심 대상은 아니다. 이 코드는 ar과 &ar의 번지가 같은가 아닌가를 확인해 보는 것인데 실행해 보면 똑같은 번지를 가리키는 것으로 출력된다. 그렇다면 ar과 &ar이 같은 자격을 가진다는 얘기인지 아니면 &ar은 뭔가 다른 의미를 가지는지 다음 코드로 테스트해 보자.

 

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

int *pi;

 

pi=ar;                // 가능

pi=&ar;                   // 에러

 

정수형 포인터 pi는 정수형 포인터 상수인 ar을 대입받을 수 있지만 &ar을 대입받을 수는 없다. 대입이 안된다는 얘기는 좌우변의 타입이 다르다는 뜻이다. ar은 정수형 배열의 시작 번지를 가리키는 포인터 상수이므로 정확한 타입은 int * const이며 대상체는 int이다. 그러나 &ar의 타입은 이와는 다른데 대상체가 크기 5의 정수형 배열이며 타입은 int (*)[5] const가 된다. 즉 &ar은 크기 5의 정수형 배열을 가리키는 배열 포인터 상수이다.

양변의 타입이 다르므로 대입을 거부하는 것이 당연하다. 만약 꼭 대입하려면 pi=(int *)&ar; 로 캐스팅해야 하는데 억지로 대입할 수는 있지만 타입이 다르므로 틀린 대입이다. pi의 타입을 int (*pi)[5]로 수정해야 대입받을 수 있는데 수정한 후 다음 예제로 &ar이 어떤 의미를 가지는지 테스트해 보자.

 

: ampersanearray

#include <Turboc.h>

 

void main(void)

{

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

     int *p1;

     int (*p2)[5];

 

     p1=ar;

     p2=&ar;

     printf("before = %p\n",p1);

     printf("before = %p\n",p2);

     p1++;

     p2++;

     printf("after = %p\n",p1);

     printf("after = %p\n",p2);

}

 

정수형 배열 ar의 번지를 가지는 p1과 정수형 배열의 포인터인 &ar의 번지를 가지는 p2를 선언한 후 이 두 변수의 초기 위치와 1증가시킨 후의 번지를 출력해 보았다. 출력되는 번지는 물론 시스템에 따라 달라질 것이다.

 

before = 0012FF6C

before = 0012FF6C

after = 0012FF70

after = 0012FF80

 

p1, p2는 최초 같은 번지를 가리키고 있다. 그러나 1증가했을 때 p1은 4바이트 뒤로 이동하지만 p2는 20바이트 뒤로 이동한다. 이로부터 두 포인터의 타입과 대상체가 다르다는 것을 확실히 확인할 수 있는데 p1은 정수형 포인터이며 p2는 크기 5의 정수형 배열에 대한 포인터이다. &ar의 의미는 ar을 부분 배열로 가지는 가상의 전체 배열에 대한 이차 배열 포인터 상수라고 할 수 있다. 마치 int *pi=&i; 에 의해 pi가 i를 첫 번째 요소로 가지는 가상의 일차 배열 포인터가 되는 것처럼 말이다.

&ar의 정체를 알았으면 이제 다음 코드에 대해 점검해 보자. 포인터를 처음 배우는 사람들이 흔히 이 문장이 가능한 이유에 대해 많이 질문하는데 보기보다 이유가 복잡하다. scanf로 문자열을 입력받는데 이 함수는 참조 호출을 하므로 변수앞에 &를 붙여야 하지만 배열의 경우는 그 자체가 포인터이므로 &를 붙이지 않아도 된다. 그래서 문자열을 입력받을 때는 통상 다음과 같이 한다.

 

char name[20];

scanf("%s",name);

 

그런데 이 문장을 scanf("%s",&name);으로 써도 잘 동작한다. 컴파일도 잘되고 동작도 아주 정상적이다. name과 &name의 타입이 다르지만 가리키는 주소는 우연히 같다. 가변 인수 함수는 인수의 타입을 점검하지 않으며 서식과 일치하는 타입으로 간주한다. name, &name은 둘 다 포인터형이므로 4바이트이고 %s 서식과 대응될 수 있으며 타입은 달라도 우연히 같은 번지를 가리키고 있으므로 동작에도 이상이 없는 것이다. 하지만 결코 정상적인 문법은 아니므로 name앞에는 &를 붙이지 말아야 한다. 차라리 &name[0]라고 쓰는 것이 정상적이다.