C++ 스택 오버플로우 에러 | "Stack Overflow" 크래시 원인과 해결
이 글의 핵심
C++ 스택 오버플로우 에러에 대한 실전 가이드입니다.
들어가며: “재귀 함수를 호출했더니 프로그램이 크래시…"
"Segmentation Fault가 나는데 포인터는 안 썼어요”
스택 오버플로우(Stack Overflow)는 스택 메모리가 부족해서 발생하는 크래시입니다. 주로 무한 재귀, 큰 지역 변수, 깊은 재귀 호출이 원인입니다.
// ❌ 무한 재귀
void foo() {
foo(); // 종료 조건 없음 → 스택 오버플로우
}
int main() {
foo(); // 크래시
}
// Segmentation Fault (Linux)
// Stack Overflow (Windows)
이 글에서 다루는 것:
- 스택 오버플로우의 4가지 주요 원인
- 재귀 깊이 제한
- 스택 크기 조정 방법
- 힙 할당으로 전환
- 꼬리 재귀 최적화
목차
1. 스택 오버플로우란?
스택 메모리
스택은 함수 호출 시 지역 변수와 반환 주소를 저장하는 메모리 영역입니다.
void foo() {
int x = 42; // 스택에 저장
bar();
} // x 자동 해제
void bar() {
double y = 3.14; // 스택에 저장
} // y 자동 해제
스택 크기 제한:
- Linux: 기본 8MB
- Windows: 기본 1MB
- macOS: 기본 8MB
스택 오버플로우 발생
스택 메모리:
[함수1 지역 변수]
[함수2 지역 변수]
[함수3 지역 변수]
...
[함수10000 지역 변수] ← 스택 한계 초과 → 크래시!
2. 4가지 주요 원인
원인 1: 무한 재귀
// ❌ 종료 조건 없음
int factorial(int n) {
return n * factorial(n - 1); // 무한 재귀
}
int main() {
factorial(5); // 크래시
}
해결:
// ✅ 종료 조건 추가
int factorial(int n) {
if (n <= 1) return 1; // 종료 조건
return n * factorial(n - 1);
}
원인 2: 큰 지역 변수
// ❌ 스택에 큰 배열
void process() {
int bigArray[1000000]; // 4MB → 스택 한계 초과
// ...
}
int main() {
process(); // 크래시
}
해결:
// ✅ 힙 할당
void process() {
std::vector<int> bigArray(1000000); // 힙에 할당
// ...
}
// 또는
void process() {
auto bigArray = std::make_unique<int[]>(1000000);
// ...
}
원인 3: 깊은 재귀
// ❌ 재귀 깊이 제한 없음
int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
int main() {
fibonacci(50); // 깊이 매우 깊음 → 크래시 (또는 매우 느림)
}
해결:
// ✅ 반복문으로 전환
int fibonacci(int n) {
if (n <= 1) return n;
int prev = 0, curr = 1;
for (int i = 2; i <= n; ++i) {
int next = prev + curr;
prev = curr;
curr = next;
}
return curr;
}
// ✅ 또는 메모이제이션
std::unordered_map<int, int> memo;
int fibonacci(int n) {
if (n <= 1) return n;
auto it = memo.find(n);
if (it != memo.end()) return it->second;
int result = fibonacci(n - 1) + fibonacci(n - 2);
memo[n] = result;
return result;
}
원인 4: 중첩 함수 호출
// ❌ 깊은 호출 체인
void a() { b(); }
void b() { c(); }
void c() { d(); }
// ... (1000개 함수)
void z() {
int bigArray[10000]; // 각 함수마다 큰 지역 변수
}
int main() {
a(); // 크래시
}
해결: 지역 변수를 힙으로 이동.
3. 재귀 깊이 제한
재귀 깊이 카운터
// ✅ 깊이 제한
int factorial(int n, int depth = 0) {
const int MAX_DEPTH = 1000;
if (depth > MAX_DEPTH) {
throw std::runtime_error("Recursion too deep");
}
if (n <= 1) return 1;
return n * factorial(n - 1, depth + 1);
}
반복문으로 전환
// 재귀 버전
int sum(int n) {
if (n <= 0) return 0;
return n + sum(n - 1);
}
// ✅ 반복문 버전
int sum(int n) {
int result = 0;
for (int i = 1; i <= n; ++i) {
result += i;
}
return result;
}
4. 스택 크기 조정
Linux: ulimit
# 현재 스택 크기 확인 (KB)
ulimit -s
# 스택 크기 늘리기 (16MB)
ulimit -s 16384
# 무제한 (비권장)
ulimit -s unlimited
Windows: 링커 옵션
Visual Studio:
프로젝트 속성 → 링커 → 시스템 → 스택 예약 크기
기본: 1MB → 16MB로 변경
# 또는 링커 플래그
cl /F16777216 main.cpp # 16MB 스택
CMake
# Linux
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-z,stack-size=16777216")
# Windows
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /STACK:16777216")
5. 힙 할당으로 전환
큰 배열을 힙으로
// ❌ 스택 (4MB)
void process() {
int bigArray[1000000];
// ...
}
// ✅ 힙 (vector)
void process() {
std::vector<int> bigArray(1000000);
// ...
}
// ✅ 힙 (unique_ptr)
void process() {
auto bigArray = std::make_unique<int[]>(1000000);
// ...
}
재귀를 명시적 스택으로
// ❌ 재귀 (스택 오버플로우 가능)
void traverse(TreeNode* node) {
if (!node) return;
process(node);
traverse(node->left);
traverse(node->right);
}
// ✅ 명시적 스택 (힙 사용)
void traverse(TreeNode* node) {
std::stack<TreeNode*> stack;
stack.push(node);
while (!stack.empty()) {
TreeNode* current = stack.top();
stack.pop();
if (!current) continue;
process(current);
stack.push(current->right);
stack.push(current->left);
}
}
6. 꼬리 재귀 최적화
꼬리 재귀란?
꼬리 재귀는 재귀 호출이 함수의 마지막 연산인 경우입니다. 컴파일러가 반복문으로 최적화해 스택을 사용하지 않습니다.
// ❌ 꼬리 재귀 아님 (재귀 호출 후 곱셈)
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1); // 재귀 호출 후 n을 곱함
}
// ✅ 꼬리 재귀 (재귀 호출이 마지막)
int factorialTail(int n, int acc = 1) {
if (n <= 1) return acc;
return factorialTail(n - 1, n * acc); // 재귀 호출이 마지막
}
컴파일러 최적화 확인
# -O2 이상에서 꼬리 재귀 최적화
g++ -O2 -S main.cpp
# 어셈블리 확인 (jmp로 변환되면 최적화됨)
cat main.s | grep -A 10 factorialTail
실전 사례 분석
사례 1: JSON 파싱 스택 오버플로우
증상: 깊게 중첩된 JSON 파싱 시 크래시.
// ❌ 재귀 파싱
Json parse(const std::string& str, size_t& pos) {
if (str[pos] == '{') {
Json obj;
// ... 재귀적으로 파싱
return obj;
}
// ...
}
// 중첩 깊이 10000 → 크래시
해결:
// ✅ 명시적 스택
Json parse(const std::string& str) {
std::stack<Json> stack;
// ... 반복문으로 파싱
return stack.top();
}
사례 2: 디렉토리 순회
증상: 깊은 디렉토리 구조에서 크래시.
// ❌ 재귀 순회
void traverseDir(const std::filesystem::path& dir) {
for (const auto& entry : std::filesystem::directory_iterator(dir)) {
if (entry.is_directory()) {
traverseDir(entry.path()); // 재귀
} else {
processFile(entry.path());
}
}
}
해결:
// ✅ 명시적 스택
void traverseDir(const std::filesystem::path& root) {
std::stack<std::filesystem::path> stack;
stack.push(root);
while (!stack.empty()) {
auto dir = stack.top();
stack.pop();
for (const auto& entry : std::filesystem::directory_iterator(dir)) {
if (entry.is_directory()) {
stack.push(entry.path());
} else {
processFile(entry.path());
}
}
}
}
정리
스택 오버플로우 원인 체크리스트
- 무한 재귀가 있는가? (종료 조건 확인)
- 큰 지역 변수가 있는가? (> 1MB)
- 재귀 깊이가 깊은가? (> 1000)
- 중첩 함수 호출이 많은가?
해결 방법 우선순위
- 종료 조건 추가 (무한 재귀 방지)
- 큰 변수를 힙으로 (vector, unique_ptr)
- 반복문으로 전환 (재귀 → 명시적 스택)
- 꼬리 재귀 최적화 (컴파일러 의존)
- 스택 크기 조정 (임시 방편)
핵심 규칙
- 큰 배열은 힙에 할당 (vector, unique_ptr)
- 재귀 깊이를 제한 (깊이 카운터)
- 깊은 재귀는 반복문으로 (명시적 스택)
- 꼬리 재귀를 활용 (-O2 이상)
- 스택 크기 조정은 최후 수단
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 메모리 기초 | 스택·힙·정적 메모리
- C++ 재귀 함수 | 재귀 vs 반복문 선택 가이드
- C++ 꼬리 재귀 최적화 | Tail Call Optimization
- C++ Segmentation Fault | 메모리 에러 디버깅
마치며
스택 오버플로우는 재귀 깊이와 지역 변수 크기를 관리하면 방지할 수 있습니다.
핵심 원칙:
- 큰 배열은 힙에 (vector, unique_ptr)
- 재귀 깊이 제한 (깊이 카운터)
- 깊은 재귀는 반복문으로
- 꼬리 재귀 활용 (-O2 이상)
재귀는 코드가 간결하지만, 스택 오버플로우 위험이 있습니다. 깊이가 예측 불가능하면 반복문 + 명시적 스택을 사용하세요.
다음 단계: 스택 오버플로우를 방지했다면, C++ 재귀 최적화 가이드에서 더 효율적인 재귀 코드를 작성해 보세요.
관련 글
- C++ Segmentation Fault |