jam 블로그

[C++] 013. 예외처리(Exception Handling) 본문

개발 및 관련 자료/C

[C++] 013. 예외처리(Exception Handling)

kid1412 2013. 5. 12. 20:19
728x90

I. 기존의 예외처리 방식

  • 예외를 처리하지 않는 프로그램의 오류
#include <iostream>
using namespace std;
int main()
{
    int a,b;
    cout<<"입력 : ";
    cin>>a>>b;
    cout<<"a/b의 몫 : "<<a/b<<endl;
    cout<<"a/b의 나머지 : "<<a%b<<endl;
    return 0;
}
  • 위의 소스는 정말 쉬운 소스이다.

    • 하지만 치명적인 오류가 있다. 다들 알다시피 b가 0일때의 문제이기 때문이다.

 

  • 전통적인 스타일의 예외처리
#include <iostream>
using namespace std;
int main()
{
    int a,b;
    cout<<"입력 : ";
    cin>>a>>b;
    cout<<"a/b의 몫 : "<<a/b<<endl;
    cout<<"a/b의 나머지 : "<<a%b<<endl;
    return 0;
}
  • 위와 같은 식으로 하면 당연히 오류가 발생하는 것을 막을수는 있다.

    • 하지만 예외처리를 위한 코드 부분과 일반적인 프로그램의 흐름을 위한 코드 부분을 명확히 구분 짓지 못한다.

      •  짧은 소스일때는 상관 없지만 길경우 구별하기 힘들다.

 

II. 기본적인 예외 처리 메커니즘(try, catch, throw)

  • try

    • 예외 발생에 대한 검사 범위를 설정 할 때 사용한다.
try
{
    //예외 발생 예상 코드
}
  •  catch

    • 예외를 처리하는 코드 구간을 선언할때 사용한다. try 구간 내에서 발생한 예외 상황을 처리하는 코드가 존재하는 영역이다.
catch(처리되어야 할 예외의 종류)
{
     //예외를 처리하는 코드가 존재할 위치
}
  •  try와 catch

    • try바로 뒤에 catch가 등장한다.
  • throw

    • 예외 상황이 발생 하였음을 알릴때 사용
try
{
    if(예외 상황 발생)
    throw ex;
}
catch(exception ex)
{
    예외 상황 처리
}

 

  •  예외 처리 메커니즘의 적용
#include <iostream>
using namespace std;
int main()
{
    int a,b;
    cout<<"입력 : ";
    cin>>a>>b;
    try
    {
        if(b==0)
            throw b;
        cout<<"a/b의 몫 : "<<a/b<<endl;
        cout<<"a/b의 나머지 : "<<a%b<<endl;
    } 
    catch (int exception)
    {
        cout<<exception<<" 입력."<<endl;
        cout<<"입력 오류! 다시 실행하세요."<<endl;
    }
    return 0;
}
  • 위와 같이 하면 된다.

 

  • 예외 처리 적용 시 프로그램의 흐름
#include <iostream>
using namespace std;
int main()
{
	int a,b;
	cout<<"입력 : ";
	cin>>a>>b;
	try
	{
		cout<<"try block start"<<endl;
		if(b==0)
			throw b;
		cout<<"a/b의 몫 : "<<a/b<<endl;
		cout<<"a/b의 나머지 : "<<a%b<<endl;
		cout<<"try block end"<<endl;
	} 
	catch (int exception)
	{
		cout<<"catch block start"<<endl;
		cout<<exception<<" 입력."<<endl;
		cout<<"입력 오류! 다시 실행하세요."<<endl;
	}
	cout<<"Thank You!"<<endl;
	return 0;
}
  • 위 소스를 실행하면 어떻게 흘러가는지 알수 있다.

 

III. Stack Unwinding(스택 풀기)

  • 전달되는 예외
#include <iostream>
using namespace std;
int divide(int a, int b);
int main()
{
	int a,b;
	cout<<"입력 : ";
	cin>>a>>b;
	try
	{
		cout<<"a/b의 몫 : "<<divide(a,b)<<endl;
	} 
	catch (int exception)
	{
		cout<<exception<<" 입력."<<endl;
		cout<<"입력 오류! 다시 실행하세요."<<endl;
	}
	return 0;
}

int divide(int a, int b)
{
	if(b==0)
	throw b;
	return a/b;
}
  • 처리되지 않은 예외는 전달된다는 것이다.

    • 이러한 현상을 가리켜 스택 unwinding이라고 한다.
#include <iostream>
using namespace std;
void fct1();
void fct2();
void fct3();
int main()
{
	try
	{
		fct1();
	}
	catch(int ex)
	{
		cout<<"예외 : "<<ex<<endl;
	}
	return 0;
}

void fct1()
{
	fct2();
}

void fct2()
{
	fct3();
}

void fct3()
{
	throw 100;
}
  • 예외가 전달되는 과정이 함수의 스택이 풀리는 순서와 일치하기 때문에 스택 unwinding 이라고 한다.

 

  • 처리되지 않은 예외
#include <iostream>
using namespace std;
int divide(int a, int b);
int main()
{
	int a,b;
	cout<<"입력 : ";
	cin>>a>>b;
	cout<<"a/b의 몫 : "<<divide(a,b)<<endl;
	return 0;
}

int divide(int a, int b)
{
	if(b==0)
	throw b;
	return a/b;
}
  • 위와 같은 소스를 실행 시켜서 오류를 나게 해보자

    • b가 0이면 예외가 발생을 한다. 하지만 여기서 보면 예외를 처리해 주는 부분이 존재하지 않는다.
    • 이러한 경우 stdlib.h 안에 abort 함수가 호출되면서 프로그램을 종료 시킨다.
#include <iostream>
#include <stdlib.h>
using namespace std;

int main()
{
	abort();
	cout<<"end"<<endl;
	return 0;
}
  • 위와 같은 소스를 실행 시키면 오류메시지를 띄우면서 cout<<"end"<<endl; 위에서 종료가 된다.
#include <iostream>
using namespace std;
int divide(int a,int b);
int main()
{
	int a, b;
	cout<<"두개 입력 : ";
	cin>>a>>b;
	try
	{
		cout<<"a/b의 몫 : "<<divide(a,b)<<endl;
	}
	catch (char exception)
	{
		cout<<exception<<" 입력."<<endl;
		cout<<"입력 오류!"<<endl;
	}
	return 0;
}

int divide(int a,int b)
{
	if(b==0)
		throw b;
	return a/b;
}
  • 위의 소스를 보면 예외를 처리해 주는 부분이 있다.

    • 다만 catch구문에서 char형 예외를 처리하겠다는 것만 있기 때문에 abort 함수가 호출된다.

 

  • 전달되는 예외 명시하기

    • 함수를 정의하는데 있어서 전달될수 있는 예외의 종류를 명시해 줄 수 있다.
int fct(double b) throw(int)
{
    ...
}
  •  상황에 따라서 int형 예외가 전달될 수 있음을 선언하고 있는 것이다.

    • int형이 아닌 다른형의 예외가 발생하면 윗부분에서 봤다시피 abort함수가 호출된다.
int fct(double b) throw(int, double, char*)
{
    ...
}
  • 위에 처럼 둘이상의 예외종류를 선언해 줄 수 있다.
int fct(double b) throw()
{
    ...
}
  • 위와 같은 경우는 어떠한 예외도 전달하지 않는다는 것이다.

    • 만약에 예외가 전달된다면 abort함수가 호출된다.

 

  • 하나의 try 블록과 여러개의 catch 블록
#include <iostream>
using namespace std;
int main()
{
	int num;
	cout<<" input : ";
	cin>> num;
	try
	{
		if(num>0)
			throw 10;
		else
			throw 'm';
	}
	catch (int exp)
	{
		cout<<"int형 예외 발생"<<endl;
	}
	catch (char exp)
	{
		cout<<"char형 예외 발생 "<<endl;
	}
	return 0;
}
  • 위의 소스 처럼 0보다 크면 int형 예외, 그 나머지는 char형 예외라고 지정을 해놓았다.

    • 위에 처럼 try~catch~catch 이런식으로 선언할수 있는데 역시나 마찬가지로 try와 catch 블록 사이에 다른 문장이 존재할 수 없다.
#include <iostream>
using namespace std;
char* account="1234-4576";
int sid = 1122;
int balance = 1000;

class AccountExpt
{
	char acc[10];
	int sid;
	public:
		AccountExpt(char* str,int id)
		{
			strcpy(acc,str);
			sid=id;
		}
	
		void What()
		{
			cout<<"계좌 : "<<acc<<endl;
			cout<<"비번 : "<<sid<<endl;
		}
};

int main()
{
	char acc[10];
	int id;
	int money;
	cout<<"계좌번호 입력 :";
	cin >> acc;
	cout<<"비밀번호 입력 : ";
	cin >> id;
	if (strcmp(account,acc)||sid!=id)
		throw AccountExpt(acc,id);
	cout<<"출금액 입력 : ";
	cin>>money;
	if(balance<money)
		throw money;
	balance -=money;
	cout<<"잔액 :"<<balance<<endl;
	return 0; 
}
  • 위의 소스는 적절하게 예외처리를 안한 상태이다.
#include <iostream>
using namespace std;
char* account="1234-4576";
int sid = 1122;
int balance = 1000;

class AccountExpt
{
	char acc[10];
	int sid;
	public:
		AccountExpt(char* str,int id)
		{
			strcpy(acc,str);
			sid=id;
		}
		
		void What()
		{
			cout<<"계좌 : "<<acc<<endl;
			cout<<"비번 : "<<sid<<endl;
		}
};

int main()
{
	char acc[10];
	int id;
	int money;
	try
	{
		cout<<"계좌번호 입력 :";
		cin >> acc;
		cout<<"비밀번호 입력 : ";
		cin >> id;
		if (strcmp(account,acc)||sid!=id)
			throw AccountExpt(acc,id);
	}
	catch(AccountExpt& expt)
	{
		cout<<"다시 입력을 확인하세요"<<endl;
		expt.What();
	}
	try
	{
		cout<<"출금액 입력 : ";
		cin>>money;
		if(balance<money)
			throw money;
		balance -=money;
		cout<<"잔액 :"<<balance<<endl;
	}
	catch(int money)
	{
		cout<<"부족 금액 :"<<money-balance<<endl;
	}
	return 0; 
}
  • 실행시켜 보면 알겠지만 예외처리는 해주었다고 하지만 엉망이다.

    • 참고로 catch(AccountExpt& expt) 이렇게 한 이유는 객체가 복사되는 부담을 줄이기 위한 것이기 때문에 꼭 이렇게 할 필요는 없다.
#include <iostream>
using namespace std;
char* account="1234-4576";
int sid = 1122;
int balance = 1000;

class AccountExpt
{
	char acc[10];
	int sid;
	public:
		AccountExpt(char* str,int id)
		{
			strcpy(acc,str);
			sid=id;
		}
		
		void What()
		{
			cout<<"계좌 : "<<acc<<endl;
			cout<<"비번 : "<<sid<<endl;
		}
};

int main()
{
	char acc[10];
	int id;
	int money;
	try
	{
		cout<<"계좌번호 입력 :";
		cin >> acc;
		cout<<"비밀번호 입력 : ";
		cin >> id;
		if (strcmp(account,acc)||sid!=id)
			throw AccountExpt(acc,id);
		cout<<"출금액 입력 : ";
		cin>>money;
		if(balance<money)
			throw money;
		balance -=money;
		cout<<"잔액 :"<<balance<<endl;
	}
	catch(AccountExpt& expt)
	{
		cout<<"다시 입력을 확인하세요"<<endl;
		expt.What();
	}
	catch(int money)
	{
		cout<<"부족 금액 :"<<money-balance<<endl;
	}
	return 0; 
}
  • 위와 같이 try 문에 다 집어 넣어야 정상적으로 된다.

    • 이유는 앞전에서 봤을때 비밀번호가 이상하면 예외처리를 부르고 나서 또 다시 그다음에 이어서 실행이 되기때문이다.

 

IV. 예외 상황을 나타내는 클래스의 설계

  •   예외를 발생시키기 위해서 클래스를 정의하고 객체를 생성하였다.

    • 이러한 객체를 예외 객체라고 하며, 예외 객체를 위해 정의되는 글래스를 가리켜 예외 클래스라 한다.

 

V. 예외를 나타내는 클래스의 상속

  • catch 블록에 예외가 전달되는 방식

    • 이어서 선언되어 있는 catch 블록에 예외가 전달되는 형태를 보면 함수 오버로딩과 유사하다.
    • 단, 오버로딩된 함수는 딱 봐서 매개변수가 일치하는 함수가 호출되고 예외를 처리할 catch 블록은 위에서 부터 순차적으로 비교를 이루어지고나서 결정이 난다.

 

  • 상속 관계에 있는 예외 객체의 전달
#include <iostream>
using namespace std;
class ExceptA
{
	public:
		void What()
		{
			cout<<"ExceptA 예외"<<endl;
		}
};
class ExceptB:public ExceptA
{
	public:
		void What()
		{
			cout<<"ExceptB 예외"<<endl;
		}
};
class ExceptC : public ExceptB
{
	public:
		void What()
		{
			cout<<"ExceptC 예외"<<endl;
		}
};

void ExceptFunction(int ex)
{
	if(ex == 1)
		throw ExceptA();
	else if(ex == 2)
		throw ExceptB();
	else
	throw ExceptC();
}

int main()
{
	int exID;
	cout<<"발생시킬 예외의 숫자 : ";
	cin>>exID;
	try
	{
		ExceptFunction(exID);
	}
	catch(ExceptA& ex)
	{
		cout<<"catch(ExceptA& ex)에 의한 처리"<<endl;
		ex.What();
	}
	catch(ExceptB& ex)
	{
		cout<<"catch(ExceptB& ex)에 의한 처리"<<endl;
		ex.What();
	}
	catch(ExceptC& ex)
	{
		cout<<"catch(ExceptC& ex)에 의한 처리"<<endl;
		ex.What();
	}
	return 0;
}
  • 입력을 1,2,3을 해도 ExceptA에 의한 처리로만 나온다. 왜그럴까?

    • 앞에서 말했다시피 catch는 순차적으로 비교를 한다. A를 먼저 비교 할텐데 1,2,3이 예외가 발생하면 각각의 catch에 가겠지만 상속을 받았기 때문에 A에서도 예외가 적용이 되어서 A로만 찍힌다.
    • 다음과 같이 바꾸어 보자.
#include <iostream>
using namespace std;
class ExceptA
{
	public:
		void What()
		{
			cout<<"ExceptA 예외"<<endl;
		}
};
class ExceptB:public ExceptA
{
	public:
		void What()
		{
			cout<<"ExceptB 예외"<<endl;
		}
};
class ExceptC : public ExceptB
{
	public:
		void What()
		{
			cout<<"ExceptC 예외"<<endl;
		}
};

void ExceptFunction(int ex)
{
	if(ex == 1)
		throw ExceptA();
	else if(ex == 2)
		throw ExceptB();
	else
		throw ExceptC();
}

int main()
{
	int exID;
	cout<<"발생시킬 예외의 숫자 : ";
	cin>>exID;
	try
	{
		ExceptFunction(exID);
	}
	catch(ExceptC& ex)
	{
		cout<<"catch(ExceptC& ex)에 의한 처리"<<endl;
		ex.What();
	}
	catch(ExceptB& ex)
	{
		cout<<"catch(ExceptB& ex)에 의한 처리"<<endl;
		ex.What();
	}
	catch(ExceptA& ex)
	{
		cout<<"catch(ExceptA& ex)에 의한 처리"<<endl;
		ex.What();
	}
	return 0;
}
  • 앞전의 소스와 달라진 점은 catch 블록의 순서가 달라졌다.

    • 바꾼 이유는 IS-A관계는 역으로 성립하지 않음을 이용한 것이다. 즉, A 예외는 C예외가 아니다.

 

VI. new 연산자에 의해 전달되는 예외

  • new 연산자에 의해서 메모리 할당에 실패 했을 경우 NULL포인터가 리턴된다고 했었다.

    • C++표준에서는 new 연산자가 메모리 할당에 실패했을 경우 bad_alloc 예외가 전달된다고 한다.
    • 자세한건 MSDN을 참조하는게 좋다.
#include <iostream>
#include <new>
using namespace std;

int main()
{
	try
	{
		int i=0;
		while(1)
		{
			cout<<i++<<"번째 할당"<<endl;
			double(*arr)[10000] = new double[10000][10000];
		}
	}
	catch(bad_alloc ex)
	{
		ex.what();
		cout<<endl<<"End"<<endl;
	}
	return 0;
}
  • 위의 소스를 실행 시켜 보면 무한루프를 돌면서 메모리 공간만 할당하고 있다.

    • 어느 순간 new 연산이 실패로 돌아가면 bad_alloc 예외가 발생하고 END를 찍게된다.

 

VII. 예외처리에 대한 나머지 문법 요소

  • 모든 예외를 처리하는 catch 블록
try
{
}
catch(...)
{
}
  •  위에서 보면 "..." 의 선언은 모든 예외를 다 처리하겠다는 선언이다.(잘 사용하지는 않는다.)

 

  • 예외 다시 던지기
#include <iostream>
using namespace std;

class Exception
{
	public:
		void what()
		{
			cout<<"Simple Exception"<<endl;
		}
};

void ThrowException()
{
	try
	{
		throw Exception();
	}
	catch(Exception& t)
	{
		t.what();
		throw;
	}
}

int main()
{
	try
	{
		ThrowException();
	}
	catch(Exception& t)
	{
		t.what();
	}
	return 0;
}
  • 먼저 ThrowException()에서 예외를 처리하고 catch에서 throw로 예외를 던졌다.

    • 던져진 예외는 메인에 있는 ThrowException();으로 가고 그다음에 있는 catch에서 다시 한번 예외가 처리되어 Simple Exception이 2번 찍힌다.
    • 다음과 같은 경우 예외를 다시 던질 것을 고려해 보자.

      • catch 블록에 의해 예외를 잡고 보니, 처리하기가 마땅치 않다. 다른 catch블록에 의해서 예외가 처리되길바란다.
      • 하나의 예외에 대해서 처리되어야 할 영역(예외가 발생했음을 알려줘야 할 영역)은 둘 이상이다.
Comments