어떠한 경우에 예외의 발생위치와 예외의 처리위치가 달라야 하는지 예를 하나 들겠습니다. 대부분의 경우에 있어서 예외의 발생위치와 예외의 처리위치는 다릅니다.
#include <iostream> #include <cstring> #include <cmath> using namespace std; int StoI(char * str) { int len = strlen(str); int num = 0; for (int i = 0; i < len; i++) { if (str[i]<'0' || str[i]>'9') throw str[i]; num += (int)(pow((double)10, (len - 1) - i) * (str[i] + (7 - '7'))); } return num; } int main(void) { char str1[100]; char str2[200]; while (1) { cout << "two number input: "; cin >> str1 >> str2; try { cout << str1 << " + " << str2 << " = " << StoI(str1) + StoI(str2) << endl; break; } catch (char ch) { cout << "character " << ch << "input." << endl; cout << "reinput again" << endl << endl; } } cout << "End of main" << endl; return 0; }
|
이 함수는 문자열을 정수로 변환하는 함수입니다. 양의 정수에 대해서만 동작하도록 정의되었습니다. 인자로 전달된 문자열에 숫자가 아닌 형태이 문자가 존재하는 경우 예외를 발생시킵니다. 인자로 전달된 문자열에 숫자가 아닌 형태의 문자가 존재하는 경우 예외를 발생시킵니다. 이 예외에 대한 처리는 SToI 하수를 호출하는 대상 및 상황에 따라서 달라지기 때문에 예외의 처리는 StoI 함수를 호출한 영역으로 넘기는 것이 타당합니다. 문자열을 정수의 형태로 변환하는 문장입니다. 정수로 변경 가능한 문자열을 입력 받을 때까지 계소개서 입력 받도록 반복문이 구성되었습니다. 덧셈 연산이 성공하고 나면, 이 문장의 실행을 통해서 반복문을 빠져나갑니다. 예외발생시 실행되는 catch 블록에는 break 문이 존재하지 않습니다. 따라서 예외가 발생하면 반복문 영역을 재실행하게 됩니다. 함수 내에서 함수를 호출한 영역으로 예외 데이터를 전달하면, 해당 함수는 더 이상 실행되지 않고 종료가 됩니다.
예외가 처리되지 않아서, 함수를 호출한 영역으로 예외 데이터가 전달되는 현상을 가리켜 스택 풀기라고 합니다.
#include <iostream> using namespace std; void SimpleFuncOne(void); void SimpleFuncTwo(void); void SimpleFuncThree(void); int main(void) { try { SimpleFuncOne(); } catch (int expn) { cout << "expn code: " << 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; } |
예외가 처리될 때까지, 호출된 함수의 역순으로 예외 데이터가 전달됩니다. 결국 예외는 SimpleFinThree 함수에서 발생했지만, 예외의 처리는 main 함수에서 이뤄지는 형태가 됩니다. 그런데 예외 데이터가 전달되면, 예외 데이터를 전달한 함수는 종료되기 때문에, 예외 데이터를 전달한 함수의 스택이 반환되는 것은 당연합니다. 스택의 반환이라고 하는 것입니다. 예외가 처리되지 않아서, 예외 데이터가 main 함수에까지 도달했는데, main 함수에서조차 예외를 처리하지 않으면, terminate 함수가 호출되면서 프로그램이 종료되어 버립니다.
자료형이 일치하지 않아도 예외 데이터는 전달됩니다.
하나의 try 블록과 다수의 catch 블록
#pragma warning(disable:4996) #include<iostream> #include<cstring> #include<cmath> using namespace std; int StoI(char * str) { int len = strlen(str); int num = 0; if (len != 0 && str[0] == '0') throw 0; for (int i = 0; i < len; i++) { if (str[i]<'0' || str[i]>'9') throw str[i]; num += (int)(pow((double)10, (len - 1) - i) * (str[i] + (7 - '7'))); } return num; } int main(void) { char str1[100]; char str2[200]; while (1) { cout << "two number input:"; cin >> str1 >> str2; try { cout << str1 << "+" << str2 << "=" << StoI(str1) + StoI(str2) << endl; break; } catch (char ch) { cout << "char" << ch << "input" << endl; cout << "re processing" << endl << endl; } catch (int expn) { if (expn == 0) cout << "0 start no" << endl; else cout << "abnormal" << endl; cout << "re input" << endl << endl; } } cout << "end of program" << endl; return 0; } |
0으로 시작하는 수의 입력에 예외를 발생시키도록 하였습니다. 삽입된 0은 프로그램 사용자가 실수로 입력한 값일 확률이 높다고 판단한 것입니다. 이러한 예외가 발생하였을 때, 예외 데이터 0을 전달하도록 throw절을 구성하였습니다. catch 블록은 char형 예외 데이터를 처리하고, catch 블록은 int형 예외 데이터를 처리합니다. 예외 데이터의 값을 통해서 예외의 종류를 구분할 수 있음을 보이고 있습니다. expn에 0이 아닌 값이 전달되지 않습니다. 이러한 구성이 가능함을 보였습니다.
함함수 내에서 발생할 수 있는 예외의 종류도 함수의 특징으로 간주합니다. 따라서 이미 정의된 특정 함수의 호출을 위해서는 함수의 이름, 매개변수 선언, 반환형 정보에 더해서, 함수 내에서 전달될 수 있는 예외의 종류(예외 데이터의 자료형)과 그 상황도 알아야 합니다. 그래서 해당 함수의 호출문장을 감싸는 적절한 try~catch 블록을 구성할 수 있지 않겠나요. 따라서 함수를 정의할 때에는 함수 내에서 발생 가능한 예외의 종류를 다음과 같이 명시해주는 것이 좋습니다. 전달되는 예외의 자료형을 명시하는 부분이 비어있습니다. 즉, 이는 어떠한 예외도 전달하지 않음을 의미합니다. 따라서 위의 함수가 예외를 전달할 경우 프로그램은 그냥 종료가 됩니다.
함수의 선언이 명시되지 않은 예외가 전달될 경우 unexpected 라는 이름의 함수가 호출이 되며, 이 함수의 기본 기능은 프로그램의 종료입니다. 때문에 명시되지 않은 예외가 전달될 경우 프로그램이 종료되는 것입니다.
예외상황을 표현하는 예외 클래스의 설계
#pragma warning(disable:4996) #include <iostream> #include <cstring> using namespace std; class DepositException { private: int reqDep; public: DepositException(int money) : reqDep(money) { } void ShowExceptionReason() { cout << "expn : " << reqDep << "input no" << endl; } }; class WithdrawException { private: int balance; public: WithdrawException(int money):balance(money) { } void ShowExceptionReason() { cout << "balance: " << balance << ", short" << endl; } }; class Account { private: char accNum[50]; int balance; public: Account(char * acc, int money) :balance(money) { strcpy(accNum, acc); } void Deposit(int money) throw (DepositException) { if (money < 0) { DepositException expn(money); throw expn; } balance += money; } void Withdraw(int money) throw (WithdrawException) { if (money > balance) throw WithdrawException(balance); balance -= money; } void ShowMyMoney() { cout << "balance : " << balance << endl << endl; } }; int main(void) { Account myAcc("56546-242342", 5000); try { myAcc.Deposit(2000); myAcc.Deposit(-300); } catch (DepositException &expn) { expn.ShowExceptionReason(); } myAcc.ShowMyMoney(); try { myAcc.Withdraw(3500); myAcc.Withdraw(4500); } catch (WithdrawException &expn) { expn.ShowExceptionReason(); } myAcc.ShowMyMoney(); return 0; } |
DepositException은 예외 클래스입니다. 입금의 과정에서 발생할 수 있는 예외상황을 알리기 위해서 정의된 클래스입니다. WithdrawException도 예외 클래스입니다. 출금의 과정에서 발생할 수 있는 잔액부족의 상황을 알리기 위해서 정의된 클래스입니다. 예외객체의 생성 및 전달의 과정을 보이고 있습니다. 기본 자료형 데이터를 이용하는 것과 차이가 없습니다. 예외 객체를 전달하는 일반적인 방법을 보이고 있습니다. 예외객체는 C++의 예외처리 메커니즘에 의해 처리되기 때문에 코드상에서 이를 직접 참조할 필요가 없습니다. 따라서 예외객체는 임시객체의 형태로 생성하는 것이 보통입니다. 참조자를 선언해서 예외객체를 전달받고 있습니다. 예외객체를 복사할 필요가 없기 때문에 참조자를 선언하였습니다.
상속을 통해서 예외 클래스를 묶으면, 예외의 처리를 단순화시킬 수 있습니다. 물론 이것이 항상 좋은 것은 아니지만, 단순화해도 되는 상황에서는 유용하게 활용될 수 있습니다. 예외가 발생하면, 해당 예외 데이터를 전달받을 수 있는 catch 블록을 위해서 아래로 찾아내려 갑니다. 적절한 catch 블록을 찾게 되면, 해당 catch 블록이 실행되면서 예외의 처리는 완료가 됩니다.
#include <iostream> using namespace std; class AAA { public: void ShowYou() { cout << "AAA exception" << endl; } }; class BBB : public AAA { public: void ShowYou() { cout << "BBB exception <<endl"; } }; class CCC : public BBB { public: void ShowYou() { cout << "CCC exception" << endl; } }; void ExceptionGenerator(int expn) { if (expn == 1) throw AAA(); else if (expn == 2) throw BBB(); else throw CCC(); } int main(void) { try { ExceptionGenerator(3); ExceptionGenerator(2); ExceptionGenerator(1); } catch (AAA& expn) { cout << "catch(AAA& expn)" << endl; expn.ShowYou(); } catch (BBB& expn) { cout << "catch(BBB& expn)" << endl; expn.ShowYou(); } catch (CCC& expn) { cout << "catch(CCC& expn)" << endl; expn.ShowYou(); } return 0; }
|
예외 클래스가 상속의 관계로 묶여 있습니다. 따라서 BBB 객체도 CCC 객체도 AAA 객체의 일종으로 인식됩니다. 예외가 발생할 수 밖에 업슨 구조이기 때문에, 주석처리를 통해서 한 문장씩 실행해서 결과를 확인해야 합니다. 예외에 적절한 catch 블록은 나열 순서대로 찾게 됩니다. 따라서 어떠한 예외가 발생하건 40행의 catch 블록을 대상으로 제일 먼저 적합성 여부를 판단하게 됩니다.
예외처리와 관련된 또 다른 특성들
new 연산자에 의해서 발생하는 예외입니다.
#include <iostream> #include <new> using namespace std; int main(void) { int num = 0; try { while (1) { num++; cout << num << "th alloc try" << endl; new int[10000][10000]; } } catch (bad_alloc &bad) { cout << bad.what() << endl; cout << "more no" << endl; } return 0; } |
메모리 할당관련 예외의 발생을 위해서 반복해서 메모리 공간을 할당만 하고 있습니다. bad_alloc 예외의 발생을 확인하기 위한 catch 블록입니다. what 함수는 예외의 원인정보를 문자열의 형태로 반환하는데, 이 때 반환되는 문자열의 내용은 컴파일러에 따라서 달라집니다.
예외 던지기
catch 블록에 전달된 예외는 다시 던져질 수 있습니다. 그리고 이로 인해서 하나의 예외가 둘 이상의 catch 블록에 의해서 처리되게 할 수 있습니다.
#include <iostream> using namespace std; void Divide(int num1, int num2) { try { if (num2 == 0) throw 0; cout << "ans: " << num1 / num2 << endl; cout << "remaining: " << 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; } |
catch 블록으로 전달된 예외가 소멸되지 않고, 다시 던져집니다. 따라서 이 함수를 호출한 영역으로 예외가 전달됩니다. Divide 함수에 의해서 다시 전달된 예외가 이 catch 블록으로 전달됩니다.