10-2.void형 포인터

10-2-가.void형

포인터형 변수는 선언할 때 반드시 대상체의 타입을 밝혀야 한다. 가리키는 대상체의 타입을 알아야 *연산자로 대상체를 읽을 수 있고 증감 연산자로 전후 이동이 가능하다. 이런 일반적인 포인터에 비해 선언할 때 대상체의 타입을 명시하지 않는 특별한 포인터형이 있는데 이것이 바로 void형 포인터이다. void형 포인터를 선언할 때는 void *타입을 지정한다.

 

 void *vp;

 

이렇게 선언하면 vp 포인터 변수의 대상체는 void형이 되며 이는 곧 대상체가 정해져 있지 않다는 뜻이다. void형은 함수와 포인터 변수에게만 적용되는 타입이므로 일반 변수에는 쓸 수 없다. void i;라는 선언문은 불법이다. 다음은 void형 포인터의 특징들이되 모두 대상체가 정해져있지 않다는 사실에 기인한다.

 

 임의의 대상체를 가리킬 수 있다.

대상체가 정해져 있지 않다는 말은 어떠한 대상체도 가리키지 못한다는 뜻이 아니라 임의의 대상체를 가리킬 수 있다는 얘기와도 같다. 선언할 때 대상체의 타입을 명시하는 일반적인 포인터는 지정한 타입의 대상체만 가리킬 수 있지만 void형 포인터는 어떠한 대상체라도 가리킬 수 있다. 그래서 pi가 정수형 포인터 변수이고, pd가 실수형 포인터 변수이고 vp가 void형 변수일 때 다음 대입문들은 모두 적법하다.

 

vp=pi;

vp=pd;

 

정수형 포인터 pi가 가리키는 번지를 실수형 포인터 pd에 대입하고 싶다면 pd=pi; 대입식을 곧바로 쓸 수 없으며 반드시 pd=(double *)pi;로 캐스팅해야 한다. 대입문의 좌변과 우변의 타입이 같아야만 정상적인 대입이 가능하다. 그러나 void형 포인터는 임의의 대상체를 모두 가리킬 수 있기 때문에 대입받을 때 어떠한 캐스팅도 할 필요가 없다. 좌변이 void형 포인터일 때는 우변에 임의의 포인터형이 모두 올 수 있다. vp는 정수형 변수도 가리킬 수 있고 실수형 변수도 가리킬 수 있는 것이다.

void형 포인터를 좀 더 쉽게 표현하자면 임의의 대상체에 대한 포인터형이다. 대상체가 정수든, 실수든 가리지 않고 메모리 위치를 기억할 수 있다. void형 포인터는 임의의 포인터를 대입받을 수 있지만 반대로 임의의 포인터에 void형 포인터를 대입할 때는 반드시 캐스팅을 해야 한다.

 

pi=(int *)vp;

pd=(double *)vp;

 

만약 이 대입문에서 캐스트 연산자를 생략해 버리면 void *형을 int *형으로 변환할 수 없다는 에러 메시지가 출력된다. 만약 pi=vp; 대입식을 허용한다면 *pi로 이 번지의 정수값을 읽을 때 이 값이 온전한 정수형임을 보장할 수 없을 것이다. 개발자는 vp가 가리키는 곳에 정수가 있다는 것을 확신할 수 있을 때만 캐스트 연산자를 사용해야 한다. 참고로 C++보다 타입 체크가 덜 엄격한 C는 pi=vp 대입을 허용한다.

 *연산자를 쓸 수 없다.

void형 포인터는 임의의 대상체에 대해 번지값만을 저장하며 이 위치에 어떤 값이 들어 있는지는 알지 못한다. 따라서 *연산자로 이 포인터가 가리키는 메모리의 값을 읽을 수 없다. 대상체의 타입이 정해져 있지 않으므로 포인터가 가리키는 위치에서 몇 바이트를 읽어야 할지, 또 읽어낸 비트를 어떤 식으로 해석해야 할지를 모르기 때문이다. 다음 예제를 실행해 보자.

 

: voidPointer

#include <Turboc.h>

 

void main()

{

     int i=1234;

     void *vp;

 

     vp=&i;

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

}

 

void형 포인터 vp는 정수형 변수 i가 저장된 번지를 대입받았다. 좌변이 void형 포인터이므로 vp=(void *)&i;와 같이 캐스트 연산자를 쓰지 않아도 곧바로 대입할 수 있다. 이 대입문에 의해 vp는 정수형 변수 i가 기억된 번지값을 가지게 될 것이다. 그러나 vp는 대상체가 정수형 변수라는 것을 모르기 때문에 *vp 로 이 번지에 들어있는 값을 읽을 수는 없다. 만약 vp 번지에 저장된 값이 정수형이라는 것을 확실히 알고 있고 이 값을 꼭 읽고 싶다면 다음과 같이 캐스트 연산자를 사용해야 한다.

 

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

 

vp를 잠시 정수형 포인터로 캐스팅하면 *연산자를 사용할 수 있다. 캐스팅된 vp는 정수형 포인터이므로 *연산자는 vp가 가리키는 번지에서 4바이트의 정수를 읽을 수 있다.

포인터 연산자와 캐스트 연산자의 우선 순위는 같으며 결합 순서는 우측 우선이므로 캐스트 연산자가 먼저 수행되어 vp를 정수형 포인터로 바꾸어 놓고 *연산자가 이 위치에서 정수값을 읽는다. 따라서 *((int *)vp)처럼 굳이 괄호를 하나 더 쓸 필요는 없다. 물론 괄호를 싸 놓으면 캐스팅이 먼저 된다는 것을 확실하게 알 수 있어서 안정되 보이기는 한다.

 증감 연산자를 쓸 수 없다.

대상체의 타입이 정해져 있지 않으므로 증감 연산자도 곧바로 사용할 수 없다. 정수값과 바로 가감 연산을 하는 것도 허용되지 않는다. 대상체의 크기를 모르기 때문에 얼마만큼 이동해야 할지를 모르는 것이다. 다음 예제를 실행해 보자.

 

: voidPointer2

#include <Turboc.h>

 

void main()

{

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

     void *vp;

 

     vp=ar;

     vp=vp+1;

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

}

 

vp=vp+1 연산문에서 에러가 발생하는데 +1이 몇 바이트 뒤인지를 결정하지 못하기 때문이다. 대상체의 크기를 모르므로 다음 요소로 이동할 수 없다. void *를 다음 요소로 이동하는 여러 가지 코드의 가능성 여부를 점검해 보자.

 

vp=vp+1 : 위 예제에서 보다시피 명백한 에러다. 대상체 크기를 모르므로 증가할 양을 결정하지 못한다. 동일한 코드의 축약형인 vp+=1도 마찬가지로 에러로 처리된다.

vp++ : vp=vp+1과 같은 식이므로 역시 안된다. 따라서 에러로 처리된다.

vp=(int *)vp + 1 : 가능하다. vp를 잠시 int *로 캐스팅한 후 1을 더하면 4바이트 뒤쪽을 가리키는 int * 타입의 포인터 상수가 된다. void *는 임의의 포인터를 대입받을 수 있으므로 별도의 캐스팅없이 정수형 포인터를 대입받을 수 있다.

(int *)vp++ : vp를 캐스팅한 후 증가하면 될 것 같지만 결합 순서에 의해 ++이 캐스트 연산자보다 먼저 연산되므로 vp++과 같은 이유로 에러이다.

((int *)vp)++ : 캐스트 연산자를 괄호로 싸 먼저 연산되도록 했다. ++연산자의 피연산자는 좌변값이어야 하는데 캐스팅을 하면 좌변값이 아니므로 증가시킬 수 없다. 그러나 Dev-C++에서는 적법한 문장으로 컴파일된다. ++(int *)vp도 가능하다.

 

보다시피 가능한 방법이 있고 그렇지 못한 방법이 있는데 모든 경우에 가능한 코드는 vp=(int *)vp+1 밖에 없다. 이렇게 수정한 후 컴파일해 보면 ar[1]인 2가 출력된다. 나머지는 당장은 가능하다 하더라도 이식에 불리하므로 쓰지 않는 것이 좋다.

void형 포인터의 특징에 대해 간단하게 요약해 보자. 대상체가 정해져 있지 않으므로 임의의 번지를 저장할 수 있지만 *연산자로 값을 읽거나 증감 연산자로 이동할 때는 반드시 캐스트 연산자가 필요하다. 값을 읽거나 전후 위치로 이동하는 기능은 빼고 순수하게 메모리의 한 지점을 가리키는 기능만 가지는 포인터라고 할 수 있다.