KoreanFoodie's Study
C++ 기초 개념 11 : 예외처리 본문
모두의 코드를 참고하여 핵심 내용을 간추리고 있습니다. 자세한 내용은 모두의 코드의 씹어먹는 C++ 강좌를 참고해 주세요!
throw 로 예외 발생시키기
예외를 발생시키는 예시 코드를 보자.
// 생략 ...
const T& at(size_t index) const {
if (index >= size) {
// 예외를 발생시킨다!
throw std::out_of_range("vector 의 index 가 범위를 초과하였습니다.");
}
return data[index];
}
위에서는 범위 초과를 알리는 메시지를 출력한다. C++ 는 예외를 던지고 싶다면 throw 로 예외로 전달하고 싶은 객체를 써주면 된다. C++ 표준에는 overflow_error, length_error, runtime_error 등의 다양한 예외 객체가 정의되어 있다.
이렇게 예외를 throw 하게 되면, throw 아래 있는 명령문은 실행되지 않는다. throw 에서 예외를 처리하는 부분으로 바로 점프하기 때문이다! 이 과정에서, stack 에 생성되었던 객체들을 빠짐없이 소멸시켜 준다! (stack unwinding)
try - catch 구문
이번에도 예시 코드를 통해 동작을 파악하자.
try {
data = vec.at(index);
} catch (std::out_of_range& e) {
std::cout << "예외 발생 ! " << e.what() << std::endl;
}
try 안에서 예외가 발생하면, catch 구문으로 이동하고, 예외가 발생하지 않았다면 catch 구문은 무시된다. 예외가 발생하면 stack 에 생성된 모든 객체들의 소멸자들이 호출되고, 가장 가까운 catch 문으로 점프한다.
what( ) 함수는 예외에 관한 내용을 저장하는 문자열 필드를 들여다 보는 함수이다. 이 경우 이미 전달한 문장인 "vector의 index가 범위를 초과하였습니다" 가 나오게 된다.
스택 풀기 (stack unwinding)
try 구문에서 예외가 발생했을 때, 가장 가까운 catch 가 실행된다. 다음 예시를 보자.
#include <iostream>
#include <stdexcept>
class Resource {
public:
Resource(int id) : id_(id) {}
~Resource() { std::cout << "리소스 해제 : " << id_ << std::endl; }
private:
int id_;
};
int func3() {
Resource r(3);
throw std::runtime_error("Exception from 3!\n");
}
int func2() {
Resource r(2);
func3();
std::cout << "실행 안됨!" << std::endl;
return 0;
}
int func1() {
Resource r(1);
func2();
std::cout << "실행 안됨!" << std::endl;
return 0;
}
int main() {
try {
func1();
} catch (std::exception& e) {
std::cout << "Exception : " << e.what();
}
}
위의 결과값은 다음과 같다.
리소스 해제 : 3
리소스 해제 : 2
리소스 해제 : 1
Exception : Exception from 3!
func1, func2 는 예외를 받는 구문이 없으므로, main 함수의 catch 가 해당 예외를 출력해준다. 중요한 것은, catch 로 점프하면서 스택 상에서 정의된 객체들을 소멸해주게 되는데, 이를 스택 풀기 (stack unwinding) 이라고 부른다!
여러 종류의 예외 받기
#include <iostream>
#include <string>
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 는 여러 종류의 throw 된 객체를 모두 받을 수 있다.
그렇다면 파생 클래스에서 catch 가 실행된다면 어떻게 될까?
#include <exception>
#include <iostream>
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" 가 출력되지만, 2 를 넣으면 "Child Catch" 가 아닌 "Parent Catch" 가 출력된다. 왤까?
catch 문의 경우 가장 먼저 대입될 수 있는 객체를 받는데,
Parent& p = Child();
는 가능하기 때문에 Parent catch 가 이를 먼저 받아버린다. 따라서 파생 클래스의 예외 catch 블록을 기반 클래스보다 먼저 써주는 것이 좋다!
try {
func(c);
} catch (Child& c) {
std::cout << "Child Catch!" << std::endl;
std::cout << c.what();
} catch (Parent& p) {
std::cout << "Parent Catch!" << std::endl;
std::cout << p.what();
}
참고로, 일반적으로 예외객체는 std::exception 을 상속받는 것이 좋다. 왜냐하면 표준 라이브러리의 유용한 함수들 (nested_exception 등) 을 사용할 수 있기 때문이다!
모든 예외 받기
아래 코드처럼, 어떤 예외를 throw 하였는데 이를 받는 catch 가 없다면 어떻게 될까?
#include <iostream>
#include <stdexcept>
int func() { throw std::runtime_error("error"); }
int main() {
try {
func();
} catch (int i) {
std::cout << "Catch int : " << i;
}
}
컴파일 후 실행하면 runtime_error 코드가 발생하며 프로그램이 종료한다. (여기서는 std::runtime_error 객체를 catch 의 인자로 써 주어야 함)
만약 타입에 상관없이 '나머지 전부'의 예외를 받고 싶으면, 다음과 같은 구문을 쓰면 된다!
try {
func(c);
} catch (int e) {
std::cout << "Catch int : " << e << std::endl;
} catch (...) {
std::cout << "Default Catch!" << std::endl;
}
예외를 발생시키지 않는 함수
int foo() noexcept {}
int bar() noexcept { throw 1; }
위처럼, noexcept 를 붙이게 되면, 예외를 발생시키지 않는 함수로 정의할 수 있다. 다만 noexcept 가 붙은 함수에서 예외를 발생시키면 예외 발생시 예외가 제대로 처리되지 않고 프로그램이 종료한다! (bar 함수처럼)
C++ 11 부터, 소멸자들은 기본적으로 noexcept 이다. 절대로 소멸자에서 예외를 던지면 안된다! 프로그램의 종료 혹은 미정의 동작을 초래할 것이기 때문이다! 메모리 누수는 덤이다.
'Tutorials > C++ : Beginner' 카테고리의 다른 글
C++ 기초 개념 12-2 : Move 문법, 완벽한 전달과 레퍼런스 겹침 (0) | 2022.04.19 |
---|---|
C++ 기초 개념 12-1 : 우측값, 이동 생성자와 우측값 레퍼런스 (0) | 2022.04.19 |
C++ 기초 개념 10-4 : C++ 문자열의 모든 것 (string과 string_view) (0) | 2022.04.18 |
C++ 기초 개념 10-3 : STL 알고리즘 (STL algorithm) (0) | 2022.04.18 |
C++ 기초 개념 10-2 : 맵(map), 셋(set), unordered_map, unordered-set (0) | 2022.03.18 |