C++ 프로그램의 비정상적인 동작은 종종 프로그램 전체의 충돌(crash)로 이어집니다. 개발을 하다 보면 Segmentation fault, Aborted, Floating point exception 같은 오류 메시지를 한 번쯤은 마주하게 됩니다. 이 글에서는 실제 예제 코드와 함께 C++ 프로그램이 충돌하는 대표적인 원인들을 하나씩 살펴보고, 각 상황에 대한 예방 방법까지 정리해 드립니다.
1. 처리되지 않은 예외(Exception)
C++에서 예외(exception)란 프로그램이 비정상적인 상황을 만났을 때 나타나는 반응을 의미합니다. 예외를 try-catch 블록으로 적절히 처리하지 않으면 프로그램은 그대로 비정상 종료됩니다. 아래 예제는 분모가 0인 나눗셈 연산으로 인해 충돌하는 경우입니다.
예제 코드
#include <iostream>
int main(){
int num1=10;
int num2=0;
int quotient=num1/num2;
printf("\n Quotient is: %d",quotient);
return 0;
}
실행 결과
Floating point exception (core dumped)
예방 방법: 나누기 연산을 수행하기 전에 분모가 0인지 반드시 검사하고, 필요하다면 try-catch 구문으로 예외 상황을 안전하게 처리해야 합니다.
2. 버퍼 오버플로우(Buffer Overflow)
버퍼(buffer)는 데이터를 임시로 저장하는 공간입니다. 프로그램이 버퍼에 데이터를 기록할 때 해당 버퍼가 담을 수 있는 크기를 초과하면, 초과된 데이터는 버퍼의 경계를 넘어 인접한 메모리 영역을 덮어쓰게 됩니다. 아래 예제에서는 입력 데이터의 크기가 변수 num 근처의 배열 c가 감당할 수 있는 크기를 초과하면서 프로그램 동작이 비정상적으로 변합니다.
예제 코드
#include <iostream>
#include <string.h>
int main(){
int num=100;
std::cout<<"\nValue for num:"<<num;
char c[2];
strcpy(c,"abcdefghijklmnopqrstuvwxyz");
std::cout<<"\nValue for c:"<<c;
return 0;
}
실행 결과
Value for num:100 Segmentation fault (core dumped)
예방 방법: 고정 크기의 문자 배열 대신 자동으로 크기가 조절되는 std::string을 사용하고, C 스타일 함수(strcpy 등) 대신 strncpy처럼 경계를 검사하는 함수를 사용하는 것이 안전합니다.
3. 스택 오버플로우(Stack Overflow)
스택 오버플로우는 호출 스택(call stack) 포인터가 스택의 경계를 초과할 때 발생합니다. 스택은 크기가 제한된 메모리 공간이므로, 프로그램이 사용 가능한 공간보다 더 많은 메모리를 요구하면 스택이 넘쳐서 프로그램이 충돌합니다. 가장 흔한 원인은 바로 무한 재귀 호출입니다.
아래 프로그램은 factorial() 함수를 무한히 호출합니다. 재귀 탈출 조건이 잘못 작성되어 함수가 끝없이 자기 자신을 부르게 되는 것입니다.
예제 코드
#include <iostream>
#include <string.h>
int factorial(int num){
if(num==0)
return 1;
else
return(factorial(num));
}
int main(){
int n=10;
int fact=factorial(n);
std::cout<<fact;
}
실행 결과
Segmentation fault (core dumped)
예방 방법: 재귀 함수에는 반드시 올바른 종료 조건(base case)을 두고, 매 호출마다 문제의 크기가 줄어들도록 인자를 변경해야 합니다. 위 코드라면 return factorial(num - 1) * num;처럼 수정해야 합니다.
4. 세그먼테이션 폴트(Segmentation Fault)
세그먼테이션 폴트 또는 코어 덤프(core dump)는 프로그램이 자신에게 허용되지 않은 메모리 영역에 접근하려고 할 때 발생합니다. 아래 예제에서는 포인터 str이 무한히 증가하면서 프로그램이 소유하지 않은 메모리 주소에 계속 값을 쓰게 됩니다.
예제 코드
#include <iostream>
int main(){
char *str;
char name[]="iostream";
str=name;
while(1)
(*str++)='a';
}
실행 결과
Segmentation fault (core dumped)
예방 방법: 포인터 연산 시 배열의 유효 범위를 항상 확인하고, 반복문에는 명확한 종료 조건을 설정해야 합니다. 가능하다면 std::string이나 STL 컨테이너처럼 범위를 자동으로 관리해 주는 도구를 활용하는 것이 좋습니다.
5. 메모리 누수(Memory Leak)
메모리 누수는 동적으로 할당한 메모리를 끝내 해제(deallocate)하지 않을 때 발생합니다. 더 이상 사용하지 않는 메모리는 반드시 반환해야 하며, 그렇지 않으면 메모리 누수가 누적되어 시스템 전체의 가용 메모리가 고갈되고 결국 프로그램이 충돌하게 됩니다. 아래와 같이 free() 호출 없이 반복적으로 메모리를 할당하는 코드가 대표적인 메모리 누수의 원인입니다.
예제 코드
#include <iostream>
int main(){
int *node;
node = (int *) malloc(9999999);
// free(node); // 해제 코드가 주석 처리되어 있음
}
예방 방법: malloc/free, new/delete는 항상 짝을 이루도록 작성하고, C++11 이후 환경이라면 std::unique_ptr, std::shared_ptr 같은 스마트 포인터(smart pointer)를 사용해 메모리를 자동으로 관리하는 것이 가장 안전합니다.
마무리
C++는 강력한 성능만큼이나 메모리를 직접 다루는 위험도 함께 안고 있는 언어입니다. 예외 처리, 버퍼 경계 검사, 올바른 재귀 설계, 포인터 범위 확인, 스마트 포인터 활용이라는 다섯 가지 수칙만 지켜도 대부분의 프로그램 충돌을 사전에 예방할 수 있습니다. 디버거와 정적 분석 도구를 함께 활용하면 이러한 문제를 더욱 빠르게 발견하고 수정할 수 있습니다.