C 프로그램이 실행되면, 스택과 힙이라는 두가지 유형의 메모리 공간이 할당된다.
메모리의 할당과 반환은 컴파일러에 의해 암묵적으로 이루어져, 자동 메모리라고도 불린다.
함수 안에서 변수를 선언하게 되면, 컴파일러가 함수가 호출될 때 스택에 공간을 확보한다. 함수를 리턴하게 되면, 메모리를 반환하게 되기 때문에 더이상 변수를 사용할 수 없게 된다.
void func() {
int x;
}
오랫동안 유지되어야 하는 변수를 위해서는 힙(heap) 메모리가 필요하다. 메모리의 할당과 반환은 프로그래머에 의해 명시적으로 처리된다.
void func() {
int *x = (int *) malloc(sizeof(int));
...
}
위는 코드는 정수에 대한 포인터를 heap에 할당하는 예시이다. 이 경우는 스택과 힙 할당이 모두 발생한다.
malloc() 함수힙에 요청할 공간의 크기를 넘겨주면, 성공시에는 새로 할당된 공간에 대한 포인터를 반환하고 실패시에는 NULL을 반환한다. 함수를 사용하기 위해 stdlib.h를 불러와야 한다.
malloc()의 인자는 size_t 타입의 변수이고 이 변수는 필요 공간의 크기를 바이트 단위로 표시한 것이다. 숫자를 직접 입력하지는 않고, sizeof() 연산자를 이용한다.
malloc()은 void 타입에 대한 포인터를 반환하기 때문에, 해당 주소 공간을 어떤 타입의 자료를 저장할지는 프로그래머가 결정하게 된다. 전형적인 C의 방식. 타입 변환을 이용하여 공간 활용을 결정한다.
free() 함수할당된 메모리를 언제, 어떻게 해제하고 해제 여부를 확인하는 것은 어렵다. 더 이상 사용되지 않는 힙 메모리를 해제하기 위해 프로그래머는 free()를 호출한다. malloc()에 의해 반환된 포인터를 인자로 가진다.
int *x = (int *) malloc(sizeof(int));
...
free(x);
할당된 영역의 크기는 전달되지 않고, 라이브러리가 알고 있다. 어떻게?
*malloc()이나calloc()함수를 호출하여 동적 메모리를 할당하면, 실제로 요청된 크기보다 약간 더 많은 메모리가 할당된다. 추가로 할당된 영역에는 메타데이터 정보가 저장되며, 할당된 메모리 블록의 크기, 사용 여부 등의 정보가 포함된다. 이러한 메타데이터는 메모리 블록이 해제되어야 할 때free()함수가 몇 바이트의 메모리를 해제해야 하는지 알 수 있게 해준다. 일반적으로 이 메타데이터는 할당된 메모리 블록 바로 앞에 위치하게 된다.*
char *src = "hello";
char *dst;
strcpy(dst, src); // *dst는 메모리에 할당되지 않음. segmentation fault 오류 발생
프로그램이 자신이 접근 권한이 없는 메모리 영역에 접근하려고 시도할 때 Segmentation Fault 오류가 발생한다. 자세한 내용은 OSTEP 16 Segmentation에서 알아보자.
char *src = "hello";
char *dst = malloc(strlen(src)); // 메모리를 부족하게 할당
strcpy(dst, src);
buffer overflow 오류라고 불린다. 경우에 따라 오류가 나지 않을 수는 있지만, 프로그램이 한 번 올바르게 실행된다고 하더라도, 올바른 프로그램이라는 것은 아니다.
char *src = "hello";
char *dst = malloc(strlen(src) + 1);
if (dst != NULL) {
strcpy(dst, src);
}
위와 같은 코드가 올바른 코드이다.
새로 할당받은 데이터 타입에 값을 넣지 않는다면, 초기화되지 않은 읽기(uninitialized read) 즉 힙으로부터 알 수 없는 값을 읽는 일이 생긴다.
메모리 누수(memory leak). 장시간 실행되는 응용 프로그램이나 운영체제 자체와 같은 시스템 프로그램에서 큰 문제이다. 메모리가 천천히 누수되면서 결국 메모리 부족으로 시스템을 재시작해야한다.
Garbage Collector가 있더라도 메모리 청크에 대한 참조가 있다면, 그 청크를 해제하지 않을 것이다. 이런 현대적인 언어에서도 memory leak은 문제가 된다.
dangling pointer. 해제된 메모리 포인터를 사용하면 프로그램 크래시가 발생하거나 유효 메모리 영역을 덮어쓸 수 있다. free()를 호출하고 다른 용도롤 malloc()을 호출하면 잘못 해제된 메로리를 재사용할 수 있다.
double free. 해제된 메모리를 관리하게 되는 링크드리스크가 있는데, free()가 두 번 수행되면 같은 메모리가 두번 들어가게 된다. 그러면 다른 변수가 같은 메모리공간을 사용하게 될 수 있다.
malloc() 받은 포인터만 free() 해야한다. 유효하지 않은 해제(invalid frees)는 매우 위험
malloc(), free()를 논의하면서 시스템 콜은 언급하지 않았는데, 두 함수는 라이브러리의 함수일 뿐이다.
malloc 라이브러리는 가상 주소 공간을 관리하지만, 라이브러리 자체는 시스템에게 더 많은 메모리를 요구하고 반환하는 시스템 콜(brk, sbrk)을 기반으로 구축된다.
brk 시스템 콜은 프로그램의 break 위치를 변경하는데 사용된다. break는 힙의 마지막 위치를 나타낸다. brk 는 새로운 break 주소를 나타내는 한 개의 인자를 받는데, 기존 break보다 작다면 힙의 크기를 줄이고 크다면 힙의 크기를 증가시킨다.
직접 사용하려고 한다면, 메모리 할당 라이브러리와 충돌이 발생할 수 있기 때문에 malloc()과 free()를 사용하자.
mmap() 함수를 통해 운영체제에게 메모리를 얻는 방법도 있다. 특정 파일과 연결되지 않고 스왑 영역(swap space)에 연결된 annnoymous 메모리 영역을 만든다. 이 메모리는 힙처럼 취급되고 관리된다.
calloc()메모리를 0으로 채워서 반환
realloc()이미 할당된 공간에 대해 추가 공간이 필요할 때 사용. 더 큰 새로운 영역을 확보하고 옛 영역의 정보를 복사 후 새 영역의 포인터를 리턴