[C++] 예외처리(Exception Handling)
REF : 윤성우의 열혈 C++
보통, 함수화를 (모듈화)를 하면서 가장 처음에 예외처리를 작성해 프로그램 실행도중에 일어날 수 있는 문제상황(SEGV)를 처리한다.
기존에 C에선 예외처리를 하기위해 if~else문을 자주 이용했다.
이렇게 if~else문을 이용한 예외처리는 간편하지만, 코드를 읽는 사람에게 있어선
해당 코드가
- 예외처리를 위한 코드인가?
- 프로그램 흐름을 구성하는가?
인지 쉽게 파악하지 못하게한다.
하지만, C++에서는 구조적으로 예외처리를 할 수 있는 메커니즘을 제공한다.
C++의 예외처리
- 코드의 가독성과 유지보수성을 위한 메커니즘
- 예외처리를 프로그램 흐름에서 독립시킬 수 있다.
try catch throw
- try : 예외발생에 대한 검사 범위 지정
try { //예외발생검사 }
- try블록을 묶을때는 예외가 발생했을때 실행되지 않아야되는 코드도 넣어야한다.
- catch : try에서 발생한 예외를 처리하는 코드가 담긴 영역
catch(처리할 예외 종류 명시) { //예외처리 코드 삽입 }
- 항상 try블록 뒤에 이어서 등장해야한다.
- throw : 예외가 발생했음을 알리는 문장구성
throw expn;
- expn은 어떤 데이터도 가능하지만, 예외상황에 대한 의미있는 문자여야한다.
- throw한 예외데이터의 자료형과 catch에서 잡는 데이터의 자료형은 일치해야한다.
Stack Unwinding
- throw에서 예외가 발생했지만, 바로 처리되지 않는경우.
- 함수 내부에서 throw를 해서 예외가 발생했으나, catch문이 없는 경우, 함수를 호출한 영역으로 예외처리 책임이 넘어간다.
void myfunc(int num)
{
if (num == 0)
throw num;
/*
...예외 미발생시 처리할 코드
*/
}
int main()
{
...
try
{
myfunc(num);
}
catch (int expn)
{
cout<<"예외발생"<<endl;
}
}
- myfunc 내부에서 throw를 했지만, catch문이 함수 내부에 없어서 myfunc를 호출한 main으로 예외데이터가 전달된다.
- 이는 main의 catch문에서 처리된다.
- 이런 특성은 예외 발생위치와 처리위치가 다른 경우에 유용하다.
- 이런 현상을
Stack Unwinding
(스택풀기)라고 한다.
깊은 Stack Unwinding
#include <iostream>
using namespace std;
void One(void);
void Two(void);
void Three(void);
int main(void)
{
try {
One();
} catch (int expn) {
cout<<"예외코드"<<expn<<endl;
}
return 0;
}
void One(void)
{
cout<<"One()"<<endl;
Two();
}
void Two(void)
{
cout<<"Two()"<<endl;
Three();
}
void Three(void)
{
cout<<"Three()"<<endl;
throw -1;
}
- three에서는 반드시 예외 -1이 throw된다.
- 함수의 스택이 반환되지만, 예외 데이터는 계속 전달되어 main으로 전달되게된다.
- main에서도 해당 예외가 처리되지 않았다면
terminate 함수에 의해
프로그램이 강제로 종료된다. - 자료형이 불일치할 경우
- int형 예외가 발생했지만, catch블록에서 char형으로 받는다면 해당 예외는 해당 catch블록에서 처리되지 않는다.
여러개의 catch블록
- 1개의 try블록 내에서 둘 이상의 예외사항이 발생할수도 있다.
- 이 경우, 각각의 예외를 표현하기위한 예외 데이터의 자료형이 다를수도 있으므로 catch블록이 2개이상일수도 있다.
전달되는 예외 명시
- 함수의 정의부에 함수에서 발생가능한 예외 종류를 명시해줄 수 있다.
int test(int num) throw (int, char) {}
- 예외가 발생한다면 int형 예외 데이터와 char형 예외 데이터가 전달될 수 있음을 알린다.
- 함수의 몸체부분에서는
catch(int expn), catch(char expn)
이렇게 2개의 블록이 존재해야한다. 다른 자료형이 전달
될 경우,unexpected함수
의해 프로그램이 종료된다.- 따라서, 아래와 같은 함수는
int test(int num) throw () {}
예외가 발생할 경우, 프로그램이 종료되버리고만다.
예외 클래스의 설계
- 클래스의 객체도 예외 데이터가 될 수 있다.
- 그러한 예외 데이터를 구성하는 클래스를 예외 클래스라고하는데,
- 예외를 처리하는 클래스의 catch블록에 임시객체의 형태로 참조자로 전달할 수 있다.
class E
{
public:
void showreason() {cout<<"예외발생"<<endl;};
};
class test
{
private:
int mynum;
public:
test(int num) : mynum(num)
{}
void throwing() throw (E)
{
if (this->mynum < 0)
throw (E);
else
return ;
}
};
int main(void)
{
test obj1(-100);
try{
obj1.throwing();
} catch (E &e) {
e.showreason();
}
}
상속관계의 예외 클래스
- 예외 클래스도 상속관계를 구현할 수 있다.
- 유도클래스가 구현해야하는 메서드를 구현하도록 순수가상함수로 선언해 기초클래스를 추상클래스로 선언한다.
- 이 추상 클래스를 상속하는 예외 클래스들을 만들어 기초클래스의 참조자에 해당 클래스들을 담을 수 있다.
- 하지만, 상속을 이용한 예외 클래스를 사용할때는 주의할 점이 있다.
- try ~ catch 문은 기본적으로 상단에서부터 예외를 처리한다.
- 예외가 처리될 적절한 catch블록을 위에서부터 확인하고, 찾으면 catch블록을 실행하고 끝난다.
class A
{
public:
void a() {}
};
class B : public A
{};
class C : public A
{};
void ExceptionGenerate(int expn)
{
if (expn == 1)
throw A();
else if (expn == 2)
throw B();
else
throw C();
}
int main(void)
{
try {
ExceptionGenerate(3);
ExceptionGenerate(2);
ExceptionGenerate(1);
} catch (A &expn) {
cout<<"A exception!"<<endl;
} catch (B &expn) {
cout<<"B exception!"<<endl;
} catch (C &expn) {
cout<<"C exception!"<<endl;
}
}
- ExceptionGenerate함수가 C객체를 리턴했음에도 불구하고, catch블록은 A객체를 받았다고 생각하고 A exception만 잡게된다.
- 그 이유는, C객체가 일종의 A객체로 인식되어
(A *ptr = C();)가 되는 것처럼
예외처리를 하기 때문이다.- 다시말해, 유도클래스는 기초클래스의 자료형으로 담길 수 있기때문에 발생하는 오류이다.
- 이러한 오류를 잡기위해선 catch문의 순서를 유도클래스 -> 기초클래스 순으로 변경해야한다.
C++ 표준 라이브러리 exception 클래스 사용하기
class exception
{
public:
exception () throw();
exception (const exception&) throw();
exception& operator= (const exception&) throw();
virtual ~exception() throw();
virtual const char* what() const throw();
};
- 표준 라이브러리의 구성요소에서 throw된 모든 객체는 이 클래스에서 파생된다.
- 이 유형을 참조로 catch하면 모든 표준 예외를 catch할 수 있다.
- 또한, 이 exception 클래스를 상속하는 예외처리 클래스 객체도 catch할 수 있다.
exception 클래스를 상속해 커스텀 exception 클래스를 만들 수 있다.
이때, 원래 exception 클래스의 가상함수인 what
을 오버로딩해서 해당 예외를 식별하는 문자열을 리턴하게끔할 수 있다.
표준 라이브러리에서 throw되는 예외처리 클래스들도 이 함수를 오버로딩해서 어떤 예외처리가 발생했는지 알려준다.
C++ 11에서는 이를 오버로딩하기위해서
const char* what() const noexcept {return "Ooops!\n";}
noexcept
키워드를 사용해 함수를 오버로딩하는데, C++98에서는 해당 키워드가 존재하지 않는다.
따라서, C++98에서 이 메서드를 오버로딩하고싶다면,
const char* what() const throw() {return "Ooops!\n";}
이렇게 선언해서 사용해줘야한다.
throw()
키워드를 함수 정의부에 선언하면 해당 함수가 아무런 예외를 던지지 않는다는 것을 의미한다.
댓글남기기