본문 바로가기

무언가 만들기 위한 지식/C,C++,Embedded C

[All] About "void" Type

void란 형용사적의미로 비어있는, 공허한, 공석의
명사로 공백, 공간을 의미한다.

C언어 및 CPP상에서도 비슷한 의미로 사용된다.
하지만 헷갈리는 경우가 있다.
어떤 이는 [void란 어느 것도 가리킬 수 있는 것 - 어느것을 가리키는지 모르는 상태] 이라고 정의한 반면,
어떤 이는 [void란 아무 것도 가리키지 않는 것] 이라고 말한다.
(예전 네이버 지식IN 에서 이 답변으로 다투는 글을 본적이 있다.)

위 논쟁을 단번에 정리해 보자.
void는 포인터나 배열형, 구조체, 기본형과 같이 C Language 상의 Type 중의 하나이다.
직관적으로 의미하는 것은 형 없음이다. 즉 가리키는 형 이 없다는 것이다.

먼저 void가 사용되는 경우에 대해 생각해보자.

우리가 자주보는 void는 일단 함수에서 가장 많이 볼 것이다.
이 때는 함수의 매개변수(Parameter)를 void로 선언하거나 리턴 Type을 void로 선언하게 된다.

[작성환경 : Window XP   작성툴 : Visual Studio 6.0   컴파일러 : Visual Studio 6.0    사용언어 : CPP]

함수를 사용하는 아주 간단한 예로 void를 사용한 모습이다.
void가 매개변수에 정의 및 선언을 하게 되면, 그 함수를 사용할 때는 인자를 넣어주지 않아야 한다.
(사용시 인자를 넣어주면 Compile Error가 발생, 정의 및 선언에서 void는 생략이 가능해도 문제 없으나 명확성가독성을 위해서 직접 써주는 것이 좋다.)

※ 참고로 함수의 Return에서 아무것도 선언을 하지 않을때는 보통 Int Type으로 취급하나 컴파일러마다 다르다.

함수의 Return값을 void 형으로 선언하면 함수 내부에서 Return을 정의하지 않아도 된다.
(이는 반대로 형이 있으면 반드시 Return문이 존재하여야 하는 것을 의미)

이때 함수에서 이용되는 void의 의미는 "형이 없다"라는 의미이다.
(혹시나 해서 하는 말인데 void는 일반 변수에서 선언이 불가능하다. 하게되면 Compile Error 발생)


그리고 다음으로 void가 사용되는 곳은 바로 void * 이다.
보이드 포인터라고 읽히며 사용된다. 이때는 일반 변수에도 쓰이고 리턴값에도 쓰이고 비교적 자유롭게 사용된다.
(혹시 변수에서도 void를 본적있다고 생각하는 사람은 바로 이 void *를 보았기 때문일 것이다.)

처음에서 필자가 말했던 논쟁중 하나인 [어느것도 가리킬 수 있는 것 - 어느것을 가리키는지 모르는 상태]는 바로 여기서 사용되는 의미이다. void 포인터 는 바로 어느것도 가리킬 수 있는 것을 의미한다.

void *를 잘못 해석하면 void가 형이 없다는 의미라서 아무것도 가리키지 않는 포인터라고 착각할 수 있는데
이것은 큰 오산이다.
아무것도 가리키지 않는 포인터는 널포인터(NULL)을 이용하여 아무것도 가리키지 않는 것을 나타내고,
void *[어떤 것도 가리킬 수 있는 - 아직 어느 것을 가리키는지 모르는 상태] 이다.

[void형 포인터 변수의 성질]
1. 어떠한 형 변환 없이도 void 형 포인터 변수에 대입이 가능하다.
2. void 형 포인터 변수에서 값을 읽을 때는 반드시 캐스트 연산자(형변환)를 사용해야 한다.
3. * 를 사용할 때는 항상 캐스트 연산자를 사용한다.
4. void형 포인터 변수에 ++, --를 사용할때에는 항상 캐스트 연산자를 사용한다.

C 언어 포인터
카테고리 컴퓨터/IT
지은이 정재은 (정보문화사, 2003년)
상세보기
에서 발췌

void *를 이용한 간단한 테스트를 해보자.

[작성환경 : Window XP   작성툴 : Visual Studio 6.0   컴파일러 : Visual Studio 6.0    사용언어 : CPP]

3~10 Line 에서는 두가지 함수내의 Static으로 선언된 변수를 void * Type으로 값을 Caller에서 가져와서 사용하는 모습을 테스트 하기 위함이다. (Static이기 때문에 함수내에 선언되었더라도 계속 DATA 영역에 생성되어 남아 있는다.
12, 13 Line은 변수를 생성하고 14 Line에서 void *를 선언한후,
15, 16 Line에서 바로 대입한 것이다. 기본 타입이 달라도 temp1과 temp2가 void *이기 때문에 자연스럽게 대입이 가능하다.
위에서 언급한 포인터 변수의 성질 1 에 해당하는 특징이다.
17~21 Line에서 void *인 temp1, temp2를 이용하여 출력을 수행해보았다.
(이때 temp1과 temp2는 각각 int와 double형 값과 이어져 있다.)
18 Line의 경우 컴파일은 성공하나 단순히 void *변수에 저장된 주소값이 출력되는 것을 볼 수 있다.
19 Line에서는 * 를 이용하여 가리키는 값을 출력해 보려고 했으나 컴파일 에러가 발생된다.
이 이유가 중요하다.
void *로 선언된 변수에서 *을 이용하여 포인터가 가리키는 값을 가져오기 위해서는 강제 형변환을 해 주어야 한다.
그 까닭은 * 연산자가 해당 주소로부터 데이터를 읽어오기 위해서는 몇 바이트를 읽어올지 알고 있어야 하기 때문이다.
그래서 제대로 된 값을 출력하기 위해서는 17 Line에서 *(int *)temp1로 해야 한다. 이 선언은 int *로 강제 캐스팅을 해주어서 몇 바이트를 읽어올지 미리 알려주는 것이다. 그후 *을 이용하여 해당 값을 읽어올 수 있다.
이는 포인터 변수의 성질 2, 3 에 해당하는 내용이다.
21 Line에서처럼 원래는 Double로 강제 형변환을 해야하는데 int로 하게 될 경우 컴파일 에러나 런타임 에러는 아니나 잘못된 크기의 값을 읽어왔기 때문에 잘못된 값을 얻게 된다.

22 Line의 경우,
리턴타입이 void *인 함수를 테스트 해본 것이다.
함수 내부적으로는 static으로 선언된 변수가 존재하고 그 변수의 주소를 리턴해보았다.
func 함수의 경우 문자열 배열의 첫번째 주소를 리턴한 것이다.
(&str을 str로 바꿔 사용해도 어차피 void *로 넘겨주는 것이기 때문에 상관 없다.)
함수의 Return값을 char *로 강제 형변환을 해주어서 cout을 했기 때문에 출력이 무사히 이루어진다.
(배열이기 때문에 첫번째 주소를 넘겨준 것이다.)

23 Line은 인자로 받는 void *가 가리키는 값을(int) 출력할 것이기 때문에 *(int *)를 사용하였다.
이 의미는 강제 형변환후에 포인터가 가리키는 값을 가져다 사용한다는 의미이다.
강제형변환을 먼저 해야지 * 연산자가 읽어올 크기를 인지하고, 그 크기만큼가져오기 때문이다.