F.R.I.D.A.Y.

void 포인터(메모리) 본문

DEV/C C++

void 포인터(메모리)

F.R.I.D.A.Y. 2019. 12. 8. 22:39
반응형

https://www.template.net/design-templates/backgrounds/digital-background/

 C언어의 꽃이라 불리는 포인터는 그 사용의 범위가 넓은 만큼, 많은 초보 개발자들이 힘들어하는 문법이기도 합니다. 오늘은 이러한 포인터 중에서도 특별한 void 포인터에 대해 알아봅니다.

 

 모든 포인터[# typedef된 자료형 제외]는 기본 자료형 뒤에 *(asterisk)가 표시되어 있습니다. void 포인터이니 먼저 void 자료형에 대해 알아봅니다.


void

 일전에 main 함수의 반환 타입은 int가 표준이라는 포스트를 작성한 적이 있습니다. void도 결국은 반환 타입입니다. 그렇다면 void는 어떤 것을 반환할까요?

 

 정답은 '아무도. 아니, 작성자 본인만 안다'입니다. void 타입은 많은 사람들이 반환 타입이 존재하지 않으면, 달리 말해 반환할 대상이 없다면 void를 작성한다고 설명합니다.

더보기

 짧게 설명하면 CPU는 연산을 수행하면 그 값을 EAX라는 레지스터에 보관합니다. 이 레지스터를 이용하면 반환 타입이 void라 하더라도 다른 함수에서 연산한 결과를 caller 함수에서 이용할 수 있습니다. 이번 포스트는 void 포인터에 대한 내용이니 CPU 레지스터에 대한 내용은 다른 포스트로 만나겠습니다.

 반환될 값이 정수 타입일지, string일지, 실수일지는 아무도 모른다는 의미에서 void를 이용합니다.

 

 무릇, 포인터는 메모리를 asterisk 앞의 데이터 타입을 기준으로 읽어 들입니다. 그렇다면 void 포인터는 어떤 의미일까요?

 

void 포인터(void pointer, void *)

 void가 타입을 특정할 수 없는 데이터를 의미한다면, void *는 타입을 특정할 수 없는 데이터 주소를 값으로 가지는 자료형입니다. 조금 더 쉽게 말하면 가리키는 메모리의 길이가 정해지지 않은 자료형이라는 말이죠.

 길이가 특정되지 않은 자료형을 어디에 사용하느냐고요? 많은 부분에서 사용합니다. 오히려 이 포인터의 존재로 인해 우리는 조금 더 수월하게 프로그래밍을 진행하곤 합니다.


void 포인터의 중요성

 처음 C언어에 입문하면, 많은 책과 사이트에서 첫 예제로서 다음 코드를 작성해 실행해보라고 권합니다.

#include <stdio.h>

int main(void){
    printf("Hello world!\n");
    return 0;
}

 처음에는 어떤 원리로 화면에 출력되는지도 모르고 무작정 따라 작성하지만, 배움의 시간이 늘어남에 따라 우리는 다양한 함수를 직접 만들고 구현하며 프로그램을 만들어갑니다. 그러다 동적 할당이라는 기술을 배우면 우리는 malloc, calloc, realloc과 같은 함수를 이용합니다. 이 함수들의 반환형은 다음과 같습니다.

 

void *

 

 만일 void 포인터가 존재하지 않았다면 우리는 반환형에 따른 함수를 모두 만들어주어야 했을 것입니다. 빌트인 자료형에 대해서는 그래도 할 만합니다. 그러나 사용자 정의 자료형에 대해서는 어떻게 할 수 있을까요? 사용자가 일일이 그에 대한 함수를 작성해야 할까요? 우리는 이것이 굉장히 비효율적임을 알고 있습니다. 그래서 void 포인터가 탄생한 것입니다.

더보기

 CPU는 다음 내용을 가지고 연산을 진행합니다.

 

명령어, 메모리 주소, 해당 메모리 길이, 값

 

 컴파일러는 앞에 *(asterisk) 앞에 자료형을 표현함으로써 <해당 메모리 길이>를 이용자가 직접 작성하지 않아도 되도록 알아서 처리합니다. 그러나 void 타입이 앞에 존재하는 void 포인터의 경우에는 <해당 메모리 길이>에 대한 값을 컴파일러가 처리하지 않으므로 이용자가 직접 결정할 권한을 가지도록 해줍니다. 조금 불편할 수 있지만, 역으로 보면 오히려 선택의 폭이 넓어진 것입니다.

 


어디에 쓰고 있을까

 메모리와 관련된 많은 함수에서 인자의 타입으로 void *를 사용하고 있습니다.

void *memset(void *dest, int c, size_t count);
void *memcpy(void *desk, const void *src, size_t count);
void *malloc(size_t size);
void *calloc(size_t number, size_t size);
void *realloc(void *memblock, size_t size);
void free(void *memblock);

// reference : ms docs

 이러한 메모리 관련 함수들은 사용자가 사용하고자 하는 메모리가 어떤 타입인지 모르기 때문에 모두 void *를 이용해 구성되어있습니다. 만일 이러한 함수들이 void 타입의 포인터가 아닌, 명시된 타입의 포인터로 작성되어있다면 많은 컴파일러에서 타입이 일치하지 않다고 경고를 뿜거나 심하면 에러를 반환하고 컴파일을 중단할 수 있습니다.

 


 앞서 봐온 것처럼 메모리는 사용자가 이용하기 나름입니다. 메모리에는 타입이 명시되어있지 않습니다. 타입이 확정적이라는 고정관념에 사로잡혀 있으면 메모리를 사용하는 방법 또한 제한될 뿐입니다.

 유연한 사고를 해야 하는 많은 순간들이 있겠지만, 그중에서도 프로그래밍은 문제를 유연하게 대처해야 하는 자세가 많이 필요합니다.


Extra. 메모리 이용하기

 우리는 메모리에 자료형이 존재하지 않음을 알게 되었습니다. 그래서 우리는, 한번 시험해봅니다. 아래와 같은 구조체가 하나 있다고 생각합니다.

struct test{
	int a;
	char b;
	short c;
	char d;
};

int main(void){
	
	struct test t1;
	
	t1.a = t1.b = t1.c = t1.d = 0;
	
	// 여기에 작성하세요.
    
	printf("%d %hhd %hd %hhd\n", t1.a, t1.b, t1.c, t1.d);
	
	return 0;
}

 반환 값으로 다음 값이 순서대로 출력되어야 합니다. 즉, 자료형이 struct test인 변수 t1의 멤버 b의 값이 4가 되어야 합니다.

0 4 0 0

 

 단, 아래와 같이 직접 b에 접근해선 안됩니다.

t1.b = 4;

 한 번 도전해보세요. 문제의 정답은 더보기를 눌러 확인할 수 있습니다.

더보기

# 정답 보기

#include <stdio.h>

struct test{
	int a;
	char b;
	short c;
	char d;
};

int main(void){
	
	struct test t1;
	
	t1.a = t1.b = t1.c = t1.d = 0;
	
	*((char *)&t1 + 4) = 4;
	
	printf("%d %hhd %hd %hhd\n", t1.a, t1.b, t1.c, t1.d);
	
	return 0;
}

 이 코드뿐만 아니라, t1의 멤버 b에 접근하는 방법은 여러 가지가 있습니다. 이 문제를 풀어봤다면 c, d 멤버에 접근하는 방법도 생각해보세요.

# index

728x90
반응형
Comments