힙과 스택
내가 면접 때 많이 들어봤던 질문 중의 하나였다.
“힙과 스택에 대하여 설명해보세요.”
C/C++을 공부하면서 당연히 배우는 과정 중의 하나인데 문제는 이런 내용은 C 처음 배울 때 나온 내용이라 기억의 저편으로 멀리 가버렸다는 점. 기억을 되살리기 위해 다시 정리해본다.
2026년 수정 안내 처음 이 글을 쓸 때 검색으로 모은 내용 중 몇 가지가 틀려 있었다. 특히 “변수명은 스택에, 값은 힙에 저장된다”는 서술은 Java의 참조 의미론을 C/C++에 잘못 옮긴 것이었다. 틀린 항목을 바로잡고 근거를 붙여 다시 정리한다.
스택의 특징은…
- 컴파일러가 자동으로 할당/해제하는 메모리 영역이다.
- 자동변수(지역변수)와 함수 파라미터가 생성되고, 스코프가 끝나면 사라지는 영역.
- 함수 호출 시 복귀 주소(return address) 와 이전 스택 프레임 정보가 저장된다. 함수의 반환 값은 보통 레지스터(x86-64라면 RAX/XMM0)로 전달되며, 크기가 큰 객체일 때만 호출자가 마련한 메모리에 쓰인다. 복귀 주소와 반환 값은 다른 것이다.
- LIFO 구조를 가진다. 가장 나중에 들어간 자료가 가장 먼저 나온다.
- 윈도우 프로그램의 경우 기본 스택 예약 크기는 1MByte이다. (링커의
/STACK옵션으로 변경 가능. 리눅스는 보통 8MByte이며ulimit -s로 확인한다.) - 스레드가 여러 개라면 각 스레드마다 고유한 스택 영역을 가진다.
- 하나의 함수 호출에 대응하는 스택 메모리의 묶음을 스택 프레임이라고 부른다.
스택에 대해 흔히 잘못 알려진 것들
-
“함수 안에서 선언된 변수는 모두 스택으로 들어간다” → 아니다.
static지역변수는 데이터 영역에,thread_local변수는 TLS(스레드 로컬 저장소)에 놓인다. 스택에 놓이는 것은 자동 저장 기간(automatic storage duration)을 가진 변수뿐이다.void f() { int a = 0; // 스택 static int b = 0; // 데이터 영역 (호출이 끝나도 유지) thread_local int c; // TLS } -
“모든 포인터도 스택에 올라간다” → 아니다. 포인터도 하나의 변수일 뿐이라, 그 변수가 어디에 선언되었느냐에 따라 위치가 정해진다. 힙에 할당된 객체의 포인터 멤버는 힙에, 전역 포인터는 데이터 영역에 있다.
-
“스택 크기는 컴파일 시점에 전부 정해져서 실행 중에 바꿀 수 없다” → 정확하지 않다. 스택의 예약 크기는 링크 시점에 정해지지만, 프레임 안에서 얼마를 쓸지는
alloca나 가변 길이 배열(VLA)처럼 실행 중에 결정되는 경우가 있다. 스레드 스택 크기도CreateThread/pthread_attr_setstacksize로 실행 중에 지정한다. -
“스택을 넘어가면 페이지 폴트 인터럽트, 즉 스택 오버플로우가 발생한다” → 둘은 다른 것이다. 스택이 아래로 자라며 새 페이지를 건드릴 때 발생하는 페이지 폴트는 정상 동작이고, OS가 페이지를 커밋해준다. 예약된 영역 끝의 가드 페이지까지 침범해 더 이상 확장할 수 없을 때 비로소 스택 오버플로우(윈도우라면
EXCEPTION_STACK_OVERFLOW)가 된다. -
“변수명은 스택에, 변수의 내용은 힙에 저장된다” → 완전히 틀렸다. 변수 이름은 컴파일 시점의 심볼일 뿐 실행 중에는 존재하지 않는다. 지역
int x = 10;이라면 값 10은 스택 프레임 안에 그대로 들어간다. 힙이 관여하는 것은new/malloc으로 직접 할당했을 때뿐이다.
힙의 특징은…
- 프로그래머가 직접 할당하고 해제하는 메모리 영역이다.
- 프로그램이 동적으로 할당받아 사용하는 영역. C에서는
malloc/free, C++에서는new/delete를 쓴다. (C++에서는 직접delete를 부르는 대신std::unique_ptr,std::shared_ptr같은 스마트 포인터에 맡기는 것이 현대적인 방법이다.) - 할당 크기와 수명이 실행 중에 결정된다. 사용자가 얼마나 많은 데이터를 입력할지 예측할 수 없을 때 쓴다.
- 힙은 프로세스 전체가 공유한다. 여러 스레드가 같은 힙을 쓰므로 할당자 내부에 동기화가 들어가고, 이것이 멀티스레드 환경에서 병목이 되기도 한다. (그래서 tcmalloc, jemalloc 같은 스레드 캐시 기반 할당자를 쓴다.)
- 힙이 부족하면 프로그램이 곧바로 종료되는 것이 아니라,
malloc은NULL을 반환하고new는std::bad_alloc예외를 던진다. (new(std::nothrow)를 쓰면nullptr을 반환한다.) 이를 처리하지 않으면 그때 비로소 종료된다.
이외에도 데이터 영역과 텍스트 영역(코드 영역)이 있다.
데이터 영역의 특징은…
- 프로그램이 시작될 때 잡히고 종료될 때까지 유지되는 공간이다.
- 전역 변수,
static변수 등이 저장된다. - 초기값이 있는 변수는
.data에, 0으로 초기화되는 변수는.bss에 나뉘어 들어간다..bss는 실행 파일에 실제 데이터를 담지 않고 크기만 기록하므로 파일 크기를 줄여준다. - 프로세스당 하나의 영역이 할당된다.
텍스트 영역(코드 영역)의 특징은…
- 프로그램의 실행 코드를 저장한다.
- 보통 읽기 전용으로 매핑되며, 같은 실행 파일을 여러 프로세스가 띄우면 이 영역은 공유된다.
- 문자열 리터럴도 대개 이 근처의 읽기 전용 영역(
.rodata)에 놓인다.char* p = "abc"; p[0] = 'x';가 죽는 이유가 이것이다.
한 줄로 정리하면
스택과 힙의 본질적인 차이는 “누가 수명을 관리하는가”이다. 스택은 컴파일러가 스코프에 맞춰 자동으로, 힙은 프로그래머가 명시적으로 관리한다. 그래서 스택은 빠르고 안전하지만 크기와 수명에 제약이 있고, 힙은 자유로운 대신 누수와 단편화의 책임이 프로그래머에게 있다.
댓글 남기기