013. 예외처리(Exception Handling)

I. 기존의 예외처리 방식

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

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

 

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

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

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

 

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

  • try

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

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

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

    • 예외 상황이 발생 하였음을 알릴때 사용
  1. try
  2. {
  3. if(예외 상황 발생)
  4. throw ex;
  5. }
  6. catch(exception ex)
  7. {
  8. 예외 상황 처리
  9. }

 

  •  예외 처리 메커니즘의 적용
  1. #include <iostream>
  2. using namespace std;
  3. 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;
    }
  • 위와 같이 하면 된다.

 

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

 

III. Stack Unwinding(스택 풀기)

  • 전달되는 예외
  1.  #include <iostream>
  2. using namespace std;
  3. int divide(int a, int b);
  4. 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;
    }
  5. int divide(int a, int b)
    {
     if(b==0)
      throw b;
     return a/b;
    }
  • 처리되지 않은 예외는 전달된다는 것이다.

    • 이러한 현상을 가리켜 스택 unwinding이라고 한다.
  1.  #include <iostream>
  2. using namespace std;
  3. void fct1();
    void fct2();
    void fct3();
  4. int main()
    {
     try
     {
      fct1();
     }
     catch(int ex)
     {
      cout<<"예외 : "<<ex<<endl;
     }
     return 0;
    }
  5. void fct1()
    {
     fct2();
    }
  6. void fct2()
    {
     fct3();
    }
  7. void fct3()
    {
     throw 100;
    }
  • 예외가 전달되는 과정이 함수의 스택이 풀리는 순서와 일치하기 때문에 스택 unwinding 이라고 한다.

 

  • 처리되지 않은 예외
  1.  #include <iostream>
  2. using namespace std;
  3. int divide(int a, int b);
  4. int main()
    {
     int a,b;
     cout<<"입력 : ";
     cin>>a>>b;
     cout<<"a/b의 몫 : "<<divide(a,b)<<endl;
     return 0;
    }
  5. int divide(int a, int b)
    {
     if(b==0)
      throw b;
     return a/b;
    }
  • 위와 같은 소스를 실행 시켜서 오류를 나게 해보자

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

  3. int main()
    {
     abort();
     cout<<"end"<<endl;
     return 0;
    }
  • 위와 같은 소스를 실행 시키면 오류메시지를 띄우면서 cout<<"end"<<endl; 위에서 종료가 된다.
  1. #include <iostream>
  2. using namespace std;
  3. int divide(int a,int b);
  4. int main()
    {
     int a, b;
  5.  cout<<"두개 입력 : ";
     cin>>a>>b;
  6.  try
     {
      cout<<"a/b의 몫 : "<<divide(a,b)<<endl;
     }
     catch (char exception)
     {
      cout<<exception<<" 입력."<<endl;
      cout<<"입력 오류!"<<endl;
     }
     return 0;
    }
  7. int divide(int a,int b)
    {
     if(b==0)
      throw b;
     return a/b;
    }
  • 위의 소스를 보면 예외를 처리해 주는 부분이 있다.

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

 

  • 전달되는 예외 명시하기

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

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

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

 

  • 하나의 try 블록과 여러개의 catch 블록
  1.  #include <iostream>
  2. using namespace std;
  3. int main()
    {
     int num;
     cout<<" input : ";
     cin>> num;
  4.  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 블록 사이에 다른 문장이 존재할 수 없다.
  1.  #include <iostream>
  2. using namespace std;
  3. char* account="1234-4576";
    int sid = 1122;
    int balance = 1000;
  4. class AccountExpt
    {
     char acc[10];
     int sid;
  5. public:
     AccountExpt(char* str,int id)
     {
      strcpy(acc,str);
      sid=id;
     }
     void What()
     {
      cout<<"계좌 : "<<acc<<endl;
      cout<<"비번 : "<<sid<<endl;
     }
  6. };
  7. int main()
    {
     char acc[10];
     int id;
     int money;
  8.  cout<<"계좌번호 입력 :";
     cin >> acc;
     cout<<"비밀번호 입력 : ";
     cin >> id;
     if (strcmp(account,acc)||sid!=id)
      throw AccountExpt(acc,id);
  9.  cout<<"출금액 입력 : ";
     cin>>money;
  10.  if(balance<money)
      throw money;
     balance -=money;
     cout<<"잔액 :"<<balance<<endl;
     return 0; 
    }
  • 위의 소스는 적절하게 예외처리를 안한 상태이다.
  1. #include <iostream>
  2. using namespace std;
  3. char* account="1234-4576";
    int sid = 1122;
    int balance = 1000;
  4. class AccountExpt
    {
     char acc[10];
     int sid;
  5. public:
     AccountExpt(char* str,int id)
     {
      strcpy(acc,str);
      sid=id;
     }
     void What()
     {
      cout<<"계좌 : "<<acc<<endl;
      cout<<"비번 : "<<sid<<endl;
     }
  6. };
  7. int main()
    {
     char acc[10];
     int id;
     int money;
  8.  try
     {
      cout<<"계좌번호 입력 :";
      cin >> acc;
      cout<<"비밀번호 입력 : ";
      cin >> id;
      if (strcmp(account,acc)||sid!=id)
       throw AccountExpt(acc,id);
     }
     catch(AccountExpt& expt)
     {
      cout<<"다시 입력을 확인하세요"<<endl;
      expt.What();
     }
  9.  try
     {
  10.   cout<<"출금액 입력 : ";
      cin>>money;
  11.   if(balance<money)
       throw money;
      balance -=money;
      cout<<"잔액 :"<<balance<<endl;
     }
     catch(int money)
     {
      cout<<"부족 금액 :"<<money-balance<<endl;
     }
  12.  return 0; 
    }
  • 실행시켜 보면 알겠지만 예외처리는 해주었다고 하지만 엉망이다.

    • 참고로 catch(AccountExpt& expt) 이렇게 한 이유는 객체가 복사되는 부담을 줄이기 위한 것이기 때문에 꼭 이렇게 할 필요는 없다.
  1.  #include <iostream>
  2. using namespace std;
  3. char* account="1234-4576";
    int sid = 1122;
    int balance = 1000;
  4. class AccountExpt
    {
     char acc[10];
     int sid;
  5. public:
     AccountExpt(char* str,int id)
     {
      strcpy(acc,str);
      sid=id;
     }
     void What()
     {
      cout<<"계좌 : "<<acc<<endl;
      cout<<"비번 : "<<sid<<endl;
     }
  6. };
  7. int main()
    {
     char acc[10];
     int id;
     int money;
  8.  try
     {
      cout<<"계좌번호 입력 :";
      cin >> acc;
      cout<<"비밀번호 입력 : ";
      cin >> id;
      if (strcmp(account,acc)||sid!=id)
       throw AccountExpt(acc,id);
      cout<<"출금액 입력 : ";
      cin>>money;
  9.   if(balance<money)
       throw money;
      balance -=money;
      cout<<"잔액 :"<<balance<<endl;
     }
     catch(AccountExpt& expt)
     {
      cout<<"다시 입력을 확인하세요"<<endl;
      expt.What();
     }
     catch(int money)
     {
      cout<<"부족 금액 :"<<money-balance<<endl;
     }
  10.  return 0; 
    }
  • 위와 같이 try 문에 다 집어 넣어야 정상적으로 된다.

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

 

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

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

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

 

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

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

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

 

  • 상속 관계에 있는 예외 객체의 전달
  1.  #include <iostream>
  2. using namespace std;
  3. class ExceptA
    {
    public:
     void What()
     {
      cout<<"ExceptA 예외"<<endl;
     }
    };
  4. class ExceptB:public ExceptA
    {
    public:
     void What()
     {
      cout<<"ExceptB 예외"<<endl;
     }
    };
  5. class ExceptC : public ExceptB
    {
    public:
     void What()
     {
      cout<<"ExceptC 예외"<<endl;
     }
    };
  6. void ExceptFunction(int ex)
    {
     if(ex == 1)
      throw ExceptA();
     else if(ex == 2)
      throw ExceptB();
     else
      throw ExceptC();
    }
  7. 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로만 찍힌다.
    • 다음과 같이 바꾸어 보자.
  1.  #include <iostream>
  2. using namespace std;
  3. class ExceptA
    {
    public:
     void What()
     {
      cout<<"ExceptA 예외"<<endl;
     }
    };
  4. class ExceptB:public ExceptA
    {
    public:
     void What()
     {
      cout<<"ExceptB 예외"<<endl;
     }
    };
  5. class ExceptC : public ExceptB
    {
    public:
     void What()
     {
      cout<<"ExceptC 예외"<<endl;
     }
    };
  6. void ExceptFunction(int ex)
    {
     if(ex == 1)
      throw ExceptA();
     else if(ex == 2)
      throw ExceptB();
     else
      throw ExceptC();
    }
  7. 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을 참조하는게 좋다.
  1.  #include <iostream>
    #include <new>
  2. using namespace std;
  3. int main()
    {
     try{
      int i=0;
      while(1)
      {
       cout<<i++<<"번째 할당"<<endl;
       double(*arr)[10000] = new double[10000][10000];
      }
  4.  }
     catch(bad_alloc ex)
     {
      ex.what();
      cout<<endl<<"End"<<endl;
     }
     return 0;
    }
  • 위의 소스를 실행 시켜 보면 무한루프를 돌면서 메모리 공간만 할당하고 있다.

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

 

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

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

 

  • 예외 다시 던지기
  1.  #include <iostream>
  2. using namespace std;
  3. class Exception
    {
    public:
     void what()
     {
      cout<<"Simple Exception"<<endl;
     }
    };
  4. void ThrowException()
    {
     try
     {
      throw Exception();
     }
     catch(Exception& t)
     {
      t.what();
      throw;
     }
    }
  5. int main()
    {
     try
     {
      ThrowException();
     }
     catch(Exception& t)
     {
      t.what();
     }
     return 0;
    }
  • 먼저 ThrowException()에서 예외를 처리하고 catch에서 throw로 예외를 던졌다.

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

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

이 글은 스프링노트에서 작성되었습니다.

'2. C/C++ > 02. C++' 카테고리의 다른 글

012. 템플릿(Template)  (0) 2008.12.07
011. string 클래스 디자인  (0) 2008.12.07
010. 연산자 오버로딩  (0) 2008.12.07
009. virtual의 원리와 다중 상속  (0) 2008.12.07
008. 상속과 다형성  (0) 2008.12.07
Posted by kid1412
,