예외 처리(Exception handling)

이재원·2024년 5월 27일
0

C++

목록 보기
9/11

예외 처리의 필요성

1. 프로그램의 안정성 보장

예외 처리 없이 프로그램이 실행되면 예외 상황에서 프로그램이 예기치 않게 종료 될 수 있는데, 이런 경우 예외 처리를 통해 프로그램이 안정적으로 실행될 수 있게 한다.

2. 디버깅 용이성

예외 처리를 통해 예외가 발생한 위치와 이유를 쉽게 파악할 수 있게 도와주므로 디버깅을 용이하게 한다.

3. 사용자 친화적인 인터페이스 제공

예외 처리를 통해 사용자에게 예외 상황을 알릴 수 있다. 이를 통해 사용자는 예외에 대해 적절히 대응할 수 있다.

실행 오류의 종류와 원인

  • 개발자의 논리가 잘못된 경우
  • 예외에 대한 대책을 준비하지 않은 경우
    ex) 0으로 나누는 경우

예외 처리 매커니즘

C++에서는 try, catch, throw를 통해 예외 처리를 한다.

try

try {
// 예외 발생 지역
}

try 블록은 예외가 발생할 수 있는 코드를 감싸는 역할을 한다. catch문과 함께 사용되며 하나의 try 블록 안에 여러 개의 catch문을 사용할 수 있다.

catch

catch 블록은 특정한 타입의 예외를 잡아내어 처리하는 역할을 한다.

catch 블록의 괄호 안에는 잡아낼 예외 파라미터를 지정한다. 예외 파라미터는 예외 타입과 매개 변수로 선언하며, throw문이 던진 예외 값의 타입이 예외 타입과 일치하는 경우에 예외 값이 매개 변수에 전달되고 catch 블록이 실행된다.

throw문에서 던진 예외 값을 받을 catch 블록이 없다면 프로그램은 바로 종료된다.

참고로, 예외 파라미터는 한 개만 선언 가능하다.

catch(const char* e) {
	std::cout << "Caught an exception: " << e;
}

throw

throw 키워드는 예외가 발생했음을 알리는 신호로, 예외를 던지는 역할을 한다.

예외를 던질 때는 기본 타입 외에도 사용자 정의 타입이나 객체 모두 가능하다.

throw 3; // int 타입의 예외 값 3을 던짐
throw "empty stack" // char* 타입의 문자열 예외를 던짐

throw가 던진 예외는 catch문에서 받아 처리된다. throw가 실행되면 try 안에 있는 나머지 코드들은 실행되지 않고 바로 catch문으로 넘어가게 된다.

try-throw-catch의 예외 처리 과정

예외 처리에 대한 여러가지 방법

하나의 try 블록에 다수의 catch 블록 연결

try {
	// 예외 발생 가능 코드
} catch(int e) {
	std::cout << "Integer exception: " << e << std::endl;
} catch(const char* e) {
	std::cout << "String exception: " << e << std::endl;
} catch(...) {
	std::cout << "Unknown exception: " << e << std::endl;
}

여기서 핵심은 catch 블록이 예외의 타입에 따라 다르게 작동한다는 점이다. 예외의 타입은 throw문에 의해 결정되고, 이 타입은 catch문에서 지정한 타입과 일치해야 한다.

catch(…) 블록은 catch-all 블록이라 부르며 어떤 타입의 예외든 처리 가능하다. 이 블록은 마지막에 위치해야 하는데, 그 이유는 첫번째로 일치하는 catch 블록이 실행되고 나면 다른 catch 블록은 검사하지 않기 때문이다.

이렇듯 catch문을 여러개 작성하여 각 예외에 가장 적절한 방법으로 대응할 수 있다. 또한 예외 처리는 프로그램의 동작을 보다 예측 가능하게 만들어 버그를 줄이고 프로그램의 전반적인 품질을 향상시킨다.

함수를 포함하는 try 블록

함수들이 여러 번 중첩되어(nasted) 호출된다 하더라도 try 블록 안에서 호출된 함수에서 throw문을 실행하면 try 블록에 연결된 catch 블록에서 예외 처리가 된다.

#include <iostream>
using namespace std;

int getExp(int base, int exp) {
  if (base <= 0 || exp <= 0)
    throw "음수 사용 불가";
  int value = 1;
  for (int i = 0; i < exp; i++)
    value *= base;
  return value;
}

int main() {
  int v = 0;
  try {
    v = getExp(2, 3);
    cout << "2의 3승은 " << v << "입니다.\n";

    v = getExp(2, -3);
    cout << "2의 -3승은 " << v << "입니다.\n";
  } catch (const char *s) {
    cout << "예외 발생!! " << s << endl;
  }
}

// 출력
// 2의 3승은 8입니다.
// 예외 발생!! 음수 사용 불가

예외를 발생시키는 함수의 선언

throw문을 가지고 있는 함수는 함수 선언문에 예외 발생을 명시할 수 있다. 그 형식은 함수에서 발생시키는 모든 예외 타입을 함수 원형 뒤에 throw()의 괄호 안에 나열한다.

예제 1

double valueAt(double *p, int index) throw(int, char*) {
	if(index < 0)
		throw "index out of bounds exception";
	else if(p == NULL)
		throw 0;
	else
		return p[index];
}

예제 2

#include <iostream>
using namespace std;

void solved() throw(const char *) {
  while (true) {
    int n = 0;
    cout << "양수입력>> ";
    cin >> n;
    // cin.fail()은 숫자형 변수에 문자를 넣으려고 할 경우 참(=true)를 반환.
    if (cin.fail())
      throw "입력 오류가 발생하여 더 이상 입력되지 않습니다. 프로그램을 "
            "종료합니다";
    else if (1 > n || 9 < n) {
      cout << "잘못된 입력입니다. 1~9 사이의 정수만 입력하세요" << endl;
      continue;
    }
    for (int i = 1; i <= 9; i++)
      cout << n << 'x' << i << '=' << n * i << ' ';
    cout << endl;
  }
}
int main() {
  try {
    solved();
  } catch (const char *e) {
    cout << e << endl;
  }
}

이렇게 작성하는 것이 필수는 아니지만 아래와 같은 장점이 있다.

프로그램의 작동을 명확히 한다

컴파일러는 함수 선언문의 throw()에 선언되지 않은 예외가 발생하면 프로그램을 종료시킨다. 그러나 컴파일러에 따라서는 그냥 넘어가기도 한다.

예외와 관련된 프로그램의 가독성을 높인다

원형만 봐도 함수에서 발생시키는 예외를 알 수 있어 코드를 쉽게 이해할 수 있다.

중접 try 블록

try 블록 내에 다른 try 블록을 중첩하여 작성할 수 있다. 이때, 안쪽 try 블록의 throw에서 던진 예외를 처리할 catch 블록이 없으면, 바깥 쪽 try 블록에 연결된 catch 블록에 예외가 전달된다.

throw 사용 시 주의 사항

throw 문의 위치

throw문은 항상 try 블록 안에서 실행되어야 한다. 그렇지 않은 경우 시스템은 abort() 함수를 호출하여 프로그램을 종료시킨다.

예외를 처리할 catch 블록이 없으면 프로그램은 종료된다.

throw가 던지는 타입의 예외를 처리할 catch 블록이 선언되어 있지 않은 경우 throw 문이 실행되면 시스템이 abort() 함수를 호출하여 프로그램을 비정상 종료시킨다.

try {
	throw "aa"; // cahr* 타입의 예외를 처리할 catch 블록이 없기 때문에 비정상 종료
} catch(double p) {
	...
}

catch 블록 내에도 try & catch 블록 선언 가능

try {
	throw 3;
	...
} catch(int x) {
	try {
		throw "aa"; // 아래의 catch(const char* p)에서 처리된다
		...
	} catch(const char* p) {
		...
	}
}

사용자 정의 예외 클래스 만들기

기본 타입을 사용할 때보다 클래스를 사용하면 catch 블록에 더 많은 정보를 전달할 수 있다. 이를 통해 문제가 발생한 구체적인 상황을 잘 표현할 수 있고, 디버깅을 훨씬 쉽게 만든다.

사용자 정의 예외 클래스는 표준 예외 클래스인 std::exception을 상속받아 생성한다.

#include <exception>
using std::exception;

class MyException : public exception {
public:
  const char *what() const throw() override { 
    return "My exception occured"; 
  }
};

이 클래스는 exception을 상속받아 what() 함수를 오버라이딩한다. 이 함수는 throw() 지정자를 가지고 있는데, 이는 이 함수가 예외를 던지지 않음을 보장한다.

이제 이 사용자 정의 예외를 throw문을 사용해 던질 수 있다.

try {
  throw MyException();
} catch (MyException &e) {
  cerr << e.what() << "\n";
} catch(exception &e) {
  // other errors
}

사용자 정의 예외 클래스를 사용하면, 발생 가능한 여러 가지 예외 상황에 대응하는 예외 타입을 생성할 수 있다. 이를 통해 각 예외 상황에 대한 정보를 더욱 세밀하게 제공할 수 있고, 예외 처리를 더욱 명확하게 할 수 있다. 또한, 프로그램의 안정성과 유지보수성도 향상할 수 있다.

class MyException : public exception {
  string message;

public:
  MyException(const string &msg) : message(msg) {}
  virtual const char *what() const throw() override { 
    return message.c_str(); 
  }
};

int main() {
  try {
    throw MyException("Something went wrong!");
  } catch (MyException &e) {
    cerr << e.what() << "\n";
  } catch (exception &e) {
    cerr << "Other exceptions" << endl;
  }
}

다음 예제는 exception을 상속받지 않고 예외 클래스를 만든 것이다.

class MyException {
  int lineNo;
  string func, msg;

public:
  MyException(int n, string f, string m) : lineNo(n), func(f), msg(m) {}
  void print() { cout << func << ": " << lineNo << ", " << msg << endl; }
};

class DividedByZeroException : public MyException {
public:
  DividedByZeroException(int lineNo, string func, string msg)
      : MyException(lineNo, func, msg) {}
};

class InvalidInputException : public MyException {
public:
  InvalidInputException(int lineNo, string func, string msg)
      : MyException(lineNo, func, msg) {}
};

int main() {
  int x, y;
  try {
    cout << "나눗셈을 합니다. 두 개의 양의 정수를 입력하세요: ";
    cin >> x >> y;
    if (x < 0 || y < 0)
      throw InvalidInputException(33, "main()", "음수 입력 예외 발생");
    if (y == 0)
      throw DividedByZeroException(25, "main()", "0으로 나누는 예외 발생");
    cout << (double)x / (double)y;
  } catch (DividedByZeroException &e) {
    e.print();
  } catch (InvalidInputException &e) {
    e.print();
  }

  return 0;
}

출처
명품 C++ Programming - 황기태
https://gdngy.tistory.com/180

profile
20학번 새내기^^(였음..)

0개의 댓글