Chapter 25. 메모리 관리와 메모리의 동적 할당
25-1 C언어의 메모리 구조
프로그램을 실행하면 해당 프로그램의 실행을 위한 메모리 공간이 운영체제에 의해서 미리 마련이 된다. 그리고 바로 이 메모리 공간 내에서 변수가 선언되고, 문자열이 선언되는 것이다.
메모리의 구성
프로그램 실행 시 운영체제에 의해서 마련되는 메모리의 구조는 다음과 같이 네 개의 영역으로 구분이 된다.
● 코드 영역
● 데이터 영역
● 힙 영역
● 스택 영역
이렇게 메모리 공간을 나눠놓은 이유는 유사한 성향의 데이터를 묶어서 저장하면, 관리가 용이해지고 메모리의 접근속도가 향상되기 때문이다.
메모리 영역별로 저장되는 데이터 유형
코드 영역(Code Area)
이름 그대로 코드가 저장되는 메모리 공간이다. 따라서 CPU는 코드영역에 저장된 명령문들을 하나씩 가져가서 실행을 한다.
데이터 영역(Data Area)
전역변수와 static 변수가 할당된다. 즉, 이 영역에 할당되는 변수들은 "프로그램의 시작과 동시에 메모리 공간에 할당되어 프로그램 종료 시까지 남아있게 된다" 는 특징이 있다.
스택 영역(Stack Area)
지역변수와 매개변수가 할당된다. 즉, 이 영역에 할당되는 변수들은 함수가 시작되면 메모리 공간에 할당되어 함수를 빠져나올때 소멸된다.
힙 영역(Heap Area)
데이터 영역과 스택 영역에 선언되는 변수와 달리 다른 성격(원하는 시점에 변수를 할당하고 또 소멸하도록)을 지닌 변수가 할당이 된다.
프로그램의 실행에 따른 메모리의 상태 변화
지금까지는 '프로그램의 시작은 main 함수의 호출에서부터이다!'라고 이야기해 왔지만, 실제로는 main함수가 호출되기 이전에 데이터 영역이 먼저 초기화된다. 그리고 그 후 main 함수가 호출된다.
main함수의 return 문이 실행되면서 프로그램이 종료가 되면 운영체제에 의해서 할당된 메모리 공간 전체를 반환하게 되는데, 바로 그때가 전역변수가 소멸되는 시점이다.
스택(Stack)은 "쌓아 올려진 더미'를 뜻한다. 그리고 후입선출(LI-FO : Last In First Out)의 구조를 가졌다.
25-2 메모리의 동적 할당
전역변수와 지역변수 로만 충분하다고 생각할지 모른다. 그러나 프로그램을 구현하다 보면 이 둘과는 다른 유형의 변수가 필요로 하게 된다.
동적 할당 이란 할당되는 메모리의 크기를 컴파일러가 결정하지 않고, 프로그램의 실행 중간에 호출되는 malloc 함수가 결정하기 때문이다.
전역변수와 지역변수로 해결이 되지 않는 상황
이 둘로 해결이 되지 않는 상황이 있다. 예를 들어서 하나의 함수가 있는데 이 함수를 호출할때마다 사람의 이름을 입력받으려고 하면 이전 값이 지워지거나 그 주소값이 유효하지 않는 값으로 바껴버려 나중에 다른 데이터를 저장할때 쓰게 된다.
이럴때 사용하는게 동적할당이다 지역변수는 함수가 시작될때 할당되어 종료될때 소멸되고, 전역변수는 시작할때 생성 종료될때 소멸된다. 그러나 동적할당을 이용하면 사용자가 원할때 할당 또는 소멸을 시킬 수 있다.
힙 영역의 메모리 공간 할당과 해제: malloc과 free 함수
#include <stdlib.h>
void* malloc(size_t size); // 힙 영역으로의 메모리 공간 할당 // 성공시 주소값 반환 실패시 NULL 반환
void free(void* ptr); // 힙 영역에 할당된 메모리 공간 해제
힙 영역을 흔히 '프로그래머가 관리하는 메모리 공간'이라고 한다. malloc 함수호출로 할당된 메모리 공간은 프로그래머가 직접 free 함수의 호출을 통해서 해제하지 않으면 계속 남아있기 때문이다.
결국 malloc과 free는 fopen 과 fclose 처럼 한 쌍으로 사용해야 한다.
int main(void)
{
void* ptr1 = malloc(4); // 4바이트가 힙 영역에 할당
void* ptr2 = malloc(12); // 12바이트가 힙 영역에 할당
. . . . .
free(ptr1); // ptr1이 가리키는 4바이트 메모리 공간 해제
free(ptr2); // ptr2가 가르키는 12바이트 메모리 공간 해제
. . . . .
}
이렇듯 malloc 함수는 인자로 전달된 정수 값에 해당하는 바이트 크기의 메모리 공간을 힙 영역에 할당하고,
이 메모리 공간의 주소 값을 반환한다. 따라서 위의 코드를 실행하게 되면 ptr1은 첫 번쨰 malloc 함수호출을 통해서 할당된 메모리 공간의 첫 번째 바이트를 가리키게 된다. 그리고 free 함수를 호출하는 시점에 ptr1이 가르키는 메모리 공간은 소멸된다.
malloc 함수는 주소 값을 반환하고, 그 주소 값을 이용해서 힙에 접근을 해야 하기 한다. 그래서, 힙에 할당된 메모리 공간은 포인터 변수를 이용해서 접근하는 방법밖에 없다.
malloc 함수의 반환형이 void형 포인터인 이유와 힙 영역으로의 접근
Chapter 19에서 우리는 주소 값을 담는 바구니에 지나지 않는 void형 포인터에 대해서 공부한바 있다.
그런데 malloc 함수의 반환형은 void형 포인터 이다. 따라서 malloc 함수의 반환 값에 아무런 가공도 가하지 않으면, 이를 이용해서 할당된 메모리 공간에 접근이 불가능하다.
void* ptr = malloc(sizeof(int)); // int형 변수 크기의 메모리 공간 할당
*ptr = 20; // ptr이 void형 포인터이므로 컴파일 에러
malloc 함수는 포인트 형을 결정할 수 없다. 전달받는 것은 숫자 뿐이므로 무슨 자료형의 데이터를 넣을지 알수가 없다. 그러므로 void형 포인터를 반환하는 것이다. 우리의 입맛에 맞게 형변환으로 사용하면 된다.
free 함수를 호출하지 않으면 프로그램 종료 후에도 메모리가 남게 되나요?
프로그램 실행시 할당된 메모리 공간은 프로그램이 종료되면 운영체제에 의해서 전부 해제가 된다.
그러나 앞으로 구현하게 될 프로그램은 지금 우리가 구현하는 콘솔형식 처럼 간단히 실행되고 종료되는 프로그램이 아니다. 그러므로 실제 프로그램 구현에서는 반드시 free함수를 호출해야 한다.
malloc 함수의 사촌 뻘 되는 calloc 함수
힙 영역 메모리 공간을 할당하는 함수로 calloc 이라는 함수가 다음과 같이 추가로 정의되어 있다.
malloc 함수와 유일한 차이점은 메모리 공간의 할당을 위한 인자의 전달방식에 있다.
#include <stdlib.h>
void* calloc(size_t elt_count, size_t elt_size);
-> 성공 시 할당된 메모리의 주소 값, 실패 시 NULL 반환
두 개의 숫자를 인자로 전달받고 첫 번째 인자는 갯수, 두 번째 인자는 크기로 받는다.
calloc(30, 4); -> 4바이트 크기의 블록 30개를 힙 영역에 할당해 주세요.
그 외로 calloc 함수로 선언하면 그 할당된 공간의 값을 0으로 초기화 시켜주나 malloc 함수는 쓰레기 값이 저장되어 있다. 그리고 calloc 함수로 할당된 메모리 공간을 해제할 때도 free 함수를 사용하면 된다.
힙에 할당된 메모리 공간 확장 시 호출하는 realloc 함수
한번 할당된 메모리 공간은 그 크기를 확장할 수 없다, 이는 모든 영역의 메모리 공간에 해당하는 말이다. 이미 할당되어버린 배열의 길이를 늘릴 수 있는가? 어느 영역에 선언을 하건 간에 이러한 일은 불가능하다. 그러나 힙 영역은 realloc 함수를 사용해서 가능하다.
#include <stdlib.h>
void * realloc(void * ptr, size_t size);
-> 성공 시 새로 할당된 메모리의 주소 값, 실패 시 NULL 반환
-> "ptr이 가르키는 메모리의 크기를 size 크기로 조절해줘(늘려줘)"
malloc 함수가 반환한 주소값과 realloc 함수가 반환한 주소 값이 같은 경우와 다른 경우가 있다.
이는 기존에 할당된 메모리 공간 뒤를 이어서, 확장할 영역이 넉넉한 경우에 발생하고 공간이 부족하면 새로운 곳에 복사시켜 할당하고 기존에 할당된 것은 소멸시키므로 반환되는 주소 값이 달라진다.