C++

C++ 예외 처리(Exception Handling) 총정리

김구티2 2024. 4. 23. 20:55

1. 예외 처리(Exception Handling)의 개념

C++에서 '예외'는 프로그램이 실행하는 동안 발생하는 런타임 이상이나 비정상적인 상태다. 이러한 예외를 처리하는 과정을 예외 처리라고 한다. 예외 처리 메커니즘을 사용하면 예외가 발생한 프로그램의 한 부분의 제어를 코드의 다른 부분으로 전달할 수 있다.

2. 예외의 유형

예외에는 2가지 유형이 존재한다.

① 동기: 입력 데이터의 오류로 인해 문제가 발생하거나, 프로그램이 현재 작업 중인 데이터 유형을 처리할 수 있는 장비가 없을 때 발생하는 예외다. ex) 숫자를 0으로 나누는 것

② 비동기: 디스크 고장, 키보드 인터럽트 등 프로그램이 통제할 수 없는 예외다.

3. 예외 처리를 위한 내장 기능

C++는 예외 처리를 위한 내장 기능을 제공한다. ①try ②catch ③throw 와 같은 전문 키워드를 사용하여 수행할 수 있다.

 

①try
try 키워드는 try 블록 내부에 위치한 예외를 던질 수 있는 코드 블록을 나타낸다. 그 다음에 하나 이상의 catch block이 뒤따른다. 예외가 발생하면 try block이 해당 예외를 던지게 된다.

②catch
catch 문은 특정 예외가 try 블록에서 던져질 때 실행되는 코드 블록을 나타낸다. 예외를 처리할 코드는 catch 블록 내부에 작성된다. 참고로, 여러 catch 문을 사용하여 try block에서 던진 다양한 유형의 예외를 잡을 수도 있다.

③throw 
throw 키워드를 사용하여서도 C++의 예외를 던질 수 있다. 프로그램은 throw 문이 발생하면 즉시 현재 함수를 종료하고, throw된 예외를 처리할 일치하는 캐치 블록을 찾기 시작한다.

try 및 catch 키워드는 쌍으로 제공된다. 우리는 일부 코드를 테스트하기 위해 try 블록을 사용하고 코드가 예외를 발생시키면 catch 블록에서 처리한다.

4. 예외 처리가 필요한 이유

기존 오류 처리에 비해 예외 처리가 갖는 주요 이점은 다음과 같다.

① 오류 처리 코드와 정상 코드의 분리 전통적인 오류 처리 코드에서 오류를 처리하기 위해서는 항상 if else 조건이 있다. 이러한 조건과 오류를 처리하는 코드는 정상 흐름과 혼합된다. 이로 인해 코드의 가독성과 유지보수성은 떨어진다. 그럴 때 Try/Catch 블록을 사용하면 오류 처리 코드가 정상 흐름과 분리되는 것이다.

② 함수/메소드는 그들이 선택한 예외만을 처리할 수 있다. 함수는 많은 예외를 던질 수 있지만, 일부 예외를 처리하도록 선택할 수도 있다. throw 되지만 catch는 되지 않는 다른 예외는 발신자가 처리할 수 있다. 만약 발신자가 그것들을 잡지 않기로 선택하면, 예외는 발신자의 발신자가 처리한다.

 

C++에서 함수는 throw 키워드를 사용하여 예외를 지정할 수 있다. 이 함수의 호출자는 예외를 어떤 방식으로든(다시 지정하거나 포착하여) 처리해야 한다.

③ 에러 타입 그룹화: C++에서는 기본 타입과 개체를 모두 예외로 던질 수 있다. 예외 객체의 계층 구조를 만들고 이름 공간이나 클래스의 예외를 그룹화한 다음 타입에 따라 분류할 수 있다.

5. 예외 처리의 예시

throw 예외 예시:

// C++ program to demonstate the use of try,catch and throw
// in exception handling.

#include <iostream>
#include <stdexcept>
using namespace std;

int main()
{

     // try block
     try {
          int numerator = 10;
          int denominator = 0;
          int res;

          // check if denominator is 0 then throw runtime
          // error.
          if (denominator == 0) {
               throw runtime_error(
                    "Division by zero not allowed!");
          }

          // calculate result if no exception occurs
          res = numerator / denominator;
          //[printing result after division
          cout << "Result after division: " << res << endl;
     }
     // catch block to catch the thrown exception
     catch (const exception& e) {
          // print the exception
          cout << "Exception " << e.what() << endl;
     }

     return 0;
}

 

출력:

Exception Division by zero not allowed!

 

try/catch 블록의 실행 흐름을 보여주는 예시:

// C++ program to demonstate the use of try,catch and throw
// in exception handling.

#include <iostream>
using namespace std;

int main()
{
     int x = -1;

     // Some code
     cout << "Before try \n";

     // try block
     try {
          cout << "Inside try \n";
          if (x < 0) {
               // throwing an exception
               throw x;
               cout << "After throw (Never executed) \n";
          }
     }

     // catch block
     catch (int x) {
          cout << "Exception Caught \n";
}

     cout << "After catch (Will be executed) \n";
     return 0;
}

 

출력:

Before try 
Inside try 
Exception Caught 
After catch (Will be executed) 

 

속성 ①

catch(…)라고 쓰여진 catch-all 블록이라는 특별한 캐치 블록이 있는데, 이 블록은 모든 종류의 예외를 잡는 데 사용될 수 있다.

 

catch(…)라고 쓰여진 'catch-all' 블록이라는 특별한 캐치 블록이 있는데, 이 블록은 모든 종류의 예외를 잡는 데 사용될 수 있다. 아래 프로그램에서는 예외적으로 int를 던지지만, int에 대한 catch 블록이 없으므로 catch(…) 블록이 실행된다:

// C++ program to demonstate the use of catch all
// in exception handling.

#include <iostream>
using namespace std;

int main()
{
     // try block
     try {

          // throw
          throw 10;
     }

     // catch block
     catch (char* excp) {
          cout << "Caught " << excp;
     }

     // catch all
     catch (...) {
          cout << "Default Exception\n";
     }
     return 0;
}

 

출력:

Default Exception

 

속성 ②

primitive 타입의 경우, 암시적 타입 변환이 발생하지 않는다.

 

다음 프로그램에서 'a'는 암묵적으로 int로 변환되지 않는 것을 볼 수 있다:

//// C++ program to demonstate property 2: Implicit type
/// conversion doesn't happen for primitive types.
// in exception handling.

#include <iostream>
using namespace std;

int main()
{
     try {
          throw 'a';
     }
     catch (int x) {
          cout << "Caught " << x;
     }
     catch (...) {
          cout << "Default Exception\n";
     }
     return 0;
}

 

출력:

Default Exception

 

속성 ③

만약 예외가 던져지고 아무 데에서도 잡히지 않으면, 프로그램이 비정상적으로 종료된다.

 

다음 프로그램에서 char가 던져지지만, char를 잡을 catch 블록이 없다:

// C++ program to demonstate property 3: If an exception is
// thrown and not caught anywhere, the program terminates
// abnormally in exception handling.

#include <iostream>
using namespace std;

int main()
{
     try {
          throw 'a';
     }
     catch (int x) {
          cout << "Caught ";
     }
     return 0;
}

 

출력:

terminate called after throwing an instance of 'char'

 

우리는 예상치 못한 기능을 작성함으로써 이러한 비정상적인 종료 행동을 변경할 수 있다.

참고로, base 클래스 예외 이전에 derived 클래스 예외를 잡아야 한다.

자바와 마찬가지로, C++ 라이브러리에는 모든 표준 예외의 기본 클래스인 표준 예외 클래스가 있다. 표준 라이브러리의 구성 요소가 던지는 모든 객체는 이 클래스에서 파생된다. 따라서 이 유형을 잡으면 모든 표준 예외를 잡을 수 있는 것이다.

속성 ④

자바와 달리, C++에서는 모든 예외가 선택 해제되어 있다. 즉, 컴파일러는 예외가 잡혔는지 아닌지를 확인하지 않는다는 것이다. 따라서 함수 선언에 잡히지 않은 예외를 모두 지정할 필요는 없다. 하지만 예외 처리는 그렇게 하는 것이 권장되는 방법이다.

다음 프로그램은 정상적으로 컴파일되지만, 이상적으로는 fun()의 시그니처는 확인되지 않은 예외를 나열해야 한다:

// C++ program to demonstate property 4 in exception
// handling.

#include <iostream>
using namespace std;

// This function signature is fine by the compiler, but not
// recommended. Ideally, the function should specify all
// uncaught exceptions and function signature should be
// "void fun(int *ptr, int x) throw (int *, int)"
void fun(int* ptr, int x)
{
     if (ptr == NULL)
          throw ptr;
     if (x == 0)
          throw x;
     /* Some functionality */
}

int main()
{
     try {
          fun(NULL, 0);
     }
     catch (...) {
        cout << "Caught exception from fun()";
     }
     return 0;
}

 

출력:

Caught exception from fun()

 

위의 코드를 더욱 나은 방식으로 쓰는 방법은 다음과 같다:

// C++ program to demonstate property 4 in better way

#include <iostream>
using namespace std;

// Here we specify the exceptions that this function
// throws.
void fun(int* ptr, int x) throw(
     int*, int) // Dynamic Exception specification
{
     if (ptr == NULL)
          throw ptr;
     if (x == 0)
          throw x;
     /* Some functionality */
}

int main()
{
     try {
          fun(NULL, 0);
     }
     catch (...) {
          cout << "Caught exception from fun()";
     }
     return 0;
}

 

출력:

Caught exception from fun()

 

※ 동적 예외 규격의 사용은 C++11 이후로 사용이 중단되었다. 이에 대한 이유 중 하나는.. 아마 프로그램을 무작위로 중단할 수 있기 때문일 수 있다. 동적 예외 규격에 언급되지 않은 다른 유형의 예외를 던졌을 때 이 문제가 발생할 수 있다. 해당 시나리오에서는 기본적으로 abort()를 호출하는 terminate()를 (*간접적으로) 호출하기 때문에 프로그램이 자체 중단된다.

 

속성 ⑤

C++에서 try/catch 블록을 중첩할 수 있으며, throw;를 사용하여 예외를 다시 던질 수 있다.

 

다음 프로그램은 try/catch 블록 nesting을 보여준다:

// C++ program to demonstrate try/catch blocks can be nested
// in C++

#include <iostream>
using namespace std;

int main()
{

     // nesting of try/catch
     try {
          try {
               throw 20;
          }
          catch (int n) {
               cout << "Handle Partially ";
               throw// Re-throwing an exception
          }
     }
     catch (int n) {
          cout << "Handle remaining ";
     }
     return 0;
}

 

출력:

Handle Partially Handle remaining 

 

함수는 또한 같은 throw; 구문을 사용하여 함수를 다시 던질 수 있다. 그리하여 함수는 부분을 처리하고 발신자에게 나머지 부분을 처리하도록 요청할 수 있다.

 

속성 ⑥

예외가 던져지면, 컨트롤이 캐치 블록으로 전송되기 전에 enclosing try 블록 내부에 생성된 모든 객체가 파괴된다.

 

다음 프로그램은 위의 속성을 보여준다:

// C++ program to demonstrate

#include <iostream>
using namespace std;

// Define a class named Test
class Test {
public:
     // Constructor of Test
     Test() { cout << "Constructor of Test " << endl; }
     // Destructor of Test
     ~Test() { cout << "Destructor of Test " << endl; }
};

int main()
{
     try {
          // Create an object of class Test
          Test t1;

          // Throw an integer exception with value 10
          throw 10;
     }
     catch (int i) {
          // Catch and handle the integer exception
          cout << "Caught " << i << endl;
     }
}

 

출력:

Constructor of Test 
Destructor of Test 
Caught 10

6. 예외처리의 한계

예외처리라고 당연히 만능은 아니고 단점 및 주의사항이 존재한다.


① 코드에 보이지 않는 출구가 여러 개 생성되어, 코드를 읽고 디버깅하기가 어려워질 수 있다. 그래서 코드의 구조나 흐름을 깨뜨릴 수 있다.
② 예외 처리를 제대로 하지 않으면 리소스가 유출될 수도 있다.
③ 안전한 예외 코드를 작성하는 방법을 배우는 것은 난이도가 다소 높다.
④ 예외 처리를 사용하는 방법에 대한 C++ 표준이 없으므로, 예외 처리 방법에 많은 변형이 존재한다. 위에서 난이도가 높다고 말하는 것도 이런 탓이 있다.

7. 결론

C++의 예외 처리는 문제를 효율적으로 관리하기 위해 "try" 및 "catch" 블록을 사용하여 예기치 않은 발생을 처리하는 데 사용된다. 이러한 예외 처리는 런타임에 발생하는 오류를 별도로 처리할 수 있기 때문에 우리의 프로그램을 더욱 안정적으로 만들고, 오류가 발생했을 때 프로그램이 충돌하고 프로그램이 갑자기 종료되는 것을 방지하는 데 도움이 된다.

728x90