개요

C++에서 말하는 예외의 정의는 다음과 같다.

프로그램 실행 도중에 발생하는 문제상황
프로그램의 논리에 맞기 않는 상황

따라서 문법적인 에러는 예외의 범주에 포함되지 않는다.

C에서는 보통 if else문을 사용해 분기를 구성함으로서 이러한 예외들을 처리한다. 그러나 이러한 방식은 예외처리를 위한 코드와 프로그램의 흐름을 구성하는 코드를 쉽게 구분하지 못한다는 문제가 있다. C++은 이러한 문제를 해결하기 위해 별도의 예외 처리 매커니즘을 보유하고 있다.


try, catch, throw

C++에서 예외 처리를 위한 키워드는 아래의 3가지가 있다.

키워드기능
try예외를 발견한다.
catch예외를 잡는다.
throw예외를 던진다.
  • try 블록

try 블록은 예외 발생에 대한 검사의 범위를 지정할 때 사용된다. 즉, try 블록 내에서 예외가 발생하면 이는 C++의 예외처리 매커니즘에 의해서 처리가 된다.

 
try
{
	// 예외발생 예외지역
}
 
  • catch 블록

catch 블록은 try 블록에서 발생한 예외를 처리하는 코드가 담기는 영역으로서, 그 형태가 마치 반환형 없는 함수와 유사하다.

 
catch(/*처리할 예외의 종류 명시*/)
{
	// 예외처리 코드의 삽입
}
 
  • 사실 try와 catch는 하나의 단위이다.
    • 따라서 항상 이어서 등장해야 하며, 중간에 다른 문장이 오면 안된다.

종합적으로, try 블록 내에서 발생하는 예외는 이어서 등장하는 catch 블록에 의해 처리된다.

  • throw

키워드 throw는 예외가 발생했음을 알리는 문장의 구성에 사용된다.

 
throw expn;
 

최종적으로 try, catch, throw는 다음과 같이 동작한다.

  1. throw에 의해서 ‘예외 데이터’가 던져진다.
  2. ‘예외 데이터’를 감싸는 try 블록에 의해 감지된다.
  3. 이어서 등장하는 catch 블록이 예외를 처리한다.

구체적인 동작은 아래의 예제를 참고하자.

 
/*HandlingExceptionTryCatch.cpp*/
 
#include <iostream>
using namespace std;
 
int main(void)
{
	int num1, num2;
	cout<<"두 개의 숫자 입력: ";
	cin>>num1>>num2;
	
	try //try 블록으로 에러 발생이 예상되는 영역을 묶는다.
	{
		if (num2 == 0) //에러의 조건
			throw num2; //에러 데이터를 던진다
		//만약 에러가 발생하면 발생 지점 이후의 try 블록은 건너뛴다.
		cout<<"나눗셈의 몫: "<<(num1 / num2)<<endl;
		cout<<"나눗셈의 나머지: "<<(num1 % num2)<<endl;
	}
	catch(int expn) //에러 데이터를 받아 블록 내에서 적절한 예외 처리를 한다.
	{
		cout<<"제수는 "<<expn<<"이 될 수 없습니다."<<endl;
		cout<<"프로그램을 다시 실행하세요."<<endl;
	}
	cout<<"end of main"<<endl;
	
	return (0);
}
 
  • 팁 : try 블록 내에서, 에러 발생 시점 이후의 문장들은 건너뛴다는 특성을 잘 활용하자. 에러와 직접적으로 관련이 없더라도 에러 발생 시 실행되면 안되는 문장들은 try 블록 안으로 집어넣는 것이 바람직하다.

스택 풀기(Stack Unwinding)

try 블록 내에서 직접적으로 throw가 실행되지 않고, try 내부에서 호출한 함수 내부에서 throw가 실행되는 경우가 있다.

 
/*PassException.cpp*/
 
#include <iostream>
using namespace std;
 
void Divide(int num1, int num2)
{
	if (num2 == 0)
		throw num2; //throw는 try, catch 블록이 없다. 이러한 경우에는 함수를 호출한 곳으로 '예외 데이터'를 던진다.
	cout<<"나눗셈의 몫 : "<< num1 / num2 <<endl;
	cout<<"나눗셈의 나머지 : "<< num % num2 <<endl;
}
 
int main(void)
{
	int num1, num2;
	cout<<"두 개의 숫자 입력 : ";
	cin>>num1>>num2;
	
	try //try 블록 내에서 throw가 직접적으로 호출되지 않고, Divide 내에서 호출된다.
	{
		Divide(num1, num2);
		cout<<"나눗셈을 마쳤습니다."<<endl;
	}
	catch(int expn)
	{
		cout<<"제수는 "<<expn<<"이 될 수 없습니다."<<endl;
		cout<<"프로그램을 다시 실행하세요."<<endl;
	}
	return (0);
}
 

위 예제에서 다음의 특성을 알 수 있다.

예외가 처리되지 않으면, 예외가 발생한 함수를 호출한 영역으로 예외 데이터가(더불어 예외처리에 대한 책임까지) 전달된다.
함수 내에서 함수를 호출한 영역으로 예외 데이터를 전달하면, 해당 함수는 더 이상 실행되지 않고 종료가 된다.

다음과 같이 여러 개의 함수를 걸쳐 ‘예외 데이터’가 전달되는 케이스를 생각하여보자.

 
/*StackUnwinding.cpp*/
 
#include <iostream>
using namespace std;
 
void SimpleFuncOne(void);
void SimpleFuncTwo(void);
void SimpleFuncThree(void);
 
int main(void)
{
	try
	{
		SimpleFuncOne();
	}
	catch (int expn)
	{
		cout<<"예외코드: "<<expn<<endl;
	}
	return (0);
}
 
void SimpleFuncOne(void)
{
	cout<<"SimpleFuncOne(void)"<<endl;
	SimpleFuncTwo();
}
void SimpleFuncTwo(void)
{
	cout<<"SimpleFuncTwo(void)"<<endl;
	SimpleFuncThree();
}
void SimpleFuncThree(void)
{
	cout<<"SimpleFuncThree(void)"<<endl;
	throw -1;
}
 

만약 예외가 발생할 경우, try catch 문을 만날 때까지 호출된 함수의 역순으로 예외 데이터가 전달된다. 예외 데이터를 전달한 함수는 종료되기 때문에, 호출된 함수의 역순으로 함수의 스택은 반환된다.
이를 **스택 풀기(스택의 반환)**이라고 명명한다.

만약 예외가 처리되지 않아서, 예외 데이터가 main 함수에까지 도달했는데, main 함수에서조차 예외를 처리하지 않으면, terminate 함수(프로그램을 종료시키는 함수)가 호출되면서 프로그램이 종료되어 버린다.

만약 throw로 던진 ‘예외 데이터’의 자료형과 catch 블록 인자의 자료형이 일치하지 않으면, catch 블록으로 예외가 전달되지 않는다. 예외 데이터를 던질 수 있는 다른 영역이 있으면 그 곳으로 예외를 던지고, 그렇지 않으면 terminate 함수로 인해 종료된다.


throw 키워드로 던질 수 있는 ‘예외 데이터’의 자료형은 복수일 수 있기 때문에, 다음과 같이 catch 문도 여러 개 존재할 수 있다.

 
#include <iostream>
using namespace std;
 
int main()
{
	char c;
 
	cout<<"값 입력 : ";
	cin>>c;
 
	try
	{
		if (c == "1")
			throw 1; //int 형을 던짐
		else if(c == "a")
			throw "a"; //char 형을 덤짐
	}
	catch (int expn)
	{
		cout<<"입력값은 int type"<<endl;
	}
	catch (char expn)
	{
		cout<<"입력값은 char type"<<endl;
	}
	cout<<endl<<"end of main"<<endl;
}
 

전달되는 예외의 명시

함수 내에서 발생할 수 있는 예외의 종류도 함수의 특징으로 간주된다. 따라서 이미 정의된 특정 함수의 호출을 위해서는 함수의 이름, 매개변수 선언, 반환형 정보에 더해서, 함수 내에서 전달될 수 있는 예외의 종류(예외 데이터의 자료형)과 그 상황도 알아야 한다. 따라서 함수를 정의할 때에는 함수 내에서 발생 가능한 예외의 종류를 다음과 같이 명시해 주는 것이 좋다.

 
int ThrowFunc(int num) throw (int, char)
{
	// . . . .
}
 

함수에 사전에 명시한 자료형 이외의 ‘예외 데이터’를 함수가 반환하면, 역시 terminate(unexpected) 함수의 호출로 인해서 프로그램은 종료된다.


클래스의 객체 또한 ‘예외 데이터’가 될 수 있다. 예외 발생을 알리는 데 사용되는 객체를 가리켜 ‘예외 객체’라 하며, 예외 객체의 생성을 위해 정의된 클래스를 ‘예외 클래스’라고 한다. 객체를 이용해서 예외 상황을 알리면, 예외가 발생한 원인에 대한 정보를 자세히 담을 수 있다는 장점이 있다.

또한, 예외클래스 또한 상속이 가능하며, 이를 통하여 예외의 처리를 더욱 단순화할 수 있다.


예외의 전달방식에 따른 주의사항

try 블록의 뒤를 이어서 등장하는 catch 블록이 둘 이상인 경우, 적절한 catch 블록을 찾는 과정은 다음과 같다.

 
try
{
	//예외 발생
}
catch(type1 param) //1차 : 이 예외에 타당한 catch 블록인가?
{
	//예외 처리
}
//아니라면
catch(type2 param) //2차 : 이 예외에 타당한 catch 블록인가?
{
	//예외 처리
}
//그것도 아니라면
catch(type3 param) //3ck : 이 예외에 타당한 catch 블록인가?
{
	//예외 처리
}
//다 아니라면 다른 영역으로 전달
 

따라서 클래스 A가 있고, 클래스 B와 C가 A를 상속한다면 다음과 같이 블록을 짜면 안된다. 첫번째 블록에서 걸리기 때문이다.

 
try
{}
catch (A param) //B와 C도 A의 일종으로 간주되므로 여기서 걸린다.
{}
catch (B param)
{}
catch (C param)
{}
 

다음과 같이 구성하는 것이 좋다.

 
try
{}
catch (C param) //A, B는 여기서 걸리지 않음
{}
catch (B param) //A는 걸리지 않음
{}
catch (A param)
{}
 

new 연산자에 의해서 발생하는 예외

new 연산에 의한 메모리 공간의 할당이 실패하면 bad_alloc이라는 예외가 발생한다. bad_alloc은 헤더파일 <new>에 선언된 예외 클래스로서 메모리 공간의 할당이 실패헀음을 알리는 의도로 정의되었다. 그럼 이와 관련하여 다음 예제를 보자.

 
/*BadAlloc.cpp*/
 
#include <iostream>
#include <new>
using namespace std;
 
int main(void)
{
	int num = 0;
	
	try
	{
		while (1)
		{
			num++;
			cout<<num<<"번째 할당 시도"<<endl;
			new int[10000][10000];
		}
	}
	catch(bad_alloc &bad)
	{
		cout<<bad.what()<<endl;
		cout<<"더 이상 할당 불가!"<<endl;
	}
	return (0);
}
 

예외 2번 던지기

catch 블록에 전달된 예외는 다시 던져질 수 있다. 그리고 이로 인해서 하나의 예외가 둘 이상의 catch 블록에 의해서 처리되게 할 수 있다.

 
/*ReThrow.cpp*/
 
#include <iostream>
using namespace std;
 
void Divide(int num1, int num2)
{
	try
	{
		if (num2 == 0)
			throw (0);
		cout<<"몫: "<<(num1 / num2)<<endl;
		cout<<"나머지: "<<(num1 % num2)<<endl;
	}
	catch (int expn)
	{
		cout<<"first catch"<<endl;
		throw; //예외를 다시 던진다.
	}
}
 
int main(void)
{
	try
	{
		Divide(9, 2);
		Divide(4, 0);
	}
	catch (int expn)
	{
		cout<<"second catch"<<endl;
	}
	return (0);
} 
 

bad_cast: 형 변환 중 발생하는 예외

dynamic_cast를 이용한 형 변환 중, 다음의 경우 bad_cast 예외가 발생한다.

  1. 변환 대상이 실제로 가리키는 객체의 자료형과, 변환 대상의 변환 후 자료형이 다를 경우
  2. 변환 대상의 자료형이 포인터가 아니라 참조형이어서 dynamic_cast가 NULL을 반환할 수 없는 경우

구체적인 동작은 아래의 예제를 참고하자

 
/*DynamicBadCastRef.cpp*/
 
#include <iostream>
using namespace std;
 
class SoSimple
{
	public :
		virtual void ShowSimpleInfo()
		{
			cout<<"SoSimple Base Class"<<endl;
		}
};
 
class SoComplex : public SoSimple
{
	public :
		void ShowSimpleInfo()
		{
			cout<<"SoComplex Derived Class"<<endl;
		}
};
 
int main(void)
{
	SoSimple simObj;
	SoSimple &ref = simObj;
	
	try
	{
		SoComplex &comRef = dynamic_cast<SoComplex &>(ref); //bad_cast 예외 발생
		comRef.ShowSimpleInfo();
	}
	catch(bad_cast expt)
	{
		cout<<expt.what()<<endl;
	}
	return (0);
}
 

참고자료

윤성우의_열혈_cpp_프로그래밍