ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 예외 처리
    플밍/C++ (overview) 2012. 1. 3. 23:20
    2006/09/01 02:47 


    * 프로그래머는 일반적인 상황과 예외적인 상황을 모두 고려해서 프로그램을 디자인 해야 한다.

    사용자는 프로그래머의 입장에서 프로그램을 사용하진 않는다.

     

    ========================================================================================

     

    기존의 예외 처리

     

    기초적인 예외로 divide by 0 가 있다.

    기존엔 이런 예외 상황을 처리 하는 방법으로,

    0으로 나누게 되느냐 아니냐를 직접 기술해서 예외를 방지했다.

     

    예를 들면..

     

    if(a == 0)

           cout << "Error : divide by 0" << endl;

    else

           cout << b/a << endl;

     

    이런 식이다. 그러나 이런식의 프로그램은 예외처리를 위한 코드 부분과 일반 코드부분을

    명확히 구분짓지 못한다.

     

    두가지 상황을 구분 지을수 있는 메커니즘이 없다는 것은,

    프로그램을 작성하는 프로그래머나 유지보수를 해야 하는 엔지니어들에게도 많은 불편이 따른다. 그만큼 프로그램이 복잡해지기 때문이다.

     

    * 프로그램의 성격에 따라, 성능을 극도로 중시한다면 예외처리 메커니즘보다 이런식의 방법이

    쓰이기도 한다.

    ========================================================================================

     

    C++의 예외 처리 메커니즘

     

    C++에서는 예외처리를 위한 메커니즘을 제공하는데, 이는 코드 가독성을 높이고, 이후 유지보수

    에도 많은 도움을 준다. 예외 상황 처리를 위한 코드부분을 독립시키는것이 가능하기 때문이다.

     

    1. 기본적인 예외 처리 메커니즘

     

    try{

           예외 발생 예상 지역;

    }

    catch(처리되어야 할 예외 종류){

           예외 처리 방법;

    }

     

    try 블록 내에서 발생하는 예외는 바로 뒤따라 나오는 catch 블록에 의해 처리된다 는 뜻이다.

     

    throw : 예외 발생을 알린다.

     

    try{

          if(예외 발생)

                throw ex;

    }

    catch(exception ex){

          예외 상황 처리;

    }

     

    throw는 직접적으로 예외가 생겼다고 신고하는 격이다. 신고가 들어오면 곧장 catch블럭이

    처리한다.

     

    ex는 변수, 상수, 객체 등등 아무거나 될수 있다.

    (객체일 경우엔 생성자 형태를 써줘야 한다. ex : throw AAA() )

    다만, 전달되는 예외와 이를 받아줄 변수의 자료형은 일치해야 한다.

    즉, ex가 int형이라면, catch문에서의 ex도 int형이어야 한다.

     

    (throw와 catch는 함수 호출과 거의 흡사하다. catch블록 내의 변수들은 마찬가지로 스택공간에

    할당된다.)

     

    ========================================================================================

     

    Stack Unwinding(스택 풀기)

     

    1. 예외는 전달되어~

     

    main(){

           try{

                  divide(20,0);

           }

           catch(int exception){

                  ...

           }

    }

     

    void divide(int a, int b){

           if(b==0)

                  throw b;

           cout << a/b<<endl;

    }

     

    이런 문장이 있을때, try안에서 divide를 호출한다. 그런데 그 함수 내에서 0으로 나누는 예외가

    발생한다. 그럼 예외를 처리하는 catch블록으로 가야 하는데, divide함수 내에는 예외처리를

    하는부분이 존재하지 않는다. 그럼 어떡하지? 호출된 함수 내부에 예외처리를 위한 구문이

    존재하지 않을때는 다음과 같이 진행된다.

    예외가 발생했으므로(throw b) 곧장 자신(divide함수)은 스택에서 해제 되면서,

    자신을 호출한 영역으로 예외를 전달해준다.

    그러면 다시 try블럭내로 돌아왔다. 여전히 catch블럭을 찾고 있는데, 마침 여기는 있군..

    이리하여 예외가 처리되었다.

     

    이렇게 예외가 함수를 호출한 부분으로 전달되는 것을 가리켜 "스택풀기"라고 부른다.

    전달된다는것은 거기서 함수는 제 할일을 다 했으며, 그리하여 스택에서 해제되기 때문이다.

     

    main(){

          try{

                fct1();

          }

          catch(int ex){

                ...

          }

    }

     

    void fct1(){

          fct2();

    }

     

    void fct2(){

          fct3();

    }

     

    void fct3(){

          throw 100;

    }

     

    이와 같이 꼬리에 꼬리를 무는 함수호출에서도 마찬가지이다.

     

    main->fct1->fct2->fct3, 예외 발생! catch문 없다! ->fct2로 예외(100)전달, catch문 없다! ->

    fct1로 예외(100)전달, catch문 없다! -> main으로 예외(100)전달, catch문 있다! -> catch블록실행

     

    이런식으로 전달에 전달을 하다보면, 꼬리에서부터 함수는 해제가 되어서 이를가리켜

    스택풀기라고 한다. (예외가 전달되는 방향이 스택이 해제되는 방향과 일치한다.)

     

    2. 처리되지 않은 오류

     

    main(){

           int a=0;

           int b;

           if(a==0){

                  throw a;

           }

           cout << "abc" ;

    }

     

    이렇게 예외를 던지는 문장은 있는데, 받는 문장(catch)은 없을때는 어떻게 될까?

    이것은 컴파일 오류가 아니다. 그리고, 에러메시지를 출력하고 프로그램은 종료된다.

    그러므로 그 다음 문장인 cout << "abc"; 는 실행되지 않는다.

     

    자세히 들여다보면, 이는 abort()함수가 보이지 않게 호출되었기 때문인데, 이 함수는 사용자가

    직접 쓸수도 있다.

     

    abort();라고 그냥 쓰면 되고, 그 이후 문장들은 실행되지 않는다.

     

    (stdlib.h 에 정의되어있다.)

     

    예외를 던졌는데, 받는 catch가 없으면 abort()가 호출된다고 했다.

    catch문이 전혀 없을때, 그리고 있긴 있는데 같은 종류의 exception을 받는 catch없을때 역시

    마찬가지이다.

     

    throw (int) a;

    했는데, catch(int ex)가 없고 catch(char ex)... 같은것만 있다면 역시나 abort()가 호출된다.

     

    3. 전달되는 예외 명시하기

     

    어떤 함수내에서 무슨무슨 예외가 전달될것이라는것을 미리 명시해주는것을 말한다.

    무엇보다도 가독성을 높이는데 도움을 준다.

     

    void func() throw (자료형, ..., 자료형){}

     

    과 같이 쓰면 된다.

    void func() throw (int){}

    void func() throw (char){}

    void func() throw (double, char*){}

    .

    .

    .

    그리고 함수 내에서 괄호안에 명시된 자료형이 아닌 타입의 예외를 던지면 abort()가 호출되어

    종료된다.

     

    그렇다면,

    void func() throw (){}  이건 뭘 의미하는걸까?

    자료형이 비어있다! 이것은 아무 예외나 던질수 있다는게 아니라, 아무런 예외도 전달하지 않는다

    는것을 의미한다. 즉, 이 함수내에서 throw문이 등장하면 abort()가 호출되면서 종료된다.

     

    4. 하나의 try블록과 여러개의 catch블록

     

    try{

          ...

    }

    catch(자료형1){

          ...

    }

    catch(자료형2){

          ...

    }

    .

    .

    .

    catch(자료형n){

          ...

    }

     

    try와 catch는 굳이 1대1로 쓰이지 않아도 된다. (쓸데없이 프로그램이 길어진다.-_-)

     

    (try블록의 범위를 어떻게 지정해줘야 하는가는 프로그램의 업무적 성격(business logic)

    에 따라 달라지는 것이다.)

     

    class StuExcep{

           StuExcep(...){

                  ...

           }

    };

     

    class Student{

           ...

    };

     

    main(){

           try{

                  Student a;

                  if(..)

                         throw StuExcep(...);

           }

           catch(StuExcep se){    

                  ...

           }

    }

     

    이렇게 예외를 위한 클래스를 따로 만들었다.

    이렇게 따로 예외를 위한 객체를 사용한 이유는, 예외 상황이 발생한 원인에 대한 정보를

    보다 자세히(정확히) 담을수 있기 때문이다.

     

    StuExcep를 예외 클래스라고 하고, 이를 통해 생성된 객체를 예외 객체라고 부른다.       

     

    ========================================================================================

     

    예외를 나타내는 클래스와 상속

     

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

     

    이전에 catch가 함수오버로딩과 유사하다고 한적이 있는데, 한가지 차이점이 있다.

    함수오버로딩은 딱 봐서 호출할 함수에 바로 찾아가지만, catch는 첫번째 catch블록부터

    순차적으로 뒤져가면서 해당되는 블록을 수행한다.

     

    즉, 첫번째 catch에서 예외를 전달받을수 없는 상황(자료형 불일치)이라면 두번째 catch로

    가서 살펴보고, 거기서도 아니면 세번째로 간다..

     

    2. 상속 관계에 있는 예외 객체의 전달

     

    class AAA{

    };

     

    class BBB : public AAA{

    };

     

    class CCC : public BBB{

    };

     

    void fun(int x){

           if(x==1)

                  throw AAA();

           else if(x==2)

                  throw BBB();

           else

                  throw CCC();

    }

     

    main(){

           int a;

           cin >> a ;

           try{

                  fun(a);

           }      

           catch(AAA aa){      

                  ...

           }      

           catch(BBB bb){

                  ...

           }

           catch(CCC cc){

                  ...

           }

    }

     

    이와 같은 상속 관계에서 실행시켜보면, 1, 2, 3 모두 첫번째 catch블록에서 해결되고

    있는걸 볼수 있다.

    왜? BBB, CCC는 동시에 AAA도 되기 때문이다.

    제 목적에 맞도록 짜기 위해서는 catch블록의 순서를 역순으로 해야 한다.

     

    (적합한 catch블록을 찾기위해 첫번째부터 순서대로 하기때문에, 이는 is-a관계와도 같은

    방향이기때문에 첫번째 블록에서 다 잡혀버린다. 그렇기 때문에 is-a관계와 반대방향으로

    catch블록들을 나열하면 1은 AAA에서, 2는 BBB에서, 3은 CCC에서 처리된다.)

     

    ========================================================================================

     

    기타

     

    1. new 연산자에 의해 전달되는 예외

     

    new는 동적할당 연산자로, 적합한 공간을 찾아 할당을 하지 못했을 경우엔 예외를 발생시킨다.

     

    new에 실패했을때는 NULL포인터를 반환한다. 사실 이것은 과거 C++표준이고,

    오늘날의 C++표준은 헤더파일 new에 있는 bad_alloc 예외가 전달된다.

     

    try{

          동적할당..

    }

    catch(bad_alloc ex){

          ...

    }

     

    이렇게 활용하면 되겠다.

     

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

     

    try{

          ;

    }

    catch(...){

          ;

    }

     

    이는 어떤 형태의 예외든 상관없이 모두 처리하겠다는 뜻이다.

    하지만, 실용적인 형태는 아니다. 예외 형태에 따라 각기 다른 처리방법이 쓰일때가 대부분이기

    때문이다.

     

     

     

     

     

    void func(){

          try{

                throw ex;

          }

          catch(..){

                ;

                throw;

          }

    }

     

    main(){

          try{

                func();

          }

          catch(..){

                ;

          }

    }

     

    우선 흐름을 따라가보자.

     

    main에서 func()를 호출 -> func()내에서 예외 발생 -> func()내의 catch에서 처리

    -> func()내의 catch에서 다시 throw -> (함수가 호출된 영역으로 예외를 던짐) ->

    main()의 catch에서 그 예외를 잡아서 처리

     

    이처럼 굵은 throw; 는 예외를 다시 던지겠다는 뜻이다. 자주 사용되는 것은 아니지만,

    다음과 같은 상황에선 이렇게 예외를 두번 던지게 될 경우가 있다.

     

     

    a) catch 블록에 의해 예외를 잡고 보니, 처리하기가 마땅치 않다.

        다른 catch 블록에 의해서 예외가 처리되길 바란다.

     

    b) 하나의 예외에 대해 처리되어야 할 영역(예외가 발생했음을 알려 줘야 할 영역)이 둘 이상이다.

    '플밍 > C++ (overview)' 카테고리의 다른 글

    템플릿(template)  (0) 2012.01.03
    string 클래스 디자인  (0) 2012.01.03
    연산자 오버로딩  (0) 2012.01.03
    virtual 응용(원리, 다중상속)  (0) 2012.01.03
    상속(심화), 두종류binding, virtual  (0) 2012.01.03
Designed by Tistory.