오늘은 한번에 두가지 주제에 대해서 다루겠습니다. void와 malloc에 대해서 글을 써내려 가려하는데 좀 버거울 수도 있겠습니다. 저도 두 가지를 연관 지으려면 어떻게 해야할지 고심되고, 또 제가 포스팅을 하는 방식은 가능하면 다른 측면으로 생각해 보려고 노력하기 때문에 엉뚱해 질 수 도 있겠군요.
좀 길어질지 모르겠지만 우선 void에 대해서 알아보죠.
void형(type)은 타입으로 보기엔 부족한 부분이 있습니다.
그 부족한 점 때문에 void라는 타입은 포인터변수에만 사용할 수 있네요.
일반 변수에 사용하면, 컴파일러는 에러를 뿜어낼 겁니다..(variable or field '변수명' declared void)
또, 함수의 반환형으로 선언할 수 있는데, 이경우 주의해야할 점이 있습니다.
void형으로 반환되는지 아니면 void * 형으로 반환되는지에 따라 의미가 차이가 생기는데, 이부분이 헷갈립니다. 왜 이렇게 일반 타입들과 구별이 되는지 void형에 대해서 알아보고 위 문제에 대해서 생각해 보죠.
이부분은 특히 포인터에 대한 확실한 이해가 전제되야하기 때문에 포인터에대해서 제가 쓴 글들을 꼭 읽어보시길 권합니다.
2013/11/28 - [프로그래밍/C언어] - C언어 포인터에 대한 이해(1)
2014/01/12 - [프로그래밍/C언어] - C언어 포인터에 대한 이해(2)
2014/01/13 - [프로그래밍/C언어] - C언어 포인터에 대한 이해(3)
일반변수에 사용할 수 없는 이유는?
void형은 타입이 없기 때문입니다.
타입이 필요한 이유는 할당된 주소값을 기준으로 메모리상에 어떤식으로 얼마만큼의 메모리를 해석해야할지를 말해주는데, void는 그런 형식(type)이 없기 때문에, 일반변수에 사용할 수 없는 것입니다.
그런데 주소값은 가질 수 있습니다. 특별히 주소값은 타입에 관계없이 4byte로 일정하다는 건 포인터를 공부 하면서 익히 아셨을 겁니다. 형식이 일정하기 때문에, void형은 포인터로 선언 될 수 있는 것입니다.
그럼 void포인터 변수는 어떤의미가 있을까?
타입없이 가리키기 때문에 어떤 값이든 대입받을 수 있습니다. 다시말해 임의의 대상체의 시작점을 가리킬 수 있다는 겁니다. 보통 모든 타입에 대해서 일괄적으로 쓰일 수 있는 함수를 만들 때 void*인수를 선언하게 됩니다. 또한 이경우 반환형도 보통 void*형이 되겠죠.
그런데 void*의 제한이 있습니다. 이건 타입이 없다는 void형의 특성에서 기인 하는 것이기 때문에, 특성을 생각하면서 이해하면 됩니다.
void형은 타입이 없기 때문에 void* 형 변수에 대해 포인터 연산을 할 수 없습니다.
다시 말해 대상체의 타입의 크기(포인터연산+1에 대응되는 크기)를 알 수 없기 때문에 연산이 안된다는 겁니다..
처음에 void형을 타입으로 보기에 부족하다고 했는데 그 부족함이 방금 말한 것입니다.
그럼 부족함을 보완하기 위해선 어떻게 해야하냐면, 직접 서식지정자로 값을 읽던가, 아니면 캐스팅을 해야 합니다.
!캐스팅을 하더라도 주의점이 있습니다.
이건 중요하니 이전에 포스팅한 lvalue과 관련지어서 다음에 따로 포스팅을 하겠습니다.
void*형을 사용하는 예
아래 코드는 허접하고 아무런 의미는 없지만, 에러없이 잘 작동합니다.
단순히 void*형의 사용 예를 위한 것이지 효용성은 없습니다.
인수의 대입 과정을 보면 void*형 인수가 int*형과 str*형 두가지 타입을 모두 받고 있습니다.
한번 중학교 수학에 나온 필요조건,충분조건으로 표현해 보죠.
잉어는 물고기입니다.(o)
물고기는 잉어입니다.(x)
int형 포인터 타입은 void*에 대입될 수 있습니다. (o)
void*형은 int형 포인터 타입에 대입될 수 있습니다. (x)
이런식인데 int형 뿐아니라 모든 임의의 타임에 대해서 성립합니다.
아무튼 중요한건 대입관계가 저렇다는 겁니다.
그럼 대략 여기까지 하고 malloc함수로 넘어 가겠습니다.
mallco 함수는 프로그램 실행 시간중에 동적으로 메모리를 할당하는 함수입니다.
malloc함수의 프로토 타입은 다음과 같습니다.
void * malloc(size_t size)
size_t는 unsigned int로 부호없는 정수형입니다.
원하는 크기만큼의 메모리 사이즈를 인수로 넣으면 새로운 메모리공간(힙영역)에 인수만큼의 공간을 할당해주고 그 시작 주소값을 반환해 줍니다. 리턴 타입은 void*형이기 때문에 시작 주소만 있고 타입은 없는 형태입니다.
이젠 감이 잡히시는 분도 있겠죠. malloc 함수를 사용하려면 void에 대해서 잘 이해하고 계셔야합니다. 이렇게 할당된 메모리에 대한 주소값은 알고 있어도 메모리를 제대로 사용하려면 타입을 정해줘야합니다.
그 방법으로 사용한 것이 캐스팅입니다.
(malloc함수는 내부적으로 메모리 할당은 해 놓은뒤 주소값을 반환합니다. 메모리를 어떻게 사용할지는 프로그래머가 미리 알고 그에 맞게 캐스팅을 해줘야합니다.)
캐스팅은 일시적으로 타입을 변환시켜 주기 때문에 다른 타입끼리 대입연산을 할수 있게 해줍니다.
물론 타입을 바꾸는건 강제적이고 문제가 될 소지가 있지만, void*은 타입에 대한 정보가 없기때문에 필요합니다. 위 코드는 단순히 일련의 숫자를 대입한 후 출력하는 아주 간단한 코드입니다.
실질적인 예로 코드를 하나 더 만들어 보겠습니다.
구조체를 선언하여 3개까지만 저장되는 간단한 전화번호부를 만들었습니다. 23번째 줄에 malloc 함수를 사용하여 구조체 전화번호부 내부 Name 포인터가 가르키는 공간에 입력 받은 문자열의 크기만큼의 공간을 할당했습니다. 내부적인 구조는 이렇습니다.
3번의 루프를 돌렸을때 malloc으로 할당을 해줄때마다, 입력받은 문자열 길이만큼 공간이 할당됩니다.
실행시간에 입력받는 데이타의 크기를 예측 할 수 없다면, 이렇게 메모리를 실행시간에 할당 받을 필요가 있습니다. 아니면 애초에 Name을 배열로 지정하여 길이를 아주 충분히 길게 해주면 됩니다. 하지만 실행시간에 얼마만큼의 길이가 입력될지 모르는 일입니다. 그래서 이런 동적 할당이 필요한 겁니다.
c언어에서는 malloc 함수를 사용하지만 c++에서는 new 키워드를 사용합니다.
그만큼 동적할당이 중요하다는 의미겠죠.
ar은 선언시에 위쪽에 쓰레기값을 가지고 있다. malloc으로 메모리를 할당한뒤 heap영역에 할당된 메모리의 시작주소를 가리킨다.
다음 주제로 넘어가 보죠..malloc 함수에서 반환값은 void*형입니다.
그러면 void형으로 선언된 함수는 어떤 의미일까?
이경우는 단순히 반환값이 없다는 뜻입니다. 문법적으로는 맞지 않은 표현이지만, 컴파일러와의 약속이기때문에 그냥 알아두면 됩니다. void형 함수를 선언하고 반환값을 리턴하면 대부분의 컴파일러는 에러를 낼 것이고, 이부분은 void의 특성과 맞물려 생각하지 말고, 문법적인 표현 그대로 받아들이는 것이 좋습니다.
void*형을 함수의 인수로 대입받는 경우도 생각해보면, 인수의 대입은 다음과 같은 과정이 이루어 집니다.
b가 어떤 타입이든 상관없다.
어떤 타입의 포인터변수든 인수로 받을 수 있다는 뜻입니다.
글이 좀 길었네요. 요점을 정리하면 다음과 같습니다.
void*(void포인터)형은 임의의 타입을 대입받을 수 있다.
void*형은 포인터 연산을 할 수 없다.
void*형이 제대로 쓰일려면 캐스팅을 해야한다.
void형 함수는 리턴값이 없다는 의미다.
동적할당이나, void에 대해서 처음 접하신다면 한번에 이해하기는 버거우실 겁니다. C언어를 공부하면서 항상 부족한 부분이 생기는 이유는 포인터에 대한 개념이 약해서 라고 생각듭니다. 직접 코딩을 하지 않는다면 포인터에 대한 개념을 확실히 이해 하기 어렵습니다. 여러번 반복하고 시행착오를 거치면서, 삽질도 하다보면, 어느순간 느낌이 올 때가 있습니다. 이러한 개념들은 서서히 이해도가 높아지는 것이아니고, 어느순간 팍! 하고 와닿는 것이기 때문에 욕심낼 필요가 없다 생각됩니다. 와닿는 순간 신세계를 보는 것이겠죠. 저에게도 그런 신세계는 앞으로 수없이 많아 보입니다. ....
아무튼 쓰다보니 계속 써야할 부분이 생기네요. void*형 캐스팅을 할 때 주의해야할 점을 쓰려하니 너무 길어지는 듯하여 다음으로 미루겠습니다.