C++ 스택 오버플로우 에러 | "Stack Overflow" 크래시 원인과 해결

C++ 스택 오버플로우 에러 | "Stack Overflow" 크래시 원인과 해결

이 글의 핵심

C++ 스택 오버플로우 에러에 대한 실전 가이드입니다.

들어가며: “재귀 함수를 호출했더니 프로그램이 크래시…"

"Segmentation Fault가 나는데 포인터는 안 썼어요”

스택 오버플로우(Stack Overflow)는 스택 메모리가 부족해서 발생하는 크래시입니다. 주로 무한 재귀, 큰 지역 변수, 깊은 재귀 호출이 원인입니다.

// ❌ 무한 재귀
void foo() {
    foo();  // 종료 조건 없음 → 스택 오버플로우
}

int main() {
    foo();  // 크래시
}

// Segmentation Fault (Linux)
// Stack Overflow (Windows)

이 글에서 다루는 것:

  • 스택 오버플로우의 4가지 주요 원인
  • 재귀 깊이 제한
  • 스택 크기 조정 방법
  • 힙 할당으로 전환
  • 꼬리 재귀 최적화

목차

  1. 스택 오버플로우란?
  2. 4가지 주요 원인
  3. 재귀 깊이 제한
  4. 스택 크기 조정
  5. 힙 할당으로 전환
  6. 꼬리 재귀 최적화
  7. 정리

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)
  • 중첩 함수 호출이 많은가?

해결 방법 우선순위

  1. 종료 조건 추가 (무한 재귀 방지)
  2. 큰 변수를 힙으로 (vector, unique_ptr)
  3. 반복문으로 전환 (재귀 → 명시적 스택)
  4. 꼬리 재귀 최적화 (컴파일러 의존)
  5. 스택 크기 조정 (임시 방편)

핵심 규칙

  1. 큰 배열은 힙에 할당 (vector, unique_ptr)
  2. 재귀 깊이를 제한 (깊이 카운터)
  3. 깊은 재귀는 반복문으로 (명시적 스택)
  4. 꼬리 재귀를 활용 (-O2 이상)
  5. 스택 크기 조정은 최후 수단

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ 메모리 기초 | 스택·힙·정적 메모리
  • C++ 재귀 함수 | 재귀 vs 반복문 선택 가이드
  • C++ 꼬리 재귀 최적화 | Tail Call Optimization
  • C++ Segmentation Fault | 메모리 에러 디버깅

마치며

스택 오버플로우재귀 깊이지역 변수 크기를 관리하면 방지할 수 있습니다.

핵심 원칙:

  1. 큰 배열은 힙에 (vector, unique_ptr)
  2. 재귀 깊이 제한 (깊이 카운터)
  3. 깊은 재귀는 반복문으로
  4. 꼬리 재귀 활용 (-O2 이상)

재귀는 코드가 간결하지만, 스택 오버플로우 위험이 있습니다. 깊이가 예측 불가능하면 반복문 + 명시적 스택을 사용하세요.

다음 단계: 스택 오버플로우를 방지했다면, C++ 재귀 최적화 가이드에서 더 효율적인 재귀 코드를 작성해 보세요.


관련 글

  • C++ Segmentation Fault |