Programing/C- programing

C programming :: Void형 포인터와 NULL 포인터

sosal 2010. 1. 27. 09:03
반응형

/*
 http://sosal.tistory.com/
 * made by so_Sal
 */


Void*
NULL*



Void *

포인터는 모두 주소를 저장하는 변수로 모두 4바이트의 같은 크기를 갖고있다.
(물론 32비트 프로그래밍에서 이야기한다.)

주소값이 가지는 값들은 결국 컴퓨터에겐 단순한 숫자일 뿐이지만,
그 숫자가 의미하는것에 따라 의미를 부여하여 int, char, double 등 자료형을 만들었다.
위와 같이 일반적인 포인터는 자신이 가리키는 주소가 가지는 자료형과 같은 자료형을 쓴다.

하지만 포인터 자료형중에 void라는 특별한 놈이 있다.
이놈은 일반적인 포인터와 달리 데이터 자료형이 명시되지 않은 포인터이다.

포인터는 단지 주소에 접근하기 위함 뿐만 아니라 * 연산자를 이용해 값을 변경하고
심지어 --, ++ 등 포인터를 사용하는데는 메모리 접근만이 아님을 C언어를 공부했던 사람은 다 알것이다.

하지만 void 포인터가 왠말이냐?
void 라는 자료형 자체가 데이터 타입이 없다는 것이다. 아니 정해져 있지 않다는 말이 옳다.
데이터 타입이 없는것과 정해져있지 않다는것은 명백하게 다르다.



위 그림에에서 보듯 void형이라는 변수는 존재하지 않는다.
void는 함수와 포인터로만 사용된다.

void *a; 를 컴파일 해보면
오류 없이 잘 컴파일 되는것을 볼 수 있을 것이다.


void *는 자료형이 정해져 있지 않기 때문에 어떠한 자료형이든지 포인터 할 수 있다.

만약 char*로 int데이터를 포인터 하게 한다면?
 :: error C2440: '=' : 'int *'에서 'char *'(으)로 변환할 수 없습니다.

에러가 작살 날것이다.
하지만 void *는 그렇지 않다.
void형 포인터는 어떤 자료형이든 모두 지정할 수 있다.

예를 들자면,
int a = 10;
void* ptr;
ptr = &a;
cout<< *(int*)p <<endl;

이것이 가능하단 이야이다.

void형 포인터는 이렇게 어떠한 자료형이든 접근이 용이하다.
하지만, 편리한 만큼 감수해야 할 부분이 있다. 형변환 부분이다.
위에 출력하는 부분에서 *(int*)p 이런 부분이 있는데

* (int*) p 는 순서는 이렇다.
p라는 변수를 int * 형으로 형변환 시키고, (괄호가 먼저임을 잊지 않도록)
p라는 변수가 가리키고 있는 값 ( * )을 가져와라 라는 의미이다.


혹시나 * 연산자에 대해서 잘 모를까봐 노파심에 아래 내용을 적는다.
int a = 10;
int *ptr = &a;
cout<< *ptr <<endl; //* 연산자는 포인터 변수가 가리키는 메모리가 가진 ! :: 10
cout<< ptr << endl; // 포인터는 가리키고 있는 변수의 메모리 주소값을 뜻한다. :: 0x0032F7D4
(물론 변수의 메모리 주소값은 일정하지 않다.)


따라서 void형 포인터로 만약 값을 전달 한다고 하면

 int a = 10;
 void *p;
 p = &a;
 int b = *(int*)p;
 cout<<b;

위와 같은 형태가 되겠다.

한번더 포인터의 특징을 말하자면, 모든 포인터들 중에 오로지 void형 포인터만이
캐스트 연산자 (형변환) 의 도움 없이 모든 변수에 접근 (대입 연산)이 가능하다는 이야기이다.
그리고 void*를 사용하기 위해서는 꼭 원하는 데이터의 자료형으로 형변환 해줘야 한다는것.

cout << * (int *) ptr << endl;
printf( " %d \n", * (int *) ptr );
가 이에 속하겠다.

* (int *) ptr은 위에서 언급 했으니 더 설명하지 않겠다.



일반적인 자료형을 가지고 있는 포인터가 증감연산자 (++, --) 를 사용하였을 경우에
포인터가 가리키는 대상체의 자료형을 참고하여 1바이트 또는 1바이트 등 값이 증가할 것이다.
하지만 void형은 자료형이 없으므로 증감연산자를 사용할 수 없다.
따라서 적절한 캐스트 연산자나 형변환이 필요하다

 int a = 10;
 void *ptr = &a;
(*(int *)ptr)++;
 cout<<a;

이와 같이 void형 포인터를 이용하여 증감연산자를 할경우 필수적으로 형변환이 필요하다
여기서 주의할점은 *(int *)ptr++; 은 오류메시지를 띄울것이다.
++연산자가 우선순위가 매우 높기때문에 int형 *로 형변환 하기전에
증감 연산자를 먼저 시도하게 된다. void형 포인터는 증감연산자를 사용할 수 없기 때문에
컴파일 시 오류가 날것이다.


void형 포인터의 대표적인 쓰임새는 함수 memset으로 설명하려 한다.

정의
void* memset(void *_Dst,int _val, size_t _Size)

첫번째 매개변수를 보면 void형 포인터로 이루어져 있음을 알 수 있다.
memset은 배열을 전부 초기화 하는 함수로, 어떤 배열이든 가리지 않는다.
이 함수는 단순히 포인터(또는 배열의 이름)가 가리키는 메모리의 값을
초기화 해주는 함수로, 메모리 값의 자료형이 어떤지에는 관심이 없다.

void *를 이용하여 int형, char형, double형 배열 등 모든 데이터를
초기화 할 수 있다. 이것이 아주 큰 장점이다.

만약 memset의 첫번째 매개변수가 int * 라면,
오로지 int형으로 이루어져 있는 배열만이 초기화가 가능 할 것이고,
자료형 마다 memset 함수가 따로 있어야만 할 것이다.

결론 :: 어떠한 자료형이든 쉽게 접근 가능하다. 라는 말을 하고싶다.

void형 포인터에 대해 글이 상당히 길어졌는데,
NULL 포인터에 대해서 알아보자.



NULL *

널 포인터는 0값을 가지고 있는 (0번지를 가지고 있는) 포인터이다.
즉 아무것도 가리키고 있지 않는 포인터다.
일반적으로 포인터가 아무런 값도 가리키지 않는 것은 불가능하다.
선언된 변수라면 초기화가 되어있지 않아도, 적어도 쓰레기 값은 가지고 있기 때문이다.

그래서 개발자들은 포인터가 가질 수 있는 값 중 0이란 값을 아무 값도 아닌 값으로 약속하였다.

C에서는 malloc 함수, C++에서는 new 라는 연산자를 통하여 원하는 만큼의 메모리를
동적으로 할당하곤 했다. 이들의 리턴하는 값은 메모리의 첫 주소를 리턴한다.
하지만 메모리 할당에 실패했을 경우에 0을 리턴한다.
만약에 0을 리턴받았을때, NULL 포인터 (즉 0) 를 약속하지 않았다면
컴퓨터는 이렇게 생각할 것이다.
" 아 ! 내가 동적할당한 배열의 첫주소가 0이구나! "

포인터가 가리키는 주소가 0을 가리킬 확률은 아주 희박하다.
지역변수로 선언된 데이터들은 stack, 동적 할당으로 선언된 데이터는 heap.
stack은 거의 끝부분이기 때문에 0은 커녕 높은 주소에 변수가 할당될 것이고,
heap영역 역시 앞부분에 정적 데이터 영역이 있기 때문에 0번 주소를 받을 일은 없다.

#define NULL 0

단순히 숫자 0으로만 표현하기에는 의미가 너무 중요한 부분이기 때문에
NULL 이라는 문자로 대신하여 사용하고 있다.

이 글을 아주 간단하게 정리해보면,

void *  :: 어떤 데이터든 자유롭게 접근할 수 있는 무적의 포인터
NULL* :: 아무 것도 가리키고 있지 않고, 실패의 의미로 쓰이는 포인터 상수