업데이트:

태그: ,

카테고리:



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() 키워드를 함수 정의부에 선언하면 해당 함수가 아무런 예외를 던지지 않는다는 것을 의미한다.



댓글남기기