메모리 레이아웃(메모리 구조)
- 스택 메모리(지역 변수, 매개 변수, 배열)
- 힙 메모리
- 코드 섹션(전역 변수(파일 내에 전부 적용되는 변수), 정적 변수(별도의 공간에 값을 저장하고 복제본을 출력해서 가져오는 변수))
- 데이터 섹션
스택 메모리 : 함수가 호출되면 자라났다가 호출이 끝나면 다시 줄어든다.(함수의 값이 저장된 메모리용량이 늘어났다가 함수가 소멸되면 다시 줄어든다?)
힙 메모리도 스택 메모리와 같은 양상을 띈다.
코드 섹션 : 우리가 작성한 소스코드들이 빌드된 결과물들이 저장되는 곳
데이터 섹션 : 전역 변수와 정젹 변수등이 저장되는 곳.
스택 메모리의 단점
- 스택 메모리에 저장되는 지역 변수(배열 등)의 크기는 컴파일 타임에 고정된다.
런타임에 더 필요해진다 해도 크기를 늘릴 수가 없다.
- 함수가 종료되면 해당 스택 프레임에 더이상 접근 불가능하다.
지역변수의 수명은 함수의 수명과 함께한다.
더 오래 보존하려면 전역변수 혹은 정적 변수로 선언해야 함.
힙 메모리
- 프로그래머의 메모리 할당과 해제를 통해 관리되는 동적 할당 영역.
- 스택 메모리와 달리 함수의 호출 및 종료에 따라 자동으로 정리되지 않고, 프로그래머가 원하는 만큼, 원하는 때에 할당 및 반납이 가능함(장점)
힙 메모리의 단점
- 스택 메모리에 비교해 할당/해제 속도가 느림.
스택 메모리는 자료구조 스택의 특성상 할당 및 해제에 0(1) == 아주 짧은 시간이 걸림.
힙 메모리는 할당 받아오려면 사용중이지 않은 메모리이면서 크기가 맞는지 체크 후 제공됨.
또한 메모리 공간에 구멍(메모리 단편화)이 생길 수도 있어 효율적인 메모리 관리가 어려움.
- 프로그래머가 직접 메모리 할당 및 해제를 해야함.
메모리 할당만 하고 해제는 안하는 실수를 할 여지가 있음.
동적 할당의 세 가지 단계
메모리 할당(대여)
힙 메모리 관리자에게 필요한 바이트만큼의 메모리를 달라고 요청시,
힙 메모리 관리자는 해당 크기의 연속된 메모리를 찾아서 반환함.
반환된 값은 시작 메모리 주소.
메모리 사용
할당된 힙 메모리 시작 주소를 가지고 원하는 작업을 수행.
이때 할당된 메모리 속 데이터는 쓰레기값.
메모리 해제(반납)
힙 메모리 관리자에게 해당 메모리 주소를 돌려주면서 다 썼다고 알림,
힙 메모리 관리자는 해당 메모리를 점유되지 않은 메모리 상태로 바꿈.
메모리 주소를 돌려주지 않으면 메모리 누수(Memory leak)가 발생함.
메모리 누수란, 해당 메모리가 점유 상태를 벗어나지 못해 사용가능한 메모리가 줄어드는 현상.
동적 메모리 관련 함수
할당 : malloc()
해제 : free()
void* malloc(size_t size)
memory allocation의 약자. stdlib.h 헤더파일에 선언되어 있음.
size 바이트 만큼의 메모리를 반환해줌.
할당 실패시 NULL을 반환함.
void free(void* ptr)
동적 할당 받은 메모리를 해제하는 함수.
메모리 할당 함수를 통해서 얻은 메모리 주소만 해제 가능함.
malloc()을 작성했다면 곧바로 free()부터 작성하기
동적 할당 받은 메모리 주소를 지역 변수에 저장해뒀다가 해제 안하고 함수가 종료되버리면,
해당 지역변수를 접근할 방법이 사라져 버려서 지울 방법이 없어짐.
이렇게 반납하지 않을 시 메모리 누수가 발생하므로 꼭 free()부터 작성하는 습관 들이기.
어려움
// Main.c
#include <stdio.h>
#include <stdlib.h> // void* malloc(size_t size)을 가져오는 공구통(헤더 파일)
int main(void)
{
size_t i; // 실제 자료형은 unsigned long long. unsigned long long을 써도 되지만 너무 길어서 축약한 버전. 자료형의 레퍼런스(참조자)라고 봐도 될듯.
// unsigned long long -> 크기(양수)로 재정의를 함.
int* Nums = (int*)malloc(10 * sizeof(int)); // (10 * sizeof(int) == int 크기의 10 칸 만큼을 메모리얼로케이션을 해줘. ->(40byte)
// (int*)malloc == malloc가 void 형태의 포인터이기 때문에 int 타입으로 형변환을 시킴.
// int 타입으로 형변환된 포인터 malloc의 메모리주소값을 int 타입의 포인터 Nums의 메모리주소값에 대입시킴.
if (NULL == Nums)
{
return 0;
}
// Do something....
// 만약 여기에 Nums = NULL; 을 작성하면 free(); 를 통해 힙 메모리 관리자에게 돌려줘야할 메모리 주소가 NULL로 되어 오류가 발생함.
free(Nums); // int 타입의 포인터 Nums를 메모리 해제(반납)하여 힙 메모리 관리자에게 메모리 주소를 돌려줌.
Nums = NULL; // 힙 메모리에 Nums의 값이 그대로 남아있기에 포인터를 초기화하는 NULL을 대입시켜서 값을 초기화함.
// 코딩 컨벤션 : 공동작업 하는 프로그래머 끼리 정해놓는 규칙. 반환한 메모리 주소에 NULL을 넣어주면 동적할당 된 메모리를 사용하기 전에 if(Nums != NULL) 을 체크해서 보다 안전하게 사용 가능.
// if문으로 Nums가 NULL이 아닐 때만 true 값이 나와서 한 번 체크할 수 있음.
// 이 뒤로 Nums를 다시 대여해서 메모리 할당을 받아선 안됨.
if (NULL != Nums)
{
// TODO.
}
return 0;
}
어려움
// Psudo code.
// 책을 빌려왔으면 빌려온 사람이 반납까지 해야한다...?
char* CombineString(const char* LHS, const char* RHS) // char 타입의 포인터 함수(값이 변하지 않는 매개 포인터 함수 LHS, RHS)
{
char* APStr; // char 타입의 포인터 함수 APStr 선언
siz_t Size = strlen(LHS) + strlen(RHS); // unsigned long long
APStr = (char*)malloc(Size + 1);
if (NULL == APStr) // 포인터 APStr의 메모리주소가 NULL과 동일하다면 true
{
return NULL;
}
/* 복사 생략 */
/*
호출자 입장에선 CombineString() 함수가 내부에서
동적 메모리를 할당해서 반환한다는 사실을 어떻게 알 수 있을까요?
이처럼 동적할당된 메모리의 해제 문제는 복잡합니다.
C++에서는 RAII라는 개념을 통해 메모리 해제 실수를 구조적으로 막을 수 있습니다.
*/
return APStr; // 이 시점에서 main 함수나 다른 함수에서 이 함수가 동적 메모리를 할당해서 반환했다는 사실을 알 수 없다.
// 또한 받은 함수 쪽에서 free() 로 동적 메모리를 반납할 수 없다.
}