열혈 C++을 기초해 작성됨
예외상황과 예외처리의 이해
우리가 알고 있는 예외의 처리는 if문을 이용하는 것이다.
if문을 통해 예외상황의 발생유무를 확인한 다음 그에 따른 처리를 진행한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | int main() { int num1, num2; std::cin >> num1 >> num2; if(num2 == 0) { std::cout << "제수는 0이 될 수 없음!" << std::endl; } else { std::cout << num1 / num2 << std::endl; std::cout << num1 % num2 << std::endl; } return 0; } | cs |
일반적인 예외 처리 방식이다. 하지만 다른 사람들이 코드를 볼 때 if문이 예외 처리를 위한 분기문인지, 논리적인 기능을 위한 분기문인지 한눈에 보기 어렵다.
C++의 예외처리 메커니즘
C++의 예외처리 메커니즘 이해: try와 catch 그리고 throw의 이해
- try
- catch
- throw
try
try 블록은 예외검사의 범위지정 시 사용된다. try 블록 내에서 예외가 발생하면, C++의 예외처리 메커니즘에 의해 처리된다.
1 2 3 4 | try { . . . . . } | cs |
catch
catch 블록은 try에서 발생한 예외를 처리하는 코드가 담기는 영역으로, 그 형태가 마치 반환형 없는 함수와 유사하다.
1 2 3 4 | catch(예외 종류 명시) { . . . . . } | cs |
try블록과 catch블록
catch블록은 try블록 뒤에 이어 등장한다. try 블록에서 발생한 예외는 이곳 catch블록에서 처리된다.
1 2 3 4 5 6 7 8 9 | try { Application().run(std::unique_ptr<RendererInterface>{ renderer }); } catch (const std::exception& e) { std::fprintf(stderr, "Error: %s\n", e.what()); return 1; } | cs |
작동 예
throw
throw는 예외가 발생했음을 알리는 문장의 구성에 사용된다.
throw expn;
expn은 변수, 상수, 그리고 객체 등 표현 가능한 모든 데이터지만, 예외상황에 대한 정보를 담은 데이터다. 위 문장의 expn의 위치에 오는 데이터를 예외라고 표현한다. 또한, 예외 데이터, 예외 객체라고 표현하기도 한다.
throw에 의해 던져진 예외 데이터는 예외 데이터를 감싸는 try 블록에 의해 감지가 되어, 이어서 등장하는 catch 블록에 의해 처리된다.
1 2 3 4 5 6 7 8 9 10 11 12 | try { . . . . . if(예외 발생) { throw expn; . . . . . } catch (type expn) { . . . . . //예외 처리 } | cs |
예외처리 메커니즘의 적용
아래 코드는 try catch를 이용한 예외처리 결과이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | int main() { int num1, num2; std::cin >> num1 >> num2; try { if(num2 == 0) { throw num2; } std::cout << num1 / num2 << std::endl; std::cout << num1 % num2 << std::endl; } catch (int expn) { std::cout << "잘못된 값: " << expn << std::endl; } std::cout << "끝" << std::endl; return 0; } | cs |
입력
7 7
결과
1
0
끝
입력
7 0
잘못된 값: 0
끝
20 : 예외가 던져지는 상황이나, 던져지지 않는 상황이나 실행된 것이 보인다.
10 : num2 의 자료형은 int다
16 : catch로 받는 예외의 자료형 역시 int다
if문으로 구현한 예외처리와 비슷하지만 약간 다르게 작동하는 것이 보인다.
컴파일러가 throw를 만나면 그 이후부터 try 범위의 내부에 있는 다른 연산은 수행하지 않는다. 바로 catch로 넘어간다.
-> 예외가 발생한 지점 이후를 실행하는 것이 아니라, catch블록 이후가 실행된다.
try 블록을 묶는 기준
다음 규칙은 try의 실행 흐름이다.
- try 블록을 만나면 그 안에 삽입된 문장이 순서대로 실행된다.
- try 블록 내에서 예외가 발생하지 않으면 catch 블록 이후를 실행한다.
- try 블록 내에서 예외가 발생하면, 예외가 발생한 지점 이후의 나머지 try 영역은 건너뛴다.
try블록을 묶는 기준은 예외와 관련된 모든 문장을 함께 묶어서 하나의 일의 동작 단위로 구성하는 것이다.
1 2 3 4 5 6 7 8 9 | try { if(num2 == 0) { throw num2; } std::cout << num1 / num2 << std::endl; std::cout << num1 % num2 << std::endl; } | cs |
이렇게 코드가 짜여지지 않고
1 2 3 4 5 6 7 8 9 10 | try { if(num2 == 0) { throw num2; } } catch(int expn) { . . . . . } std::cout << num1 / num2 << std::endl; std::cout << num1 % num2 << std::endl; | cs |
이렇게 되어 있다면 문제가 된다는 것이다.
try 블록 내에서 예외가 발생하면, 예외가 발생한 지점 이후의 나머지 try 영역은 건너뛴다.
사실 당연한 말이다. 근데 왜 이렇게 꼬아서 설명했는지 모르겠군
참고
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | int main() { int num1, num2; std::cin >> num1 >> num2; try { if (num2 == 0) { throw num2; } std::cout << num1 / num2 << std::endl; std::cout << num1 % num2 << std::endl; } std::cout << "야후~\n"; //Error! catch (int expn) { std::cout << "잘못된 값: " << expn << std::endl; } std::cout << "끝" << std::endl; return 0; } | cs |
에러가 난다.
오류 C2317 줄 '6'에서 시작하는 'try' 블록에 catch 처리기가 없습니다. - LINE 9
오류 C2318 이 catch 처리기와 관련된 try 블록이 없습니다. - LINE 19
Stack Unwinding(스택 풀기)
예외의 전달
어떠한 함수 foo의 내부에서 throw절이 실행되었는데, 만약 try ~ catch 없이 실행된다면 어떻게 될까?
이러한 경우 예외처리에 대한 책임은 foo()를 호출한 영역으로 넘어간다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | void foo(int num1, int num2) { if (num2 == 0) { throw num2; } std::cout << num1 / num2 << std::endl; std::cout << num1 % num2 << std::endl; } int main() { int num1, num2; std::cin >> num1 >> num2; try { foo(num1, num2); std::cout << "끝" << std::endl; } catch (int expn) { std::cout << "잘못된 값: " << expn << std::endl; } return 0; } | cs |
이러한 경우에, foo함수 내부에서 예외가 발생해 throw로 num2를 던지는 상황이 온다면, 예외 데이터(num2)는 함수를 호출한 main함수로 전달된다.
또한, 예외처리에 대한 책임 역시 foo를 호출한 main 함수로 넘어가게 된다(실행 중이던 함수를 종료하고, main함수에 프로그램 진행 흐름을 떠넘기겠다는 소리다.)
예외상황이 발생한 위치와 예외상황을 처리해야 하는 위치가 다른 경우
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | int StoI(char* str) { int len = strlen(str); int num = 0; for (int i = 0; i < len; ++i) { if (str[i] < '0' || str[i] > '9') { throw str[i]; } num += (int)(pow((double)10, (len - 1) - i) * (str[i] + (7 - '7'))); } return num; } int main() { char str1[100]; char str2[100]; while (true) { std::cin >> str1 >> str2; try { std::cout << str1 << " + " << str2 << " = " << StoI(str1) + StoI(str2) << std::endl; break; } catch (char ch) { std::cout << "잘못된 문자: " << ch << std::endl; } } return 0; } | cs |
함수 내에서 함수를 호출한 영역으로 예외를 전달하면, 해당 함수는 더 이상 실행되지 않고 종료된다.
스택 풀기
위와 같이 예외가 처리되지 않아 함수를 호출한 영역으로 예외 데이터가 전될되는 현상을 스택 풀기라고 한다.
왜 이러한 이름이 붙게 되었을까
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | void firstfoo() { std::cout << "1" << std::endl; secondfoo(); } void secondfoo() { std::cout << "2" << std::endl; thirdfoo(); } void thirdfoo() { std::cout << "3" << std::endl; throw -1; } int main() { try { firstfoo(); } catch (int expn) { std::cout << "예외코드: " << expn << std::endl; } return 0; } | cs |
16 : firstfoo의 secondfoo의 thirdfoo에서 예외를 던진다. 그러면 쌓여있던 함수 스택을 풀면서 try문이 있는 main함수까지 내려온다.
그렇다면 만약 try문이 없다면 어떻게 될까?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | void thirdfoo() { std::cout << "3" << std::endl; throw -1; } void secondfoo() { std::cout << "2" << std::endl; thirdfoo(); } void firstfoo() { std::cout << "1" << std::endl; secondfoo(); } int main() { std::cout << "시작\n"; firstfoo(); std::cout << "끝\n"; return 0; } | cs |
결과
시작
1
2
3
그리고 에러가 나타난다.
예외가 처리되지 않아서 예외 데이터가 함수를 타고 타고 기본 함수인 main 함수까지 내려왔는데도, try문이 보이지 않으면 terminate 함수(프로그램을 종료시키는 함수)가 호출되면서 프로그램이 종료된다.
따라서 시스템 오류로 인해 발생한 예외가 아니고, 프로그램의 실행이 불가능한 예외가 아니라면 반드시 프로그래머가 예외 상황을 처리해야 한다.
자료형이 일치하지 않아도 예외 데이터는 전달됩니다.
예외 데이터의 자료형과 catch 매개변수는 자료형이 일치해야 한다. 그렇다면 그 둘이 일치하지 않는 경우에 대해 살펴보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | void thirdfoo() { std::cout << "3" << std::endl; int error = -1; throw -1; } void secondfoo() { std::cout << "2" << std::endl; try { thirdfoo(); } catch (char expn) { std::cout << "나는 char형 예외처리!\n"; } } void firstfoo() { std::cout << "1" << std::endl; secondfoo(); } int main() { firstfoo(); return 0; } | cs |
결과
1
2
3
그리고 에러 (abort() has been called!)
자료형의 불일치로 예외는 처리되지 않는다. (자료형이 다른 변수가 매개변수가 일치하는 함수를 찾지 못하는 것 처럼)
하나의 try블록과 다수의 catch 블록
이러한 이유로 try 이후로 등장하는 catch는 여러개가 될 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | int StoI(char* str) { int len = strlen(str); int num = 0; if (len != 0 && str[0] == '0') { throw 0; } for (int i = 0; i < len; ++i) { if (str[i] < '0' || str[i] > '9') { throw str[i]; } num += (int)(pow((double)10, (len - 1) - i) * (str[i] + (7 - '7'))); } return num; } int main() { char str1[100]; char str2[100]; while (true) { std::cin >> str1 >> str2; try { std::cout << str1 << " + " << str2 << " = " << StoI(str1) + StoI(str2) << std::endl; break; } catch (char ch) { std::cout << "잘못된 문자: " << ch << std::endl; } catch (int expn) { if (expn == 0) { std::cout << "0으로 시작하는 숫자는 입력 불가!\n"; } else { std::cout << "잘못된 입력\n"; } } } return 0; } | cs |
catch가 2개다.
전달되는 예외의 명시
함수를 정의할 때에는 함수 내에서 발생 가능한 예외의 종류를 명시할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | int StoI(char* str) throw (int, char) { . . . . . } int main() { . . . . . try { std::cout << str1 << " + " << str2 << " = " << StoI(str1) + StoI(str2) << std::endl; break; } catch (char ch) { std::cout << "잘못된 문자: " << ch << std::endl; } catch (int expn) { if (expn == 0) { std::cout << "0으로 시작하는 숫자는 입력 불가!\n"; } else { std::cout << "잘못된 입력\n"; } } . . . . . return 0; } | cs |
1 : throw (int, char)가 보인다.
하지만 이것은 단순히 명시의 목적으로,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | int StoI(char* str) throw (int) { . . . . . } int main() { . . . . . try { std::cout << str1 << " + " << str2 << " = " << StoI(str1) + StoI(str2) << std::endl; break; } catch (char ch) { std::cout << "잘못된 문자: " << ch << std::endl; } catch (int expn) { if (expn == 0) { std::cout << "0으로 시작하는 숫자는 입력 불가!\n"; } else { std::cout << "잘못된 입력\n"; } } . . . . . } | cs |
위와 같은 코드를 작성해서 입력으로 잘못된 char를 전달해도 catch (char ch)는 정상적으로 잡아낸다.
(비주얼 스튜디오 C++ 2017 기준이다. 이전 버전에선 어떻게 작동하는지 모른다.)
하지만 이러한 코드를 짜면 혼날 것이다.
중요한 점은, throw의 예외 데이터와 자료형의 일치다.
만약 다른 자료형이 전달될 경우 throw의 예외는 자신에게 맞는 catch를 찾아 저 멀리 떠나 결국 terminate 함수를 호출하게 될 것이다.
1 2 3 4 | int foo() throw () { . . . . . } | cs |
이는 어떠한 예외도 전달하지 않음을 의미하고, 위의 함수가 예외를 전달할 경우 프로그램은 그냥 종료된다고
책에서는 전하고 있지만, msvc 2017에서는 throw - catch만 잘 매치되어 있다면 잘 동작한다.
덧)
terminate 함수는 이 함수에 존재한다.
1 | unexpected(); | cs |
프로그램을 종료시키는 함수다.
1 2 3 4 5 6 | int main() { unexpected(); std::cout << "안녕?"; return 0; } | cs |
결과
에러난다. 안녕?은 출력되지 않는다.
main함수가 프로그램의 가장 밑에서 돌아가는 것은 아니다. int main()에서 return 0의 0을 리턴받는 곳이 가장 밑바닥이다.
catch는 저곳까지 내려가 unexpected를 호출하는 것이다.
예외상황을 표현하는 예외 클래스의 설계
예외 클래스와 예외객체
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | class DepositException { public: DepositException(int money) : reqDep(money) { } void ShowExceptionReason() { std::cout << "[예외 메시지: " << reqDep << "는 입금불가]" << std::endl; } private: int reqDep; }; . . . . . class Account { . . . . . public: void Deposit(int money) throw (DepositException) { if (money < 0) { DepositException expn(money); throw expn; } . . . . . } }; int main() { Account myAcc(. . . . .); try { myAcc.Deposit(-300); } catch (DepositException& expn) { expn.ShowExceptionReason(); } . . . . . return 0; } | cs |
예외 클래스 역시 기본적인 원리는 예외처리와 같다. 하지만 코드가 복잡해보일 여지가 있으니 해당 예외상황을 잘 표현하고, 가능한 만큼 깔끔하게 정의해야 한다.
상속관계에 있는 예외 클래스
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | class AccountException { public: virtual void ShowExceptionReason() = 0; }; class DepositException : public AccountException { public: DepositException(int money) : reqDep(money) { } void ShowExceptionReason() { std::cout << "[예외 메시지: " << reqDep << "는 입금불가]" << std::endl; } private: int reqDep; }; class WithdrawException : public AccountException { public: WithdrawException(int money) : balance(money) { } void ShowExceptionReason() { std::cout << "[예외 메시지: 잔액부족, " << balance << " ]" << std::endl; } private: int balance; }; class Account { . . . . . public: void Deposit(int money) throw (AccountException) { if (money < 0) { DepositException expn(money); throw expn; } . . . . . } void Withdraw(int money) throw (AccountException) . . . . . }; int main() { Account myAcc(. . . . .); try { myAcc.Deposit(-300); } catch (AccountException& expn) { expn.ShowExceptionReason(); } try { myAcc.Withdraw(50000); } catch (AccountException& expn) { expn.ShowExceptionReason(); } . . . . . return 0; } | cs |
상속을 통해 예외 클래스를 묶어 예외의 처리를 단순하게 만들 수 있다.
항상 좋은 것은 아니다. 코드가 커지고 난잡해질 여지가 있지만, 유용한 경우 또한 존재할 것.
예외의 전달방식에 따른 주의사항
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | class AAA { public: void Show() { std::cout << "AAA 예외!" << std::endl; } }; class BBB : public AAA { public: void Show() { std::cout << "BBB 예외!" << std::endl; } }; class CCC : public BBB { public: void Show() { std::cout << "CCC 예외!" << std::endl; } }; void ExceptionGenerator(int expn) { if(expn == 1) throw AAA(); else if(expn == 2) throw BBB(); else throw CCC(); } int main() { try { ExceptionGenerator(3); ExceptionGenerator(2); ExceptionGenerator(1); } catch(AAA& expn) { std::cout << "catch AAA" << std::endl; expn.Show(); } catch(BBB& expn) { std::cout << "catch BBB" << std::endl; expn.Show(); } catch(CCC& expn) { std::cout << "catch CCC" << std::endl; expn.Show(); } return 0; } | cs |
결과
catch AAA
AAA 예외!
코드의 의도는 아마
catch CCC
CCC 예외!
catch BBB
BBB 예외!
catch AAA
AAA 예외!
를 생각했지 싶은데, 하지만 어림없다.
원인은 상속을 공부했다면 알 수 있는 내용이니 넘어간다.
해결 방법은 catch 순서만 바꿔주면 된다.
예외처리와 관련된 또 다른 특성들
new 연산자에 의해 발생하는 예외
new 연산으로 메모리 할당이 실패하면 std::bad_alloc이라는 예외가 발생한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | int main() { int num = 0; try { while (1) { std::cout << num++ << std::endl; new int[10000][10000]; } } catch (std::bad_alloc& bad) { std::cout << bad.what(); } return 0; } | cs |
이러한 예외는 C++이 자체적으로 정의하고 있는 예외중 하나이다.
모든 예외를 처리하는 catch 블록
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | void something() { throw 1; throw 'a'; throw 3.14; throw false; } int main() { int num = 0; try { something(); } catch (...) { std::cout << "몬가.. 몬가 문제가 있어용\n"; } return 0; } | cs |
결과
몬가.. 몬가 문제가 있어용
catch (...) 는 try 내에서 전달되는 모든 예외가 자료형에 상관없이 걸려든다.
(...)는 C++이 정한 규칙, 약속이다.
그래서 보통 마지막 catch에서 덧붙여지는 경우가 많다.
어떠한 전달도, 종류도 구분이 불가능하지만, 예외가 발생했다는 것만 알 수 있다.
예외 던지기
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | void something() { try { int error = 1; } catch (int expn) { throw expn; } } int main() { int num = 0; try { something(); } catch (int expn) { std::cout << expn << "받았음\n"; } return 0; } | cs |
요렇게 짜는 것은 마지막 수단이다. 굳이 예외를 다시 던지려고 하지는 말자.
예외처리는 정말 느릴까?
https://yesarang.tistory.com/371
C++창시자인 스트...할아버지는 예외처리를 사용하라고 하고, 구글 C++ 스타일 가이드에서는 C++ 예외를 사용하지 말라고 하지만
알아서 잘 해야겠지
http://blog2928.kc39.net/textyle/13254
std::exception
1 2 3 4 5 6 7 8 | try { } catch (const std::exception&) { } | cs |
msvc2017 try catch 기본구문이다.
std::exception은 new 연산자에 의해 발생되는 std::bad_alloc의 기본형, C++에서 정의해둔 클래스다.
bad_alloc의 클래스 내부는 이렇게 생겼다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class bad_alloc : public exception { public: bad_alloc() noexcept : exception("bad allocation", 1) { } private: friend class bad_array_new_length; bad_alloc(char const* const _Message) noexcept : exception(_Message, 1) { } }; | cs |
2 : public exception이 보이셈??
다음으로는 exception의 내부 코드다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | class exception { public: exception() noexcept : _Data() { } explicit exception(char const* const _Message) noexcept : _Data() { __std_exception_data _InitData = { _Message, true }; __std_exception_copy(&_InitData, &_Data); } exception(char const* const _Message, int) noexcept : _Data() { _Data._What = _Message; } exception(exception const& _Other) noexcept : _Data() { __std_exception_copy(&_Other._Data, &_Data); } exception& operator=(exception const& _Other) noexcept { if (this == &_Other) { return *this; } __std_exception_destroy(&_Data); __std_exception_copy(&_Other._Data, &_Data); return *this; } virtual ~exception() noexcept { __std_exception_destroy(&_Data); } virtual char const* what() const { return _Data._What ? _Data._What : "Unknown exception"; } private: __std_exception_data _Data; }; | cs |
exception은 여러 종류의 예외처리 클래스를 갖는 std 클래스다.
예외처리 클래스 종류로는
- range_error
- overflow_error
- underflow_error
- regex_error(C++11)
- nonexistent_local_time(C++20)
- ambiguous_local_time(C++20)
- tx_exception(TM TS)
- system_error(C++11)
- ios_base::failure(C++11)
- filesystem::filesystem_error(C++17)
- bad_any_cast(C++17)
- bad_weak_ptr(C++11)
- bad_function_call(C++11)
- bad_alloc
- bad_array_new_length(C++11)
- bad_exception
- ios_base::failure(until C++11)
- bad_variant_access(C++17)
https://en.cppreference.com/w/cpp/error/exception
이러케 많다.
생각보다 별거 없군.
'C++ > 열혈 C++' 카테고리의 다른 글
C++의 형 변환 (0) | 2019.08.19 |
---|---|
열혈 C++ / 14 (0) | 2019.07.26 |
열혈 C++ / 13 (0) | 2019.07.24 |
열혈 C++ / 12 (0) | 2019.07.24 |
열혈 C++ / 11 (0) | 2019.07.18 |