[열혈 C++] Chap15: 예외처리
1. 예외상황과 예외처리 이해
✏️예외상황을 처리하지 않았을 때의 결과
-
예외 : 프로그램의 논리에 맞지 않는 상황
-
예외를 제대로 처리하지 않았을 때, 예외가 발생하면 그냥 프로그램이 종료되어 버린다.
-
if문을 사용해 예외처리를 할 수 있지만, 예외가 발생하는 위치와 예외가 발견되는 위치가 다를 수도 있다.
따라서 if문을 예외처리를 위한 코드로 쓰기엔 직관적이지도 않고, 주석이 있다고 해도 프로그램 흐름을 이해하는 데 협조적이지는 않다.
2. C++ 예외처리 메커니즘
- C++은 구조적으로 예외를 처리할 수 있는 메커니즘을 제공한다. 이를 통해 코드의 가독성과 유지보수성을 높일 수 있다.
✏️try, catch 그리고 throw
try : 예외 발견
catch : 예외 처리
throw : 예외 발생
- try
- 예외가 발생할 가능성이 있는 부분
- catch
- try에서 발생한 예외를 처리하는 부분
- throw
- 예외가 발생했음을 알리는 문장의 구성에 사용된다
-
throw expn;
을 보고 예외라고 표현한다. expn은 변수, 상수, 객체 등 표현 가능한 예외상황 데이터가 될 수 있다.try{ //일단 수행해보는데 예외가 발생할 수 있는 코드 //예외가 발생하면 throw expn; } catch(type expn){ //처리 방법 }
throw에 의해 던져진 예외 데이터는 try블록에 의해 감지되어 catch에서 처리된다
-
예제
#include <iostream> using namespace std; int main(void) { int num1, num2; cout<<"두 개의 숫자 입력: "; cin>>num1>>num2; try //try블록이 수행된다 { if(num2==0) //num2==0이면 참이되어 throw문을 수행하고 catch로 넘어간다 throw num2; //catch문에 num2가 전달된다 cout<<"나눗셈의 몫: "<< num1/num2 <<endl; cout<<"나눗셈의 나머지: "<< num1%num2 <<endl; } catch(int expn) { cout<<"제수는 "<<expn<<"이 될 수 없습니다."<<endl; cout<<"프로그램을 다시 실행하세요."<<endl; } cout<<"end of main"<<endl; return 0; }
✏️try 블록을 묶는 기준
-
예외가 발생할만한 영역과 관련된 모든 문장을 묶어 “work” 단위로 구성한다
-
try 블록 내에서 예외가 발생하면 예외가 발생한 지점 이후 나머지 try 영역은 건너뛰기 때문에 예외가 발생할 부분만 묶는 게 아니다.
-
예제
#include <iostream> using namespace std; int main(void) { int num1, num2; cout<<"두 개의 숫자 입력: "; cin>>num1>>num2; try //try블록이 수행된다 { if(num2==0) //num2==0이면 참이되어 throw문을 수행하고 catch로 넘어간다 throw num2; //catch문에 num2가 전달된다 } catch(int expn) { cout<<"제수는 "<<expn<<"이 될 수 없습니다."<<endl; cout<<"프로그램을 다시 실행하세요."<<endl; } cout<<"나눗셈의 몫: "<< num1/num2 <<endl; //이문장은 실행되면 안되는데 밖으로 빠져나와있어 예외처리 되고나서도 실행된다 cout<<"나눗셈의 나머지: "<< num1%num2 <<endl; cout<<"end of main"<<endl; return 0; }
3. Stack Unwinding 스택풀기
✏️예외전달
-
호출된 함수에서 throw절이 실행되면서 예외가 발생한 경우, 예외처리에 대한 책임은 이 함수를 호출한 영역으로 넘어간다
-
예제
#include <iostream> using namespace std; void Divide(int num1, int num2) { if(num2==0) throw num2; cout<<"나눗셈의 몫: "<< num1/num2 <<endl; cout<<"나눗셈의 나머지: "<< num1%num2 <<endl; } int main(void) { int num1, num2; cout<<"두 개의 숫자 입력: "; cin>>num1>>num2; try { Divide(num1, num2); cout<<"나눗셈을 마쳤습니다."<<endl; } catch(int expn) { cout<<"제수는 "<<expn<<"이 될 수 없습니다."<<endl; cout<<"프로그램을 다시 실행하세요."<<endl; } return 0; }
-
Divide()
에서 발생한 예외에 대한 try, catch문이 존재하지 않기 때문에 이 함수를 호출한 main에 예외처리의 책임이 예외데이터와 함께 넘어간다 -
함수 내에서 함수를 호출한 영역으로 예외 데이터를 전달하면, 해당 함수는 더 이상 실행되지 않고 종료된다
-
✏️스택풀기
- 예외가 처리되지 않아 함수를 호출한 영역으로 예외 데이터가 전달되는 현상
- 예제
#include <iostream> using namespace std; void SimpleFuncOne(void); void SimpleFuncTwo(void); void SimpleFuncThree(void); int main(void) { try { SimpleFuncOne(); } catch(int expn) { cout<<"예외코드: "<< 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; }
-
스택과 같이 함수가
main -> one -> two -> three
순으로 호출되었다가,
예외발생으로 예외데이터와 처리책임이three -> two -> one -> main
으로 전달된다. -
예외가 처리되지 않아 예외데이터가 main까지 도달했는데도 try, catch가 없으면
terminate()
함수가 호출되어 프로그램이 종료된다
-
✏️자료형이 일치하지 않아도
- 데이터는 전달된다.
- 그러나 자료형의 불일치로 인해 예외는 처리되지 않는다. 따라서 이 함수를 호출한 영역으로 예외데이터가 전달된다.
✏️try 한 개, 다수의 catch
-
자료형에 따라 맞는 catch문으로 예외데이터가 전달된다
#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<<"두 개의 숫자 입력: "; cin>>str1>>str2; try { cout<<str1<<" + "<<str2<<" = "<<StoI(str1)+StoI(str2)<<endl; break; } catch(char ch) { cout<<"문자 "<< ch <<"가 입력되었습니다."<<endl; cout<<"재입력 진행합니다."<<endl<<endl; } catch(int expn) { if(expn==0) cout<<"0으로 시작하는 숫자는 입력불가."<<endl; else cout<<"비정상적 입력이 이루어졌습니다."<<endl; cout<<"재입력 진행합니다."<<endl<<endl; } } cout<<"프로그램을 종료합니다."<<endl; return 0; }
✏️예외 명시
-
정의된 특정함수의 호출을 위해 함수의 이름, 매개변수, 반환형에 더해, 함수 내에서 전달될 수 있는 예외 종류와 상황도 명시해주는 것이 좋다.
int ThrowFunc(int num) throw(int, char){ ///코드 }
-
thorw() 비어있는 경우에는 어떠한 예외도 전달하지 않음을 의미하고, 따라서 이 함수가 예외를 전달할 경우 프로그램은 그냥 종료된다.
4. 예외 클래스 설계
예외객체: 예외 발생을 알리는데 사요오디는 객체 예외클래스: 예외객체 생성을 위해 정의된 클래스
- 예제
#include <iostream> #include <cstring> using namespace std; class DepositException { private: int reqDep; // 요청 입금액 public: DepositException(int money) : reqDep(money) { } void ShowExceptionReason() { cout<<"[예외 메시지: "<<reqDep<<"는 입금불가]"<<endl; } }; class WithdrawException { 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); } 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<<endl<<endl; } }; int main(void) { Account myAcc("56789-827120", 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; }
-
예외 클래스에도 상속될 수 있다. 코드가 좀 더 간단해진다
class AccountException { public: virtual void ShowExceptionReason() =0; }; class DepositException : public AccountException { private: int reqDep; // 요청 입금액 public: DepositException(int money) : reqDep(money) { } void ShowExceptionReason() { cout<<"[예외 메시지: "<<reqDep<<"는 입금불가]"<<endl; } }; 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); } void Deposit(int money) throw (AccountException) { if(money<0) { DepositException expn(money); throw expn; } balance+=money; } void Withdraw(int money) throw (AccountException) { if(money>balance) throw WithdrawException(balance); balance-=money; } void ShowMyMoney() { cout<<"잔고: "<<balance<<endl<<endl; } }; int main(void) { Account myAcc("56789-827120", 5000); try { myAcc.Deposit(2000); myAcc.Deposit(-300); } catch(AccountException &expn) { expn.ShowExceptionReason(); } myAcc.ShowMyMoney(); try { myAcc.Withdraw(3500); myAcc.Withdraw(4500); } catch(AccountException &expn) { expn.ShowExceptionReason(); } myAcc.ShowMyMoney(); return 0; }
5. 예외처리의 또다른 특성
- new 연산자에 의해 발생하는 예외
- new 연산에 의한 메모리 공간 할당이 실패하면
bad_alloc
이라는 예외가 발생한다.
- new 연산에 의한 메모리 공간 할당이 실패하면
- 모든 예외를 처리하는 catch블록
try{ }catch(...){ }
- 이렇게 catch블록을 선언하면 try에서 전달되는 모든 예외가 자료형 상관없이 걸려든다
- 예외 여러번 던지기
- catch블록에 던져진 예외는 다시 던져질 수 있다. 따라서 하나의 예외가 둘 이상의 catch블록에 의해 처리되게 할 수 있다