C++/Exceptions

C++ Exceptions

Soul-Learner 2016. 12. 27. 14:12

C++ 프로그래밍, 예외처리 ( Exceptions )


예외처리(Exception Handling)란?

프로그램 실행 중에 발생할 수 있는 오류를 인지하고 해당 오류에 적절히 반응하는 코드가 실행될 수 있도록 하는 것


예외처리의 필요성

예외를 인지하고 적절히 반응하는 코드가 준비되어 있지 않으면 프로그램은 예외가 발생한 위치에서 비정상 종료하거나 오작동을 일으킨다. 여기서 비정상 종료란 아무런 준비나 후속조치가 없이 무작정 예외의 위치에서 프로그램이 종료되는 것이다. 소프트웨어가 제어하는 미사일이 날아가는 중에 오류가 발생했는데 아무런 후속조치가 없다면 언제 어디서 폭발할지 모르는 상황이 되므로 큰 재난으로 이어질 수도 있다. 여기서 우리는 만약 오류가 발생하면 어떤 작업이 실행되도록 미리 어떤 계획이 필요하다는 것을 알 수 있다


예외(오류)가 발생하는 코드의 예

동적으로 메모리를 할당할 때 지나치게 많은 메모리를 요청하는 부분에서 런타임 오류가 발생하지만 예외를 

#include <iostream>
#include <locale>

using namespace std;

int main() {
	setlocale(LC_ALL, "");
	wcout << L"C++ 예외처리(Exception Handling)" << endl;

	int * p = 0;

	// 오류가 발생할 수 있는 과도한 동적 메모리 할당
	p = new int[10000000000];

	wprintf(L"p[0]= %d \n", p[0]);

	return 0;
}

위의 코드 실행결과 발생하는 오류

This application has requested the Runtime to terminate it in an unusual way.

Please contact the application's support team for more information.


C++ 언어의 예외처리 구문

try {         // 예외 발생을 검사하는 블럭설정

         // 오류가 발생할 수도 있는 실행문

}

catch( 던져진 예외 )       // 해당 예외 발생시 실행되는 블럭

{

         // 발생한 예외에 대응하여 실행될 적절하는 코드

}


예외처리를 적용한 예 (시스템에서 생성된 표준 예외(exception)객체가 던져지는 예)

#include <iostream>
#include <locale>

using namespace std;

int main() {
	setlocale(LC_ALL, "");
	wcout << L"C++ 예외처리(Exception Handling)" << endl;

	int * p = 0;
	try{
		// 오류가 발생할 수 있는 과도한 동적 메모리 할당
		p = new int[10000000000];
	}
	catch(exception& e)
	{
		bad_alloc* ba = dynamic_cast<bad_alloc*> (&e);
		if(ba!=NULL)
		{
			wcerr << L"동적 메모리 할당시 오류발생!" << endl;
			wcerr << e.what() << endl;
			exit(1);  
		}
	}

	wprintf(L"p[0]= %d \n", p[0]);

	return 0;
}


위와 같이 시스템에서 지원하는 표준 예외가 발생하지 않는 경우에는 개발자가 정의한 예외를 던질 수도 있다. 개발자 정의 예외를 발생시킬 때는 throw 문장을 이용하여 임의의 아규먼트를 지정하면 catch 블럭의 파라미터로 아규먼트가 전달된다.

C++에서는 0 으로 숫자를 나눌 때 예외가 발생하지 않는다. 그러나 이러한 경우에 프로그램이 그냥 실행된다면 오작동하게 될 것이므로 이러한 상황을 인지하여 적절한 처리를 해줘야 하는데 이 때에도 try catch 구문을 활용할 수 있다.

분수의 분모에 0이 전달될 때도 아무런 오류가 발생하지 않는 예

#include <iostream>
#include <locale>

using namespace std;

int main() {
	setlocale(LC_ALL, "");
	wcout << L"C++ 예외처리(Exception Handling)" << endl;

	double n = 100;   // 분자(numerator)
	double d = 0;       // 분모(denominator)

	wcout << L"분모를 입력하세요:";
	wcin >> d;

	double res = n/d;  // 분모에 0이 전달될 때도 아무런 오류가 발생하지 않는다

	wprintf(L"%f / %f = %f \n", n, d, res); // 100/0 = inf (오작동)

	return 0;
}


위의 코드를 수정하여 특정 조건에서 개발자가 예외를 던지는 예 (임의의 데이터를 던질 수 있다)

#include <iostream>
#include <locale>

using namespace std;

int main() {
	setlocale(LC_ALL, "");
	wcout << L"C++ 예외처리(Exception Handling)" << endl;

	double n = 100;     // 분자(numerator)
	double d = 0;       // 분모(denominator)

	wcout << L"분모를 입력하세요:";
	wcin >> d;

	try{
		if(d==0) throw 0; // 임의의 데이터를 던질 수 있다

		// 위의 throw 문장 실행시 아래의 문장은 실행되지 않고 바로 catch 블럭으로 이동한다
		double res = n/d;
		wprintf(L"%f / %f = %f \n", n, d, res);
	}
	catch(int cause)  // throw 에서 지정한 자료형만을 선언해야 예외처리가 실행된다
	{
		if(cause==0) {
			wcerr << L"0은 입력할 수 없습니다 ";
			exit(0);
		}
	}

	return 0;
}


원인을 알 수 없는 예외다수개의 예외를 처리하는 예

한개의 try 블럭에서 종류가 다른 다수개의 예외가 발생할 수 있다면 try 블럭 아래에 다수개의 catch 블럭을 추가할 수 있다. 

#include <iostream>
#include <locale>

using namespace std;

int main() {
	setlocale(LC_ALL, "");
	wcout << L"C++ 예외처리(Exception Handling)" << endl;

	wcout << L"구구단 숫자를 입력(2~9)하세요:";

	wstring inStr;
	wcin >> inStr;

	int dan = 0;
	try {
		// wcin은 입력을 쉽게 정수로 변환할 수 있지만 문자열 다루는 연습도 할겸...
		dan = _wtoi( inStr.c_str() );
		if(!(dan>=2 && dan<=9)) throw 0;
	}
	catch(int cause){
		wcerr << L"숫자가 아닙니다" << endl;
		exit(0);
	}
	catch(...){ // 위에서 처리하지 못한 예외는 이곳에서 모두 처리된다
		wcerr << L"원인을 알 수 없는 오류 발생" << endl;
		exit(1);
	}

	// 위의 예외처리 문장의 구조상 여기는 예외가 없을 경우에만 실행될 수 있다
	wstring line;
	for(int i=1;i<=9;i++) {
		wprintf(L"%d x %d = %d \n", dan, i, dan*i);
	}

	return 0;
}


함수의 원형에 던질 수 있는 예외 선언하기

함수 선언문에 해당 함수가 발생시칼 수 있는 예외의 종류를 명시할 수 있는데, 이렇게 하면 명시되지 않은 예외가 발생할 경우에는 처리되지 않는다. 

  • void login() throw () : 함수내에서 외부로 어떤 예외도 던질 수 없다
  • void login() ;: 함수내에서 외부로 모든 종류의 예외를 던질 수 있다
#include <iostream>
#include <locale>
#include <sstream>

using namespace std;

void login(wstring[]) throw (int); // 예외의 아규먼트로 정수만 던지도록 제한함

int main() {
	setlocale(LC_ALL, "");
	wcout << L"C++ 예외처리(Exception Handling)" << endl;

	wstring info[2];
	try{
		login(info);
		wcout << L"로그인에 성공했습니다" << endl;
	}catch(int cause){
		if(cause==0){
			wcerr << L"아이디는 3자 이상이어야 합니다" << endl;
		}else if(cause==1){
			wcerr << L"암호는 3자 이상이어야 합니다" << endl;
		}else if(cause==2){
			wcerr << L"암호는 숫자이어야 합니다" << endl;
		}
	}

	return 0;
}

void login(wstring* user_data) throw (int) {
	wcout << L"아이디(3자이상) 암호(숫자만 3자 이상)를 공백으로 구분하여 입력하세요:";

	wstring loginStr;
	getline(wcin, loginStr);

	wstringstream ss(loginStr);
	wstring id, pwd;
	ss >> id >> pwd;

	if(id.size()<3) throw 0;
	if(pwd.size()<3) throw 1;

	bool boolDigit = true;
	locale loc;
	for(unsigned i=0;i<pwd.size();i++){
		wchar_t wch = pwd.at(i);
		if(!isdigit(wch, loc)) {
			boolDigit = false;
			break;
		}
	}

	if(!boolDigit) throw 2;

	user_data[0] = id;
	user_data[1] = pwd;

	wcout << loginStr << endl;
}


개발자 정의 예외 클래스 사용하기

throw 문장으로 던져질 수 있는 데이터 타입은 기본형 데이터 뿐만 아니라 시스템에서 지원하는 exception 객체도 throw 에 의해 던져질 수 있다. 그러므로 개발자가 exception 클래스를 상속하여 개발자 정의 예외 클래스를 정의하고 특정 예외 조건에서 throw 문장으로 그 객체를 던지면 catch 블럭의 파라미터로 전달된다. exception 클래스의 what() 함수는 가상함수이므로 이를 오버라이딩하면 예외의 원인을 표현하는 적절한 문장을 리턴할 수 있다.

#include <iostream>
#include <locale>
#include <sstream>
#include <cstdlib>  // wcstombs() 사용

using namespace std;

class LoginException : public exception {
	wstring wmsg;

	public:
	LoginException(wstring& _wstr) { this->wmsg = _wstr; }

	virtual const char* what() const throw () {
		char* cmsg = new char[wmsg.size()*2];
		wcstombs(cmsg, wmsg.c_str(), wmsg.size()*2);
		return cmsg;
	}
	// looser throw specifier for 'virtual LoginException::~LoginException()'
	// 위와 같은 오류를 피하기 위해 소멸자를 오버라이드하여 throw () 추가한다
	// exception을 상속할 때는 모든 함수에 throw () 선언이 요구된다
	~LoginException() throw (){}
} ;

void login(wstring[]) throw (LoginException); // 예외의 아규먼트로 정수만 던지도록 제한함

int main() {
	setlocale(LC_ALL, "");
	wcout << L"C++ 예외처리(Exception Handling)" << endl;

	wstring info[2];
	try{
		login(info);
		wcout << L"로그인에 성공했습니다" << endl;
	}
	catch(LoginException& e)
	{
		cerr << e.what() << endl;
	}

	return 0;
}

void login(wstring* user_data) throw (LoginException) {
	wcout << L"아이디(3자이상) 암호(숫자만 3자 이상)를 공백으로 구분하여 입력하세요:";

	wstring loginStr;
	getline(wcin, loginStr);

	wstringstream ss(loginStr);
	wstring id, pwd;
	ss >> id >> pwd;

	// 참고 : 아래의 문장은 다음과 같은 컴파일 오류가 발생한다
	// LoginException ex(L"오류발생");
	// invalid initialization of non-const reference
	// 예를 들어,	int& n = 10; 와 같은 문장을 생각해보면...
	// 10은 리터럴이므로 상수인데, 할당 연산자의 왼쪽에서 참조변수에 저장하면 참조변수를 사용하여
	// 리터럴 상수를 변경할 수 있다는 말이 되는데, 이런 것은 C++ 문법에서 허용되지 않는다

	if(id.size()<3) {
		wstring wstr = L"아이디는 3자 이상이어야 합니다";
		throw LoginException(wstr);
	}

	if(pwd.size()<3) {
		wstring wstr = L"암호는 3자 이상이어야 합니다";
		throw LoginException(wstr);
	}

	bool boolDigit = true;
	locale loc;
	for(unsigned i=0;i<pwd.size();i++){
		wchar_t wch = pwd.at(i);
		if(!isdigit(wch, loc)) {
			boolDigit = false;
			break;
		}
	}

	if(!boolDigit) {
		wstring wstr = L"암호는 숫자만 입력해야 합니다";
		throw LoginException(wstr);
	}

	user_data[0] = id;
	user_data[1] = pwd;

	wcout << loginStr << endl;
}


C++의 표준 예외 클래스

C++에는 exception 클래스를 파생하여 생성된 몇가지 예외 클래스가 포함되어 있다. 이들 예외가 발생되는 환경에서는 모두 catch 블럭에 파라미터로 exception 을 사용하면 예외를 잡아서 처리할 수 있다

  • bad_alloc
  • bad_cast
  • bad_exception
  • bad_typeid
  • ios_base::failure