모두의 코드 내용을 공부하고 정리한 내용입니다.



예외

  • 예외란? 사람은 실수하기 마련이고 또 컴퓨터는 필요한 자원을 무조건 제공할 수 있는 것은 아니다.
std::vector<int> v(3); // 크기가 3 인 벡터 만듦
std::cout << v.at(4); // 실수로 인덱스 범위 밖 접근근 ==> 오류발생
std::vector<int> v(1000000000); // 평범한 시스템의 경우 이렇게 큰 메모리를 할당할 수 없음
  • 이렇게 정상적인 상황에서 벗어난 모든 예외적인 상황들을 예외(exception)이다.


  • c언어에선 문제가 생길만한 곳에 if문으로 return하는 것으로 예외를 처리했지만 c++은 명시적으로 예외가 발생했음을 표시할 수 있다.


throw

  • 간단하게 백터 클래스를 만든다고 생각하자
    • at함수를 만드는데 접근하려는 범위가 해당 벡터 크기 범위 이내라면 data[index] 그냥 리턴하면 되지만
    • 범위 밖이라면??
    • 문제는 at함수는 리턴값이 const T& 이라서 따로 리턴하는 것으로 처리 불가능

      throw로 명시적으로 알린다.

if (index >= size) 
   throw std::out_of_range("vector 의 index 가 범위를 초과하였습니다."); 
  • C++ 에는 예외를 던지고 싶다면, throw로 예외로 전달하고 싶은 객체를 써주면 됩니다.
  • 예외로 아무 객체나 던져도 상관 없지만 C++ 표준 라이브러리에는 여러가지 종류의 예외들이 정의되어 있다.
    • out_of_range 외에도 overflow_error, length_error, runtime_error 등등…


  • 이렇게 예외를 throw 하게 되면, throw 한 위치에서 즉시 함수가 종료되고
  • 예외 처리하는 부분까지 점프한다. 그러면 throw 밑의 로직은 실행되지 않음.

  • 여기서 예외를 처리하는 부분에 도달하기 까지 함수를 빠져나가면서 stack에 생성된 객체들을 전부 소멸시킨다.
    • 대신 소멸자를 제대로 작성해야함


  • 그럼 예외를 던지고 그것을 처리하는 것은 어떻게 하냐?


예외 처리 - try & catch

try {
data = vec.at(index);
} catch (std::out_of_range& e) {
std::cout << "예외 발생 ! " << e.what() << std::endl;
}
  • 우선 try 부분은 예외가 발생할 만한 지역을 지정한다.
  • catch는 예외가 발생되면 실행되는 코드
    • 예외를 처리한다.
  • catch 블록은 항상 try 블록의 뒤에 이어서 등장해야 하며, try 블록에서 발생한 예외는 catch 블록에서 처리된다.
  • 만약 예외가 일어나지 않는다면 그냥 try … catch 부분이 없는 것처럼 실행된다.


예외 처리는 어떻게 이루어 지는가?

  • 예외가 발생한다면 위에서 말한 stack에 생성된 모든 객체의 소멸자가 호출되고
    • 이와 같이 catch 로 점프 하면서 스택 상에서 정의된 객체들을 소멸시키는 과정을 스택 풀기(stackunwinding) 이라고 한다.
  • 가장 가까운 catch문으로 점프한다.
    • 위의 경우 throw 다음 아래 로직이 실행된다.
catch (std::out_of_range& e) {std::cout << "예외 발생 ! " << e.what() << std::endl; }
  • 여기서 catch 문은 throw에서 던진 객체에 맞는 객체를 받는다.
  • 위에서 throw std::out_of_range("vector 의 index 가 범위를 초과하였습니다.");
  • out_of_range 를 throw 하였는데, 위 catch 문이 out_of_range를 받아서 처리한다.


  • 1가지 주의할 점이 있다. 생성자에서 예외가 발생하면 소멸자가 호출되지 않는다.
    • 만약 발생한다면 흭득한 자원에 대해서 catch에서 잘 해제해야 한다.


catch는 여러 종류의 예외를 받을 수 있고 try뒤에 catch를 여러개 달 수 있다.

int func(int c) {
  if (c == 1) {
    throw 10;
  } else if (c == 2) {
    throw std::string("hi!");
  } else if (c == 3) {
    throw 'a';
  } else if (c == 4) {
    throw "hello!";
  }
  return 0;
}

int main() {
  int c;
  std::cin >> c;

  try {
    func(c);
  } catch (char x) {
    std::cout << "Char : " << x << std::endl;
  } catch (int x) {
    std::cout << "Int : " << x << std::endl;
  } catch (std::string& s) {
    std::cout << "String : " << s << std::endl;
  } catch (const char* s) {
    std::cout << "String Literal : " << s << std::endl;
  }
}
  • 첫번째 catch 문에서는 char 형 값을, 두 번째에서는 int 형 값을, 세 번째 에서는 string 객체를, 마지막에서는 const char* 형 값을 받는다.
  • 실제로도 각기 다른 값들을 throw 하였을 때, 작동하는 catch 가 달라진다.


상속을 받는 클래스에서 처리하는 방식이 따로 있다.

class Parent : public std::exception {
  public:
     virtual const char* what() const noexcept override { return "Parent!\n"; }
};

class Child : public Parent {
  public:
    const char* what() const noexcept override { return "Child!\n"; }
};

int func(int c) {
  if (c == 1) { throw Parent(); } 
  else if (c == 2) { throw Child(); }
  return 0;
}

int main() {
int c;
std::cin >> c;

  try {
    func(c);
  } catch (Parent& p) {
    std::cout << "Parent Catch!" << std::endl;
    std::cout << p.what();
  } catch (Child& c) {
    std::cout << "Child Catch!" << std::endl;
    std::cout << c.what();
   }      // 1입력시 -> Parent Catch!  Parent! 출력
}         // 2입력시 -> Parent Catch!  Child! 출력
  • Parent 클래스 객체를 throw할 때는 예상했던데로 Parent 를 받는 catch 문이 실행되어서 “Parent Catch!” 가 출력.
  • 반면에 Child 객체를 throw 하였을 때에는 예상과는 다르게, Child 를 받는 catch 문이 아닌, Parent 를 받는 catch 문이 실행되어 “Parent Catch!” 가 출력.


  • 이와 같은 일이 발생한 이유는 catch 문의 경우 가장 먼저 대입될 수 있는 객체를 받기 때문이다.
    • catch문이 여러 개일 경우 예외 객체를 받을 수 있는 제일 앞의 catch문이 실행
  • Parent& p = Child();는 가능하기 때문에 Parent catch 가 먼저 받아버리는 것
  • 위와 같은 문제를 방지하기 위해서는 언제나 Parent catchChild catch 보다 뒤에 써야한다.
    • 이를 통해서 Child 객체가 Parent catch 에 들어가는 것을 막을 수 있고
    • Child &c = Parent();는 성립하지 않기에 Child catchParent 객체가 들어가지도 않는다.


  • Parent 클래스는 std::exception를 상속 받았는데
  • 일반적으로 예외 객체는 std::exception 을 상속 받는 것이 좋다.
    • 왜냐면 유용한 함수들이 있어서…


  • 만약 throw했는데 이를 받을 catch가 없다면??
    • switch문의 default랑 if-else문에서 마지막 else와 같은 기능이 존재한다.
    • 이 기능을 사용하자. 대신 어떤 예외도 다 받기에 특정한 타입에 대해 적용X
catch (...) {
    std::cout << "Default Catch!" << std::endl;
  }


noexcept

  • 만약에 어떤 함수가 예외를 발생시키지 않는다면 noexcept 를 통해 명시 가능하다.
  • noexcept 키워드를 붙였다고 해서 함수가 예외를 절대로 던지지 않는다는 것은 아니다. int func() noexcept {}


  • 컴파일러는 noexcept 키워드가 붙은 함수가 예외를 발생시키지 않는다는 것을 믿고 그대로 컴파일 한다
  • 대신 noexcept 로 명시된 함수에서 예외가 발생되면 예외가 제대로 처리되지 않고 프로그램이 종료돤다.


  • 그럼 왜 사용하냐??
    • 단순히 프로그래머가 컴파일러에게 주는 힌트
    • 컴파일러가 절대로 예외를 발생시키지 않는다는 사실을 안다면, 여러가지 추가적인 최적화를 수행
    • C++ 11 에서 부터 소멸자들은 기본적으로 noexcept이다.
      • 그러므로로 소멸자에서 절대로 예외를 던지지 말자.



참조


개인 공부 기록용 블로그입니다.
틀린 부분 있으다면 지적해주시면 감사하겠습니다!!

카테고리:

업데이트:

댓글남기기