윤성우의 열혈 c++) Chapter 15. 예외처리
try catch throw
- if 문이 아닌 예외처리문으로 예외처리 하는 이유는 if 문으로 예외처리시 예외처리를 위한 코드와 프로그램의 흐름을 구성하는 코드를 쉽게 구분하기 어렵기 때문이다. 즉 예외처리문만 봐도 이 코드는 예외처리를 위한 코드인것을 판단하기 쉬워진다. (if문은 예외처리 이외에도 많이 쓰이기 때문)
try : 예외 발견
try문 안에서 if문으로 예외처리를 한다.
throw: 예외 발생 알림 (던짐)
예외가 되는 변수 혹은 객체를 throw 한다.
이렇게 throw한 변수 혹은 객체를 catch 문이 받게된다.
catch: 예외를 잡는다
전달받은 예외를 처리한다
- 예외가 발생하면 (throw절이 실행되면) 프로그램의 흐름이 중지되고, catch 블록에 의해 예외 처리과정을 거친다.
#include <iostream>
using namespace std;
int main()
{
int num1, num2;
cout << "enter two numbers: ";
cin >> num1 >> num2;
try
{
if(num2 == 0) throw num2;
cout << "니눗셈의 몫: " << num1 / num2 << endl;
cout << "나눗셈의 나머지: " << num1 % num2 << endl;
}
catch(int expn)
{
cout << "제수는 " << expn << "이 될 수 없습니다" << endl;
}
cout << "end of main" << endl;
}
- 예외가 발생하지 않는다면 13, 14 줄이 실행되고, 예외가 발생한다면 13,14줄은 실행되지 않고 catch 문으로 넘어간다.
- try 블록 내에서 예외가 발생하면 catch 블록이 실행되고 나서, 예외가 발생한 지점 이후를 실행하는 것이 아닌 catch 블록의 이후가 실행된다.
예외가 발생했는데 try~catch문으로 처리하지 않을 경우
만약 함수 내에서 throw 절에 의해 예외가 발생했는데 try~catch문으로 처리하지 않으면 발생한 예외는 해당 함수를 호출한 영역으로 전달되고, 예외 처리에 대한 책임도 해당 영역으로 전달된다.
#include <iostream>
#include <cmath>
#include <cstring>
using namespace std;
// string to integer
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()
{
char str1[100];
char str2[200];
while(true)
{
cout << "두 개의 숫자 입력 ";
cin >> str1 >> str2;
try
{
// StoI 함수에서 throw절에 의해 예외 발생시 이 영역으로 예외가 옮겨지고 처리됨
int res = StoI(str1) + StoI(str2);
cout << str1 << " + " << str2 << " = " << res << endl;
// 예외가 발생하지 않으면 break
break;
}
// 예외가 발생했다면 catch문 실행
catch(char c)
{
cout << "문자 " << c << "가 입력되었습니다" << endl;
cout << "재입력 진행합니다" << endl << endl;
}
}
cout << "end of program" << endl;
}
Stoi 함수내의 throw절에서 예외가 발생하면 해당 함수내에 try~catch 문이 없기 때문에 함수가 호출된 영역으로 예외 데이터와 예외 처리의 책임이 옮겨진다.
예외가 발생하지 않으면 36줄의 break문에 의해 while 문을 빠져나가고, 예외가 발생하면 catch 문이 실행되 재입력을 진행한다.
또한 이렇게 함수내의 throw 문에 의하여 예외 데이터를 전달하면, return문 처럼 해당 함수는 더이상 실행되지 않고 종료된다.
Stack Unwinding (스택풀기)
위 처럼 예외가 발생했는데 예외를 처리하지 않아서 함수를 호출한 영역으로 예외 데이터가 전달되는것을 Stack Unwinding이라고 부른다. 왜 Stack Unwinding (스택 풀기)일까?
함수 내에서 예외가 발생했는데 예외 처리할 try~catch문이 없으면 throw절은 함수가 호출된 영역으로 예외 데이터를 전달한다고 하였다.
또한 이 경우 return문과 동일하게 해당 함수는 더이상 실행되지 않고 종료된다고 하였다.
따라서 함수가 종료되었으므로 해당 함수의 스택은 반환된다. (함수 실행시 함수는 스택영역에 쌓인다)
따라서 예외 데이터의 전달을 Stack Unwinding 이라고 한다.
만약 함수에 예외 처리부분이 없어 예외 데이터가 메인함수에 전달되었는데, 메인함수에도 예외 처리를 하는 try~catch문이 없다면 terminate (프로그램을 종료시키는 함수) 함수가 호출되 프로그램이 종료된다.
하나의 try 블록과 다수의 catch 블록
하나의 try 블록내에서 여러가지 예외 상황이 발생할수 있다. (여러가지 자료형이 throw 될수 있다).
이런 경우 try 블록 뒤의 catch 블록을 여러개 정의 할 수 있다.
#include <iostream>
#include <cmath>
#include <cstring>
using namespace std;
// string to integer
int StoI(char *str)
{
int len = strlen(str);
int num = 0;
// int형 throw
if(len != 0 && str[0] == '0') throw 0;
for(int i = 0; i < len; i++)
{
// char 형 throw
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()
{
char str1[100];
char str2[200];
while(true)
{
cout << "두 개의 숫자 입력 ";
cin >> str1 >> str2;
try
{
int res = StoI(str1) + StoI(str2);
cout << str1 << " + " << str2 << " = " << res << endl;
break;
}
// char형 catch
catch(char c)
{
cout << "문자 " << c << "가 입력되었습니다" << endl;
cout << "재입력 진행합니다" << endl << endl;
}
// int형 catch
catch(int expn)
{
if(expn == 0) cout << "0으로 시작되는 숫자는 입력불가" << endl;
else cout << "비정상적 입력이 이루어졌습니다" << endl;
cout << "재입력 진행" << endl;
}
}
cout << "end of program" << endl;
}
41,47 줄을 보면 각각 char형, int형을 catch해서 예외 처리하고 있다.
예외 클래스, 상속 관계에 있는 예외 클래스
클래스의 객체도 예외 데이터가 될수 있고, 예외 객체 생성을 위해 정의된 클래스를 예외 클래스라고 부른다.
또한 예외 클래스도 상속 관계를 구성할수 있다.
#include <iostream>
using namespace std;
class AccountException
{
public:
virtual void ShowExceptionReason() = 0; // 순수 가상 함수
};
// 입금 예외, AccountException 클래스 상속함
class DepositException : public AccountException
{
private:
int reqDep;
public:
DepositException(int money) : reqDep(money) {}
void ShowExceptionReason() override
{
cout << "[에외 메시지: " << reqDep << "는 입금불가]" << endl;
}
};
// 출금 예외, AccountException 클래스 상속함
class WithdrawException : public AccountException
{
private:
int balance;
public:
WithdrawException(int money) : balance(money) {}
void ShowExceptionReason()
{
cout << "[에외 메시지: 잔액 " << balance << " 잔액부족]" << endl;
}
};
class Account
{
private:
char accNum[50];
int balance;
public:
Account(char *acc, int money) : balance(money)
{
strcpy(accNum, acc);
}
// AccountException형 객체 throw 한다
// DepositException 클래스는 AccountException 클래스를 상속받았기에 가능
void Deposit(int money) throw (AccountException)
{
if(money < 0)
{
// 예외객체는 c++의 예외처리 메커니즘에 의해 처리되기 때문에 코드상에서 이를 직접 참조할 필요 없음
// 따라서 예외 객체는 보통 임시객체로 생성한다.
throw DepositException(money);
}
balance += money;
}
// 마찬가지로 AccountException형 객체 throw
void Withdraw(int money) throw (AccountException)
{
if(money > balance)
throw WithdrawException(balance);
balance -= money;
}
void ShowMyMoney()
{
cout << "잔고: " << balance << endl << endl;
}
};
int main()
{
Account myAcc("123-123", 5000);
try
{
myAcc.Deposit(2000);
myAcc.Deposit(-300);
}
// 여기서 실제 전달되는 예외객체는 DepositException 혹은 WithdrawException 이지만
// 해당 클래스는 AccountException 클래스를 상속받았기 때문에 이와 같이 표현 가능하다
catch (AccountException &expn)
{
expn.ShowExceptionReason();
}
myAcc.ShowMyMoney();
}