10-4-라.void 이중 포인터

다음은 다소 어려운 얘기일 수도 있는데 void ** 타입에 대해 연구해 보자. 그다지 실용성이 있는 내용은 아니지만 포인터를 얼마나 잘 이해했는지를 테스트해 보기에 적합한 주제라고 할 수 있다. 만약 이 내용이 잘 이해되지 않는다면 아직 포인터에 대해 완벽하게 이해하지 못한 것이되 그렇다고 해서 걱정할 필요는 없다. 다음에 실무를 좀 해 본 후에 다시 읽어 보면 그때는 분명히 이해가 될 것이다.

void **라는 타입이 void *타입을 가리키는 유도 타입이므로 void ** vpp; 변수를 선언할 때 vpp의 대상체는 임의의 대상체를 가리키는 void *타입이다. 비슷해 보이는 타입이지만 void *와는 달리 가리키는 대상체의 타입이 void *로 분명히 정해져 있고 대상체의 크기도 명확하게 알고 있다. 따라서 vpp는 void형 포인터에 적용되는 규칙 대신 일반 포인터의 규칙이 적용된다. 임의 타입의 포인터를 대입받을 수 없으며 반드시 void *형 변수의 번지만 대입받을 수 있다.

또한 대상체가 분명히 정해져 있으므로 *연산자로 대상체를 읽거나 변경할 수 있고 ++, --, +n 등의 연산으로 앞 뒤 요소로 이동할 수도 있으며 같은 void **타입끼리 대입, 비교, 뺄셈도 가능하다. 다음 예제를 통해 vpp와 vpp의 대상체에 대해 연구해 보자.

 

: voidpp

#include <Turboc.h>

 

void main()

{

     void *vp;

     void *av[5];

     void **vpp;

     int i,*pi=&i;

 

     vpp=&vp;      // 가능

     vpp=av;        // 가능

     vpp++;          // 가능

     *vpp;            // 가능

     vpp=&pi;       // 불가능

     **vpp;          // 불가능

}

 

void **로 선언된 vpp가 대입받을 수 있는 값은 void *형 변수 vp의 번지, void *배열 av의 선두 번지 등이다. int **ppi가 int *pi의 번지나 int *ar[5]의 선두 번지를 대입받을 수 있는 것과 같은 이치이다. vpp가 void *형의 av 배열을 가리키고 있을 때의 상황을 그려 보면 다음과 같다.

av 배열의 각 요소는 void *타입이므로 임의 타입 변수에 대한 번지를 가질 수 있다. 따라서 av 배열 요소가 가리키는 값을 읽으려면 반드시 캐스트 연산자가 있어야 하며 ++, -- 연산도 직접적으로 적용할 수 없다. 하지만 vpp가 가리키는 대상체는 void *로 타입이 정해져 있고 포인터는 부호없는 4바이트 길이를 가지므로 vpp 자체를 증가하여 av의 다음 요소로 이동할 수 있으며 *vpp로 가리키는 요소의 값(이 경우 번지값)을 읽을 수도 있다.

void형 이중 포인터라 하더라도 int *형 변수인 pi의 번지를 대입받을 수는 없다. int *형 변수의 번지를 가리키는 타입은 int **여야 한다. 만약 vpp가 &pi를 대입받을 수 있다면 **vpp로 정수값을 읽을 수 있어야 하는데 vpp가 가리키는 포인터의 대상체는 정해져 있지 않으므로 이런 연산이 불가능하다. void *가 임의 변수의 번지를 대입받을 수 있다고 해서 void **가 임의 포인터의 번지를 대입받을 수 있는 것은 아니다. 다소 혼란스럽겠지만 임의의 포인터들을 가리키는 변수의 타입은 여전히 void *이다. 왜 그런지 다음 그림을 보고 잘 생각해 보자.

void *는 원래 임의의 타입을 모두 가리킬 수 있는 타입이다. 이 임의의 타입에는 포인터 타입도 당연히 포함되며 포인터 변수도 분명히 변수이므로 번지가 있고 이 번지를 void *의 변수가 가질 수 있는 것이다. void *vp가 int *pi를 가리키고 있을 때 대상체를 읽고 싶다면 캐스트 연산자를 적절히 잘 사용해야 한다. **(int **)vp 이렇게 되는데 vp를 int형 이중 포인터로 잠시 바꾼 후 *를 두 번 적용하면 pi가 가리키는 정수를 읽을 수 있다.

**vpp로 vpp가 가리키는 포인터가 가리키는 대상체를 바로 읽을 수는 없다. vpp에 *를 한 번 적용하여 void *를 읽는 것은 가능하지만 이렇게 읽은 void *의 대상체의 타입은 알 수 없기 때문이다. 정 읽고자 한다면 *(int *)*vpp로 일단 void *를 읽은 후 그 결과 포인터를 원하는 타입으로 캐스팅해서 다시 *연산자를 적용해야 한다. 다음 예제는 void 이중 포인터의 현실적인 사용예이다.

 

: voidalloc

#include <Turboc.h>

 

void alloc(void **mem,size_t size)

{

     *mem=malloc(size);

}

 

void main()

{

     void *vp;

 

     alloc(&vp,sizeof(int));

     *(int *)vp=1234;

     printf("%d\n",*(int *)vp);

     free(vp);

}

 

메모리를 대신 할당하는 함수를 만들고 싶을 때 이 함수가 받아야 할 타입이 바로 void **이다. 예제의 alloc 함수는 malloc을 단순히 호출하기만 하는데 어떤 복잡한 연산을 통해 필요한 메모리양을 계산한다든가 할당 후 추가 동작을 할 수도 있다. main에서 동적 할당된 메모리의 번지 저장을 위해 void *형의 vp를 선언하고 이 변수를 할당 함수에게 참조 호출로 넘기기 위해서는 &vp를 넘겨야 한다. 그러므로 alloc이 받는 타입은 void **일 수밖에 없다.