exception
모두의 코드 내용을 공부하고 정리한 내용입니다.
예외
- 예외란? 사람은 실수하기 마련이고 또 컴퓨터는 필요한 자원을 무조건 제공할 수 있는 것은 아니다.
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 catch
를Child catch
보다 뒤에 써야한다.- 이를 통해서
Child
객체가Parent catch
에 들어가는 것을 막을 수 있고 Child &c = Parent();
는 성립하지 않기에Child catch
에Parent
객체가 들어가지도 않는다.
- 이를 통해서
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
이다.- 그러므로로 소멸자에서 절대로 예외를 던지지 말자.
개인 공부 기록용 블로그입니다.
틀린 부분 있으다면 지적해주시면 감사하겠습니다!!
댓글남기기