본문 바로가기

프로그래밍/C

[C] 이 메모리는 무료로 할당해 줍니다. (실전편)

들어가며

오늘은 필자가 주로 애용하는 유튜브 채널을 추천해 줄 생각이다.

공부를 하거나 코딩을 하거나 이렇게 블로그를 운영할 때

적막함을 한 층 덜어준 아주 고마운 채널이다.

소리를 적당히 줄이고 배경음이나 백색소음처럼 깔아 두면

잔잔한 음악이 꽤나 괜찮은 채널이다.

여러분도 한 번 애용해 보는 것을 추천한다.

 

실전 박치기

전 글에서는 메모리 할당과 해제에 대해서 알아보았다.

그렇다고 딱히 뭔가 실제로 사용해보지는 않았다.

따라서 오늘은 여러 코드를 가지고 해석해 보며

실전적으로 알아볼 예정이다.

아무튼 다음 코드를 보고 해석해 보라.

#include <stdio.h>
#include <stdlib.h>		       // malloc, free 함수가 선언되어 있는 헤더

int main()
{
	int num1 = 20;
	int* numptr1;

	numptr1 = num1;

	int* numptr2;

	numptr2 = malloc(sizeof(int));	// int = 4바이트만큼 동적 할당

	printf("%p\n", numptr1);	// 변수 num1의 메모리 주소

	printf("%p\n", numptr2);	// 동적 할당된 메모리의 첫 번째 주소

	free(numptr2);

	return 0;
}

 

실행 결과에서 출력되는 메모리 주소는 컴퓨터마다, 또 실행할 때마다 달라진다.

이 코드에서는 sizeof 연산자를 사용하여 int의 크기인

4바이트만큼의 메모리를 할당하였다.

 

스택과 힙의 영역

여기서 주목할 코드는 다음의 두 줄이다.

printf("%p\n", numptr1);	// 변수 num1의 메모리 주소
printf("%p\n", numptr2);	// 동적 할당된 메모리의 첫 번째 주소

 

numptr1은 일반 변수의 메모리 주소를 할당했고

numptr2는 malloc() 함수를 사용하여 메모리를 할당했다.

똑같이 메모리 주소가 출력되지만 내부적으로 약간의 차이가 있는데

그것은 바로 각각의 변수가 할당받은 영역이 다르다는 것이다.

그림 14 - 스택과 힙의 영역

기본 편에서도 말했다시피,

변수는 스택(Stack)에 메모리 공간을 할당받아 생성되고

malloc 함수는 힙(Heap) 영역의 메모리를 사용한다.

(스택과 힘의 위치와 커지는 방향은 운영체제 및 플랫폼에 따라 달라질 수 있다.)

가장 큰 차이점이라 함은 '메모리 해제의 유무'라고 할 수 있다.

스택에 정적 할당된 변수는 사용한 뒤, 따로 처리를 하지 않아도 되지만

힙에 malloc 함수로 동적 할당된 메모리는 반드시 free 함수로 해제를 해줘야 한다.

 

???: 산와, 산와, 산와 머니.

메모리 해제는 선택이 아닌 필수다.

다시 한번 더 말하지만

메모리 해제는 선택이 아닌 필수다.

해제하는 방법은 매우 간단하다.

free(numptr2);

 

free 함수를 호출할 때 인자 값으로

해제하고자 하는 포인터 변수의 이름을 넘겨준다.

실무에서는 메모리를 자주, 그리고 많이 할당한다고 한다.

따라서 메모리를 할당만 하고 해제하지 않으면,

그림 15 - 메모리 해제 안 해서 화난 운영체제

 

엄격하고 근엄하며 진지한 아저씨와 신나고 격렬한 쌀보리 게임을 할지도 모른다.

그러니 제때제때 메모리를 해제하여,

사용이 끝난 메모리를 해제하지 않아서 메모리가 부족해지는 현상,

'메모리 누수(Memory Leak)'가 발생하지 않도록 주의하도록 하자.

 

실전 후려치기

이번에는 할당한 메모리에 값을 저장해 보도록 하겠다.

#include <stdio.h>
#include <stdlib.h>		// malloc, free 함수가 선언되어 있는 헤더

int main()
{
	int* numptr;

	numptr = malloc(sizeof(int));	// int = 4바이트만큼 동적 할당

	*numptr = 10;               // 포인터를 역참조한 후 값 저장

	printf("%d\n", *numptr);    // 포인터를 역참조하여 메모리에 저장된 값 '10' 출력

	free(numptr);               // 절.대.해.제.해

	return 0;
}

 

malloc 함수로 할당한 메모리 값을 저장할 때는

일반 포인터를 사용하는 방식과 마찬가지로

해당 포인터를 역참조하여 값을 저장하면 된다.

출력하는 것도 같은 방식으로 역참조하여 사용하면 된다.

그리고 다 사용한 메모리는 반드시 free 함수로 해제하도록 하자.

 

실전 돌려까기

포인터를 역참조한 뒤 값을 할당할 때는

해당 자료형 크기만큼만 할당할 수가 있었다.

예를 들어 int형 포인터라면 4바이트 크기만큼 할당한다는 말이다.

 

하지만 어떤 함수를 사용하면 메모리의 내용을

'원하는 크기'만큼 '특정값'으로 설정할 수 있다.

그 함수의 정체는 다음과 같다.

  • memset(포인터, 설정할 값, 크기)
    • void* memset(void* _Dst, int _Val, size_t _Size);
    • 값 설정이 끝난 포인터를 반환

으레 C언어 계통의 함수 이름이 그렇듯이

'memory set'에서 따온 것으로 예상이 된다.

그럼 다음의 소스 코스를 어디 한번 해석해 보도록 하자.

#include <stdio.h>
#include <stdlib.h>     // malloc, free 함수가 선언되어 있는 헤더
#include <string.h>     // memset 함수가 선언되어 있는 헤더

int main()
{
	long long* numptr = malloc(sizeof(long long));	// long long의 크기 8바이트만큼 동적 할당

	memset(numptr, 0x27, 8);        // numptr이 가리키는 메모리를 8바이트만큼 0x27로 설정

	printf("0x%llx\n", *numptr);    // 0x2727272727272727: 27이 8개 들어가 있음

	free(numptr);                   // 절.대.해.제.해

	return 0;
}

음, 역시 필자도 이게 뭔 소린가 싶다.

하나하나 차근차근 한번 조져보도록 하자.

 

먼저 memset 함수를 사용하려면 string.h 혹은 memory.h 헤더 파일을 포함해야 한다.

그리고 memset 함수에 '(포인터 이름, 설정할 값, 설정할 크기)'를 넣으면 된다.

위의 코드에서는 'memset(numptr, 0x27, 8);' 과 같이 사용했다.

이것이 의미하는 바는

numptr이 가리키는 메모리에 16진수 27이 8개가 들어간다는 의미다.

즉, 일정한 크기의 어떠한 메모리 공간에 대해서

지정한 크기까지, 저장되어 있는 모든값들을 임의의 값으로 한번에 바꿀 수 있다는 의미다.

그림으로 설명하면 다음과 같다.

그림 16 - 한번에 확!

[그림 16]에서처럼 memset 함수는 주로 다음과 같이

설정할 값을 0으로 지정하여 메모리의 내용을 모두 0으로 만들 때 주로 사용한다.

 

자료형의 크기와 포인터의 크기

memset 함수에 설정할 크기를 지정할 때는

보통 숫자 대신 sizeof 연산자를 사용한다.

다음과 같이 말이다.

long long* numptr = malloc(sizeof(long long));

memset(numptr, 0, sizeof(long long));
// numptr이 가리키는 메모리의 값을 long long 크기(8바이트) 만큼 0으로 설정

 

여기서 메모리를 sizeof(long long) 크기만큼 할당했으므로

설정할 크기도 'sizeof(long long)' 과 같이 지정해야 하며

'sizeof(long long*)' 과 같이 포인터의 크기를 지정하면 된다.

포인터의 크기는 메모리 주소의 크기일 뿐

실제 메모리가 차지하는 크기가 아니기 때문이다.

이 부분은 char 포인터에 메모리를 할당해 보면 잘 알 수 있다.

char* cptr = malloc(sizeof(char));
// char의 크기인 1바이트만큼 동적 할당

memset(cptr, 0, sizeof(char));
// char의 크기인 1바이트만큼 값을 0으로 변경

memset(cptr, 0, sizeof(char*));
// 32비트: char포인터의 크기 4바이트만큼 0으로 설정
// 64비트: char포인터의 크기 8바이트만큼 0으로 설정

free(cptr);

 

위 코드에서 보이는 바와 같이

자료형의 크기와 포인터의 크기는 일치 하지 않다.

'그럼 왜 long long 이랑 long long*은 똑같누'

우연의 일치다.

왜, 뭐.

진짠데

못 믿겠으면 다음의 코드를 확인해 보라.

long long *numptr = malloc(sizeof(long long));
// long long의 크기 8바이트만큼 동적 메모리 할당

memset(numptr, 0, sizeof(long long));
// long long의 크기인 8바이트만큼 값을 0으로 변경

memset(numptr, 0, sizeof(long long *));
// 32비트: long long 포인터의 크기 4바이트만큼 0으로 설정
// 64비트: long long 포인터의 크기 8바이트만큼 0으로 설정

free(numptr);

 

아무튼 memset 함수에서 sizeof를 사용할 때는 이러한 부분을 주의해야 한다.

다시 한번 말한다.

자료형의 크기과 포인터의 크기는 다르다.

 

마치며

일단 메모리의 할당에 관련한 개념은 여기서 마치도록 하겠다.

사실 여기서 알려준 메모리와 관련된 함수보다

훨씬 더 많은 함수들이 존재하지만

대뇌과부하 방지를 위해 열심히 혈액 수냉쿨러를 돌리고 있는 독자들을 위해

아주아주 기본적인 것까지만 하도록 하겠다.

중요한 것은 메모리를 할당 할 수 있는가이다.

우린 아진 배워가는 입장인 것을 잊지 말아야 한다.

좀 더 배운 다음, 더 알아가는 것이 가장 바람직 하다고

적어도 나는 생각한다.

아니라고 생각하는가?

알겠다.

 

아니, 왜 그냥 알겠다고.


참고 및 출처

코딩의 시작, TCP School

코딩 도장