28. 메모리(Memory)
1. 키워드
- 메모리(Memory)
malloc
(Memory Allocation)- 동적 메모리 할당(Dynamic Memory Allocation)
- 스택(Stack)과 힙(Heap)
- 메모리 누수(Memory Leak)
memset
(Memory Set)- 널 포인터(
NULL
Pointer)
2. 메모리 사용하기
- 이번에는 포인터에 원하는 만큼 메모리 공간을 할당받아 사용하는 방법을 알아보자.
- 먼저 다음과 같이 메모리는
malloc
→사용
→free
패턴으로 사용한다.
3. 메모리 할당하기
- 메모리를 사용하려면
malloc
함수로 사용할 메모리 공간을 확보해야 한다. - 이때 필요한 메모리 크기는 바이트 단위로 지정한다.
- 메모리 할당과 해제 함수는
stdlib.h
헤더 파일에 선언되어 있다.
- 다음 내용을 입력한 뒤 실행해 보자.
- 만약 컴파일할 때
"error C2440: '=': 'void *'에서 'int *'(으)로 변환할 수 없습니다."
에러가 발생한다면 파일의 확장자가.c
가 맞는지 확인한다. - 파일 확장자가
.cpp
이면 C++ 컴파일러를 사용하므로 에러가 발생한다.
#include <stdio.h>
#include <stdlib.h> // malloc, free 함수가 선언된 헤더 파일
int main()
{
int num1 = 20; // int 타입 변수 선언
int *numPtr1; // int 타입 포인터 선언
numPtr1 = &num1; // num1의 메모리 주소를 구하여 numPtr에 할당
int *numPtr2; // int 타입 포인터 선언
numPtr2 = malloc(sizeof(int)); // int 타입의 크기 4바이트만큼 동적 메모리 할당
printf("%p\n", numPtr1); // 변수 num1의 메모리 주소 출력
// 컴퓨터마다, 실행할 때마다 달라짐
printf("%p\n", numPtr2); // 새로 할당된 메모리의 주소 출력
// 컴퓨터마다, 실행할 때마다 달라짐
free(numPtr2); // 동적으로 할당한 메모리 해제
return 0;
}
// 0x006bfa60
// 0x009659f0
- 메모리를 할당할 때는
malloc
함수를 사용하며 할당할 메모리 공간의 크기를 넣어준다. - 여기서는
sizeof
연산자를 사용하여int
타입의 크기(4바이트)만큼 메모리를 할당했다. - 특히 원하는 시점에 원하는 만큼 메모리를 할당할 수 있다고 하여 동적 메모리 할당이라 부른다.
printf
함수로numPtr1
과numPtr2
의 값을 출력해 보면 메모리 주소가 출력된다.
printf("%p\n", numPtr1); // 변수 num1의 메모리 주소 출력
// 컴퓨터마다, 실행할 때마다 달라짐
printf("%p\n", numPtr2); // 새로 할당된 메모리의 주소 출력
// 컴퓨터마다, 실행할 때마다 달라짐
- 여기서
numPtr1
에는 일반 변수의 메모리 주소를 할당했고,numPtr2
에는malloc
함수로 메모리를 할당했다. - 같은 메모리 주소라도 내부적으로는 약간의 차이가 있다.
- 스택과 힙 두 가지인데 변수는 스택에 생성되며
malloc
함수는 힙 부분의 메모리를 사용한다. - 이때 스택, 힙의 위치와 커지는 방향은 OS 및 플랫폼에 따라 달라질 수 있다.
- 스택과 힙의 큰 차이점은 메모리 해제에 있다.
- 스택에 생성된 변수는 사용한 뒤 따로 처리를 해주지 않아도 되지만,
malloc
함수를 사용하여 힙에서 할당한 메모리는 반드시 해제를 해줘야 한다. - 따라서 다음과 같이
free
함수로 메모리를 해제한다.
- 메모리 해제는 선택이 아닌 필수이다.
- 메모리를 할당만 하고 해제를 해주지 않으면 결국에는 시스템의 메모리가 부족해지므로 OS가 프로그램을 강제로 종료시키거나 메모리 할당에 실패하게 된다.
- 특히 메모리를 해제하지 않아 메모리 사용량이 계속 증가하는 현상을 메모리 누수라고 부른다.
4. 메모리에 값 저장하기
- 이번에는 할당한 메모리에 값을 저장해 보자.
#include <stdio.h>
#include <stdlib.h> // malloc, free 함수가 선언된 헤더 파일
int main()
{
int *numPtr; // int 타입 포인터 선언
numPtr = malloc(sizeof(int)); // int 타입의 크기 4바이트만큼 동적 메모리 할당
*numPtr = 10; // 포인터를 역참조한 뒤 값 할당
printf("%d\n", *numPtr); // 포인터를 역참조하여 메모리에 저장된 값 출력
free(numPtr); // 동적 메모리 해제
return 0;
}
// 10
malloc
함수로 할당한 메모리에 값을 저장할 때는*numPtr = 10;
처럼 포인터를 역참조한 뒤 값을 저장하면 된다.- 마찬가지로
printf
함수로 값을 출력할 때도 포인터를 역참조하여 값을 가져오면 된다. - 메모리를 할당하고 사용한 뒤에는 반드시
free
함수로 해제를 해준다.
malloc
함수에 sizeof(int) * 1024 * 1024 * 1024
를 넣으면 어떻게 될까?
- 다음과 같이 작성해 보자.
- 문제없이 할당이 된다.
- 계산을 해보면 4,294,967,296인데 GiB로는 4GiB이다.
- 더 큰 메모리도 할당할 수 있지만 실제로 값을 저장할 때는 시스템의 한계 이상 저장할 수 없다.
5. 메모리 내용을 한꺼번에 설정하기
- 포인터를 역참조한 뒤 값을 할당할 때는 해당 자료형 크기만큼만 할당할 수 있다.
string.h
헤더 파일에 선언되어 있는memset
함수를 사용하면 메모리의 내용을 원하는 크기만큼 특정값으로 설정할 수 있다.- 이때 설정하는 크기는 바이트 단위이다.
- 다음 내용을 입력한 뒤 실행해 보자.
#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); // 27이 8개 들어가 있음
free(numPtr); // 동적으로 할당한 메모리 해제
return 0;
}
// 0x2727272727272727
- 먼저
memset
함수를 사용하려면string.h
또는memory.h
헤더 파일을 포함해야 한다. - 그리고
memset
함수에 포인터, 설정할 값, 설정할 크기를 넣으면 된다. - 여기서는
memset(numPtr, 0x27, 8);
과 같이 사용했으므로numPtr
이 가리키는 메모리에는 16진수27
이8
개 들어가게 된다.
memset
함수는 주로 다음과 같이 설정할 값을0
으로 지정하여 메모리의 내용을 모두0
으로 만들 때 주로 사용한다.
자료형의 크기와 포인터의 크기
memset
함수에 설정할 크기를 지정할 때 보통 숫자 대신sizeof
를 사용한다.
long long *numPtr = malloc(sizeof(long long));
memset(numPtr, 0, sizeof(long long)); // numPtr이 가리키는 메모리를 long long 타입의 크기만큼 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);
memset(numPtr, 0, sizeof(char));
는 메모리를 1바이트만큼0
으로 설정하니 문제가 없다.- 하지만
memset(numPtr, 0, sizeof(char *));
는 32비트에서 4바이트, 64비트에서 8바이트만큼0
으로 설정하므로 할당받은 메모리 크기를 넘어서게 된다.
- 다른 예로 32비트에서
sizeof(long long)
은 8바이트인데sizeof(long long *)
는 4바이트이므로 할당받은 메모리보다 작은 공간을 설정하게 된다. - 64비트에서
sizeof(long long *)
는 8바이트로 그냥 크기가 일치한 것일 뿐 잘못된 방법이다.
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
를 사용할 때는 이러한 부분을 주의해야 한다.
6. 널 포인터 사용하기
- 메모리가 할당된 포인터도 있지만, 메모리가 할당되지 않은 포인터도 있다.
#include <stdio.h>
int main()
{
int *numPtr1 = NULL; // 포인터에 NULL 저장
printf("%p\n", numPtr1);
return 0;
}
// 0x0
NULL
이 들어있는 포인터를 널 포인터라고 하며 아무것도 가리키지 않은 상태를 뜻한다.- 따라서 역참조는 할 수 없다.
- 실무에서는 다음과 같이 포인터가
NULL
인지 확인한 뒤NULL
이면 메모리를 할당하는 패턴을 주로 사용한다.