배움 저장소
[따라하며 배우는 C++] 14. 예외처리 본문
14.1 예외처리의 기본
Exception Handling
프로그램은 정상적인 상황에 프로그램이 잘 작동할 뿐만 아니라 예외적인 상황에서도 그에 맞는 대처를 해야한다. 예외처리를 알아보자.
전통적인 예외처리 방법
(1) 함수 반환값으로 예외적인 상황을 알리는 경우
- 함수 내부에서 특정값을 예외적인 상황으로 표현한다. 함수 외부에서 이 값을 알고 있어야한다
int findFirstChar(const char* str, char ch) {
for (size_t i = 0; i < strlen(str); ++i)
if(str[i] == ch) return i;
return -1; // no match
}
(2) 매개변수로 받은 값에 성공 여부를 알리는 경우
double divide(int x, int y, bool& success) {
if (y == 0) {
success = false;
return 0.0;
}
success = true;
return static_cast<double>(x) / y;
}
int main() {
bool success;
double result = divide(5,3, success);
if(success)
cout << "Result is " << result << endl;
else
cout << "Error Occurred!" << endl;
}
<fstream>라이브러리를 활용한 파일 입출력에도 위와 같은 방법을 사용한다. 파일 입출력은 뒤에서 다룬다.
std::ifstream input_file("temp.txt");
if(!input_file)
std::cerr << "Can't open file!" << std::endl;
Try-Catch를 활용하여 예외처리하기
- 예외 상황이 발생하여도 프로그램을 작동시키고 싶을 때 try-catch 예외처리를 사용한다. 중단시키는 것도 가능하다.
- 속도가 느려지기 때문에 성능이 중요한 부분에는 사용하지 않음을 권한다.
- throw의 자료형과 일치하는 catch가 없을 경우 프로그램은 중단된다.
try : 예외 상황이 발생할 것 같은 코드
throw : 예외 상황이 발생했을 때 알리는 명령어
catch : throw가 발생했을 때 실행되는 코드
double x;
x = -1.23;
//x = 1.23;
try {
if(x < 0.0) throw string("Negative Value is came in");
cout << sqrt(x) << endl;
}
catch (string error_message) {
cout << "In catch socpe! ";
cout << error_message << endl;
}
In catch socpe! Negative Value is came in
위 예제 throw에서 string 대신 char*를 사용하면 catch가 실행되지 않는다. 일반 함수는 char*를 string으로 유연하게 형변환을 해주지만 try-catch 예외처리 내부에서는 이를 엄격하게 제한한다.
아래 예제를 보자. catch에 char* 자료형을 구현해주면 해당 자료형에 맞게 찾아감을 알 수 있다.
try {
throw "Error Occurred!";
}
catch (const char* error_message) {
cout << "Catching const char*:";
cout << error_message << endl;
}
catch (string error_message) {
cout << "Catching std::string:";
cout << error_message << endl;
}
>> Catching const char*:Error Occurred!
아래 try-catch 예외처리를 보자. throw의 자료형과 catch의 자료형이 다르면 에러가 발생하고 프로그램이 멈춘다.
try {
//throw -1; // Work!
throw -1.0; // Error!
}
catch (int x) {
cout << "Catch Integer:" << x << endl;
}
14.2 예외처리와 스택 되감기
Stack Unwinding
여러번 함수를 중첩하여 호출하면 스택에 쌓인다. 이 때 try-catch 예외처리를 사용하면 어떻게 될까?
void last() {
cout << "In " << __FUNCTION__ << endl;
try { throw -1;}
catch (float) { cerr << __FUNCTION__ << " catch exception\n"; }
cout << __FUNCTION__ << " is done\n";
}
void second() {
cout << "In " << __FUNCTION__ << endl;
try { last(); }
catch (double) { cerr << __FUNCTION__ << " catch exception\n"; }
cout << __FUNCTION__ << " is done\n";
}
void first() {
cout << "In " << __FUNCTION__ << endl;
try { second(); }
catch (int) { cerr << __FUNCTION__ << " catch exception\n"; }
cout << __FUNCTION__ << " is done\n";
}
int main() {
cout << "In " << __FUNCTION__ << endl;
try { first(); }
catch (int) { cerr << __FUNCTION__ << " catch exception\n"; }
cout << __FUNCTION__ << " is done\n";
}
In main
In first
In second
In last
first catch exception
first is done
main is done
throw를 호출하였는데 해당 자료형에 맞는 catch가 없다면 함수 스택밖으로 즉시 나가버린다. last와 second 함수 내부에서 마지막 ~ is done 코드가 실행되지 않았음을 확인하자. 자료형이 맞는 catch가 발견되면 그때부터 나머지 코드를 실행시킨다.
- catch는 해당 자료형이 맞을 때 한 번 실행된다. main 함수에서 catch 내부 코드는 실행되지 않았다.
- 만약 외부에 있는 catch를 실행시키려면 rethrow를 해주면 된다. 실행되는 catch 내부에 throw를 한 번더 실행시키자
참고)
cout : buffer에 담아두고 바로 출력하지않을 때가 있다. flush 혹은 endl이 호출되면 모두 출력한다.
cerr : buffer를 사용하지 않는다. flush 혹은 endl 없이도 바로 출력한다.
ellipsis 모든 자료형을 catch할 수 있다
- 자료형에 맞는 catch가 없다면 프로그램은 중단된다. 어떤 자료형이라도 받을 수 있는 ellipsis를 소개한다.
try {
throw -1;
//throw 'A';
//throw 1.0f;
}
catch (...) {
cerr << "Catch!" << endl;
}
exception specifier
- 함수명 뒤에 exception specifier를 표시하여 해당 함수가 throw를 작동시킴을 프로그래머에게 알릴 수 있다. 이 때 throw의 매개변수에 정확한 자료형을 입력할 수 있으나 컴파일러는 어떤 자료형이 들어오든 ellipsis로 받아들인다. throw 매개변수가 비어있으면 throw가 작동하지 않는다는 의미이다. 잘 사용하지 않는다.
void first() throw(...){
try {
throw -1;
}
catch (...) {
cerr << __FUNCTION__ << " catch exception\n";
}
}
14.3 예외 클래스와 상속
사용자 정의 자료형도 throw로 사용가능하다
- 클래스로 구현하면 멤버함수와 멤버변수를 활용할 수 있다.
class Exception {
public:
void report() { cerr << "Exception Report!" << endl; }
};
class IntArray {
private:
int m_data[5];
public:
int& operator[] (const int& index) {
if (0 < index || index <= 5) throw Exception();
return m_data[index];
}
};
int main() {
IntArray int_arr;
try { int_arr[10]; }
catch (Exception& e) { e.report(); }
}
>> Exception Report!
상속한 클래스도 throw로 사용할 수 있다.
class Exception {
public:
void report() { cerr << "Exception Report!" << endl; }
};
class ArrayException :public Exception {
public:
void report() { cerr << "Array Exception Report!" << endl; }
};
class IntArray {
private:
int m_data[5];
public:
int& operator[] (const int& index) {
if (0 < index || index <= 5) throw ArrayException();
return m_data[index];
}
};
주의할 점) 이 때 부모 클래스가 자식 인스턴스의 throw을 catch할 수 있다. 위 예제를 활용하여 실행한 예제이다.
int main() {
IntArray int_arr;
try{ int_arr[10]; }
catch (Exception& e){ e.report(); }
catch (ArrayException& e){ e.report();}
}
부모 클래스가 자식 인스턴스의 throw를 받아버렸다. 클래스에 맞게 출력되었다면 Array Exception Report!가 맞다.
Exception Report!
(1) 위 상황이 발생하지 않도록 순서를 조정하자. 자식 클래스가 위쪽에 있으면 된다.
int main() {
IntArray int_arr;
try{ int_arr[10]; }
catch (ArrayException& e){ e.report();}
catch (Exception& e){ e.report(); }
}
Array Exception Report!
(2) 가상함수를 활용하자
rethrow의 자료형을 선택하는 방법
- throw + 변수명은 매개변수 자료형을 던진다.
- throw는 원래의 자료형으로 던진다.
아래 testRethrow( )함수 내부에서 throw+변수명과 throw를 번갈아 실행시켜보자.
(1) throw+변수명을 실행하면 상위 객체로 변환되어 던지므로 객체 잘림현상이 나타난다.
(2) throw는 매개변수 자료형으로 변환되기 전 자료형으로 던진다.
class Exception {
public:
void report() { cerr << "Exception Report!" << endl; }
};
class ArrayException :public Exception {
public:
void report() { cerr << "Array Exception Report!" << endl; }
};
class IntArray {
private:
int m_data[5];
public:
int& operator[] (const int& index) {
if (0 < index || index <= 5) throw ArrayException();
return m_data[index];
}
};
void testRethrow() {
IntArray int_arr;
try { int_arr[10]; }
catch (Exception& e) {
e.report();
//throw e;
throw;
}
}
int main() {
try { testRethrow(); }
catch (ArrayException& e) { e.report(); }
catch (Exception& e) { e.report(); }
}
14.4 exception 소개
여러가지 경우를 처리할 수 있고 standard libray와 호환된다. 아래는 string 라이브러리와 함께 사용한 예제이다.
#include <iostream>
#include <exception>
#include <string>
using namespace std;
int main() {
try {
string s;
s.resize(-1);
}
catch (std::exception& e) {
cout << typeid(e).name() << endl;
cerr << e.what() << endl;
}
}
>> class std::length_error
>> string too long
이 때 length_error는 exception을 상속받은 클래스이다. 아래에서 exception 클래스의 상속자를 확인해볼 수 있다.
https://en.cppreference.com/w/cpp/error/exception
string의 멤버함수 resize가 발생시키는 오류는 다음에서 확인할 수 있다.
https://en.cppreference.com/w/cpp/string/basic_string/resize
exception 클래스의 여러 상속자를 활용해보자
try {
throw std::runtime_error("Runtime Error!");
}
catch (std::exception& e) {
cout << typeid(e).name() << endl;
cerr << e.what() << endl;
}
>> class std::runtime_error
>> Runtime Error!
exception 클래스를 직접 상속받아 사용해보자
- 이때 what( )의 상위 멤버함수는 가상함수로 virtual keyword가 붙어있다.
- noexcept keyword는 throw exception을 하지 않겠다는 의미이다.
class customException : public exception {
public: // noexcept: no intention to throw exception
const char* what() const noexcept override{
return "Custom Exception!";
}
};
int main() {
try {
throw customException();
}
catch (std::exception& e) {
cout << typeid(e).name() << endl;
cerr << e.what() << endl;
}
}
>> class customException
>> Custom Exception!
14.5 함수 try
함수 body를 정하지 않고 try-catch 예외처리로 대신할 수 있다. 잘 안쓰인다.
void try_catch_block()
try {
throw -1;
}
catch (...) {
cout << "This function has special body!" << endl;
}
int main() {
try_catch_block();
}
>> This function has special body!
생성자에서 try-catch 사용하기
- 생성자의 body block을 만들지 않고 try-catch block로 대체할 수 있다.
- 생성자는 rethrow를 하지 않아도 외부의 catch가 작동한다.
- 생성자 try-catch block을 만들면 throw가 기본값으로 들어있다.
class A {
private:
int m_x;
public:
A(int x) : m_x(x) {
if(x<=0) throw 1;
}
};
class B : public A{
public:
B(int x) try: A(x){
// do initialization
}
catch (...) {
cerr << "Catch in B constructor" << endl;
//throw; it was commented but work. because it is default
}
};
int main() {
try {
B b(0);
}
catch (...) {
cerr << "Catching it in main" << endl;
}
}
>> Catch in B constructor
>> Catching it in main
14.6 예외처리의 위험성과 단점
(1) 메모리 관리가 되지 않아 메모리누수가 발생할 수 있다
- 아래 코드를 보자. 메모리를 해제하기 전에 throw를 던졌다면 메모리를 해제할 수 없다.
try {
int *ptr = new int[100000];
// do sth with ptr
throw "error";
delete [] ptr;
}
catch (...) {
cerr << "Catching it in main" << endl;
}
해결방법: unique_ptr을 사용하자. unique_ptr은 영역을 벗어나면 자동으로 메모리를 해제한다.
try {
int *ptr = new int[100000];
unique_ptr<int> u_ptr(ptr); // use this
throw "error";
//delete [] ptr; // no needs to delete
}
catch (...) {
cerr << "Catching it in main" << endl;
}
(2) 소멸자 내부에서 throw를 던질 수 없다.
- 사용하면 에러가 발생한다. 아래코드를 실행하고 확인해보자. Error! ~A: function assumed not to throw an exception but does destructor or deallocator has a (possibly implicit) non-throwing exception specification
class A {
public:
~A()
{ throw "Error in destructor"; }
};
int main() {
try
{ A a; }
catch (...)
{ cerr << "Catching it!" << endl; }
}
(3) 빈번하게 호출되는 코드(for 내부)에서 try-catch 예외처리를 사용하면 프로그램이 느려진다
(4) 예외처리는 정말로 예외인 상황에서 사용하자. 사용자가 원하지 않는 입력값을 넣는 상황은 논리적인 구조를 만들어대응함이 바람직하다. 예상할 수 없는 일이 발생할 때 예외처리가 유용하다. 분산-병렬처리, 네트워크-서버, 하드웨어 관련작업, 하드디스크 읽기/쓰기 같은 예이다.
'Programming Language > C++' 카테고리의 다른 글
[홍정모의 따라하며 배우는 C++] 16. 표준 템플릿 라이브러리 (0) | 2022.01.01 |
---|---|
[홍정모의 따라하며 배우는 C++] 15. 의미론적 이동과 스마트 포인터 (0) | 2022.01.01 |
[홍정모의 따라하며 배우는 C++] 13. 템플릿 (0) | 2021.12.30 |
[홍정모의 따라하며 배우는 C++] 12. 가상함수들 (0) | 2021.12.28 |
[홍정모의 따라하며 배우는 C++] 11. 상속 (0) | 2021.12.27 |