[2026] C++ Stack Overflow | 스택 오버플로우 가이드
이 글의 핵심
C++ Stack Overflow: 스택 오버플로우 가이드. Stack Overflow란?·발생 원인.
Stack Overflow란?
스택 메모리 한계를 초과하여 발생하는 오류 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 무한 재귀
void func() {
func(); // 스택 오버플로우
}
// ❌ 큰 지역 변수
void func() {
int arr[1000000]; // 스택 부족
}
발생 원인
다음은 cpp를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 1. 무한 재귀
void infiniteRecursion() {
infiniteRecursion();
}
// 2. 깊은 재귀
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1); // n이 크면 위험
}
// 3. 큰 지역 변수
void largeArray() {
int arr[10000000]; // 스택에 너무 큼
}
// 4. 과도한 함수 호출
void a() { b(); }
void b() { c(); }
void c() { d(); }
// ....계속
실전 예시
예시 1: 재귀 제한
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <iostream>
// ❌ 무한 재귀
int badFactorial(int n) {
return n * badFactorial(n - 1); // 종료 조건 없음
}
// ✅ 종료 조건
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
// ✅ 반복문 사용
int factorialIterative(int n) {
int result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
}
int main() {
std::cout << factorial(10) << std::endl;
std::cout << factorialIterative(10) << std::endl;
}
예시 2: 큰 배열 처리
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <vector>
#include <memory>
// ❌ 스택에 큰 배열
void processLargeData() {
int data[1000000]; // 4MB (위험)
// 처리
}
// ✅ 힙 할당
void processLargeData() {
auto data = std::make_unique<int[]>(1000000);
// 처리
}
// ✅ 벡터 사용
void processLargeData() {
std::vector<int> data(1000000);
// 처리
}
예시 3: 꼬리 재귀 최적화
다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 일반 재귀 (스택 누적)
int sum(int n) {
if (n <= 0) return 0;
return n + sum(n - 1);
}
// ✅ 꼬리 재귀 (최적화 가능)
int sumTail(int n, int acc = 0) {
if (n <= 0) return acc;
return sumTail(n - 1, acc + n);
}
// ✅ 반복문
int sumIterative(int n) {
int result = 0;
for (int i = 1; i <= n; i++) {
result += i;
}
return result;
}
int main() {
std::cout << sumTail(10000) << std::endl;
std::cout << sumIterative(10000) << std::endl;
}
예시 4: 깊이 제한
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <iostream>
// 재귀 깊이 제한
const int MAX_DEPTH = 1000;
int fibonacci(int n, int depth = 0) {
if (depth > MAX_DEPTH) {
throw std::runtime_error("재귀 깊이 초과");
}
if (n <= 1) return n;
return fibonacci(n - 1, depth + 1) + fibonacci(n - 2, depth + 1);
}
// ✅ 메모이제이션
int fibonacciMemo(int n, std::vector<int>& memo) {
if (n <= 1) return n;
if (memo[n] != -1) return memo[n];
memo[n] = fibonacciMemo(n - 1, memo) + fibonacciMemo(n - 2, memo);
return memo[n];
}
int main() {
std::vector<int> memo(100, -1);
std::cout << fibonacciMemo(50, memo) << std::endl;
}
스택 크기 확인
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <iostream>
void checkStackSize() {
int dummy;
static int* stackStart = nullptr;
if (stackStart == nullptr) {
stackStart = &dummy;
}
size_t used = std::abs(stackStart - &dummy) * sizeof(int);
std::cout << "스택 사용: " << used << " bytes" << std::endl;
}
void recursiveCheck(int n) {
checkStackSize();
if (n > 0) {
recursiveCheck(n - 1);
}
}
자주 발생하는 문제
문제 1: 종료 조건 누락
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 종료 조건 없음
void countdown(int n) {
std::cout << n << std::endl;
countdown(n - 1); // 무한 재귀
}
// ✅ 종료 조건
void countdown(int n) {
if (n <= 0) return;
std::cout << n << std::endl;
countdown(n - 1);
}
문제 2: 상호 재귀
다음은 cpp를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 상호 재귀 (종료 조건 부족)
void funcA(int n);
void funcB(int n);
void funcA(int n) {
funcB(n);
}
void funcB(int n) {
funcA(n);
}
// ✅ 종료 조건 추가
void funcA(int n) {
if (n <= 0) return;
funcB(n - 1);
}
void funcB(int n) {
if (n <= 0) return;
funcA(n - 1);
}
문제 3: 구조체 배열
아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
struct LargeStruct {
int data[10000];
};
// ❌ 스택에 큰 구조체 배열
void func() {
LargeStruct arr[100]; // 4MB
}
// ✅ 동적 할당
void func() {
auto arr = std::make_unique<LargeStruct[]>(100);
}
문제 4: 가변 길이 배열
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ VLA (C99, 일부 컴파일러)
void func(int n) {
int arr[n]; // n이 크면 위험
}
// ✅ 벡터 사용
void func(int n) {
std::vector<int> arr(n);
}
디버깅 방법
다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# 1. 스택 크기 늘리기 (임시 해결)
# Linux
ulimit -s unlimited
# Windows (링커 옵션)
# /STACK:reserve[,commit]
# 2. 디버거 사용
gdb ./program
(gdb) run
# 스택 오버플로우 발생 시
(gdb) backtrace
# 3. AddressSanitizer
g++ -fsanitize=address -g program.cpp
방지 방법
다음은 cpp를 활용한 상세한 구현 코드입니다. 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 1. 재귀 -> 반복문
int sum(int n) {
int result = 0;
for (int i = 1; i <= n; i++) {
result += i;
}
return result;
}
// 2. 큰 데이터 -> 힙
void process() {
auto data = std::make_unique<int[]>(1000000);
}
// 3. 깊이 제한
int recursive(int n, int depth = 0) {
if (depth > MAX_DEPTH) {
throw std::runtime_error("깊이 초과");
}
// ...
}
// 4. 메모이제이션
std::unordered_map<int, int> cache;
int compute(int n) {
if (cache.count(n)) {
return cache[n];
}
// 계산
cache[n] = result;
return result;
}
스택 vs 힙
다음은 cpp를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 스택: 빠르지만 제한적
void stackAlloc() {
int arr[100]; // 빠름
}
// 힙: 느리지만 큰 메모리
void heapAlloc() {
auto arr = std::make_unique<int[]>(1000000); // 유연
}
// 선택 기준
// - 작은 데이터 (<1KB): 스택
// - 큰 데이터 (>1KB): 힙
// - 수명이 함수 범위: 스택
// - 수명이 긴 경우: 힙
스택 크기 제한을 알아두기
OS·링커 설정에 따라 기본 스택 크기가 크게 달라집니다. 대략적인 감각은 다음과 같습니다.
| 환경 | 기본 스택(대략) | 비고 |
|---|---|---|
| Linux 스레드(main) | ulimit -s (종종 8MiB) | pthread 생성 시 pthread_attr_setstacksize로 조정 |
| Windows 스레드 | 링커 /STACK (기본 1MiB 등) | 실행 파일·스레드 생성 옵션 확인 |
| macOS | ulimit -s | 필요 시 늘리되, 근본 해결은 알고리즘·할당 방식 |
| 중요: 스택을 무제한으로 키우는 것은 임시 방편입니다. 재귀 깊이나 대형 프레임 문제는 설계를 바꿔 스택 사용량을 줄이는 것이 맞습니다. |
재귀 깊이 문제
같은 알고리즘이라도 입력 크기에 따라 호출 깊이가 선형·지수로 늘어납니다. 피보나치의 순수 재귀는 깊이도 크고 호출 수도 폭발하므로, 반복문·메모이제이션·행렬 거듭제곱 등으로 바꾸는 편이 낫습니다.
깊이가 고정되어 있어도(예: 트리 DFS) 최악 깊이가 스택 한계에 근접하면 명시적 스택(std::vector로 노드 스택)으로 바꾸는 것이 안전합니다.
큰 지역 변수와 alloca
VLA(가변 길이 배열)는 C++ 표준이 아니며, 일부 확장으로 허용되더라도 n이 크면 한 번에 스택을 크게 씁니다. alloca도 마찬가지로 스택 사용량이 호출마다 달라져 예측이 어렵습니다. 크기가 런타임에 커질 수 있으면 vector나 unique_ptr<T[]>로 힙에 두세요.
해결 방법 요약
- 힙 할당:
std::vector,std::unique_ptr<T[]>— 크고 가변적인 버퍼에 적합. - 반복문으로 전환: 꼬리 재귀 형태로 바꿀 수 있으면 스택 깊이를 없앨 수 있습니다.
- 꼬리 재귀: C++ 표준은 꼬리 호출 최적화(TCO)를 보장하지 않습니다. TCO에 의존하기보다 반복문으로 바꾼 버전을 유지보수 기준으로 삼는 것이 안전합니다.
- 재귀 깊이 상한 + 에러: 신뢰할 수 없는 입력에서는 깊이 카운터로 조기 실패.
- 스택 크기 조정:
ulimit -s(Linux), MSVC/STACK(링커) 등은 배포 문서에 명시하고, 근본 원인과는 분리해 다룹니다.
디버깅 (스택 오버플로 의심 시)
- 증상:
SIGSEGV가 스택 끝 근처에서 나거나, 재귀 직후 크래시. - gdb / lldb: 크래시 시
bt가 매우 깊거나 같은 함수가 반복되면 재귀 의심.frame이동하며 지역 변수 크기를 추정합니다. - AddressSanitizer: 일부 환경에서 스택 오버플로를 보고하기도 하지만, 보장 도구는 아닙니다. 플랫폼별 가드 페이지 동작에 의존합니다.
- 컴파일러 경고: 큰 지역 배열에 대한 경고(
-Wstack-usage=등, GCC)를 켜 두면 예방에 도움이 됩니다. 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# Linux: 현재 스택 한계 확인
ulimit -s
# 깊은 재귀 재현 시 (임시)
ulimit -s unlimited
./repro
FAQ
Q1: Stack Overflow는 언제?
A:
- 무한 재귀
- 깊은 재귀
- 큰 지역 변수
Q2: 탐지 방법은?
A:
- 디버거 backtrace
- AddressSanitizer
- 스택 크기 모니터링
Q3: 방지 방법은?
A:
- 재귀 -> 반복문
- 힙 할당
- 깊이 제한
Q4: 스택 크기는?
A:
- Linux: 보통 8MB
- Windows: 보통 1MB
- 변경 가능 (ulimit, 링커)
Q5: 재귀 vs 반복문?
A:
- 재귀: 간결, 스택 사용
- 반복문: 빠름, 안전
Q6: Stack Overflow 학습 리소스는?
A:
- “C++ Primer”
- “Effective C++”
- GDB 문서
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ Heap Corruption | “힙 손상” 가이드
- C++ Stack Allocator | “스택 할당자” 가이드
- C++ Use After Free | “해제 후 사용” 가이드