[C++ Primer Plus] Chapter 15. (2) 예외
Table of Contents
C++ 기초 플러스 책을 읽고 공부한 노트입니다.
예외 #
- 두 수의 조화 평균(harmonic mean)을 계산하는 함수를 만들어보자.
- 주어진 수들의 역수의 산술 평균을 구한다. 그리고 그것의 역수를 취한다.
- 구하는 수식은 아래와 같다.
- 여기서 x가 y의 부정이면 이 공식은 0으로 나누는, 정의할 수 없는 연산이 된다.
$$ {2.0 \times x \times y \over ( x + y )} $$
- 해결하는 방법 중 하나는, 정의할 수 없는 연산일 때
abort()
를 호출하는 것이다.abort()
는cstdlib
헤더 파일에 들어 있다.abort()
가 호출되면 표준 에러 스트림(cerr
가 사용하는 스트림)에 “abnormal program temination” (비정상적인 프로그램 종료)과 같은 메시지를 보내고 프로그램이 종료된다.- 부모 프로세스나 운영체제의 컴파일러에 종속적인 어떤 값을 리턴한다. 이 때, 파일 버퍼를 비우는지 여부는 C++ 컴파일러 마다 다르다.
- 원한다면,
exit()
을 사용해서 메시지를 출력하지 않고, 파일 버퍼를 비울 수 있다. - 호출하면,
main()
으로 다시 돌아가는 일 없이, 그 프로그램을 직접 종료한다.
double HMean(double x, double y)
{
if (x == -y) // x가 y의 부정이면 0으로 나누게 되므로, abort()를 호출한다.
{
cout << "매개변수들을 HMean()에 전달할 수 없습니다. " << endl;
abort();
}
return 2.0 * x * y / (x + y);
}
- 비정상 종료보다 더 융통성 있는 방법은, 함수의 리턴값을 사용해서 문제가 무엇인지 알리는 것이다.
bool HMean(double x, double y, double* ans)
{
if (x == -y)
{
*ans = DBL_MAX;
return false; // false를 리턴한다.
}
else
{
*ans = 2.0 * x * y / (x + y);
return true;
}
}
- 이제, 예외 매커니즘을 사용해서 문제를 풀어보자. 예시로 보는 게 빠르겠다.
- 만약, 데이터형이 일치하는 예외 핸들러(
catch
구문)가 없다면 프로그램은 기본적으로abort()
를 호출한다. - 이 행동을 사용자가 수정할 수도 있다. (잘못된 예외 파트)
- 만약, 데이터형이 일치하는 예외 핸들러(
double HMean(double x, double y)
{
// 2. throw 키워드로 예외를 발생 시킨다.
if (x == -y) throw "x = -y 는 허용되지 않습니다.";
return 2.0 * x * y / (x + y);
}
int main()
{
double x, y, z;
cout << "두 수를 입력하세요: ";
while (cin >> x >> y)
{
// 1. try 블록에서 예외가 있는지 체크해본다.
try
{
z = HMean(x, y);
}
// 3. catch 블록에서 예외를 처리한다.
catch (const char* s) // 이 핸들러는 문자열로 발생된 예외를 처리한다.
{
cout << s << endl;
cout << "두 수를 새로 입력하세요: ";
continue;
}
// 예외가 발생하지 않았다면 이곳으로 바로 넘어온다.
cout << x << ", " << y << "의 조화평균은 " << z << "입니다. " << endl;
cout << "다른 두 수를 입력하세요(끝내려면 q): ";
}
}
객체를 예외로 사용하기 #
- 기하 평균(geometric mean)
- 두 수의 곱의 제곱근이다.
- 수가 음수이면 안 된다.
// 예외를 클래스로 만든다.
class BadHMean
{
private:
double v1;
double v2;
public:
BadHMean(double a = 0, double b = 0) : v1(a), v2(b) {}
void Mesg()
{
// 문자열을 출력한다.
cout << "HMean()에 x = -y 는 허용되지 않습니다. \n";
}
};
class BadGMean
{
public: // 데이터가 public 이다.
double v1;
double v2;
BadGMean(double a = 0, double b = 0) : v1(a), v2(b) {}
const char* Mesg()
{
// 문자열을 리턴한다.
return "GMean()에 음수는 허용되지 않습니다. \n";
}
};
double HMean(double x, double y)
{
if (x == -y) throw BadHMean(x, y);
return 2.0 * x * y / (x + y);
}
double GMean(double x, double y)
{
if (x < 0 || y < 0) throw BadGMean(x, y);
return sqrt(x * y);
}
int main()
{
double x, y, z;
cout << "두 수를 입력하세요: ";
while (cin >> x >> y)
{
try {
z = HMean(x, y);
cout << x << ", " << y << "의 조화평균은 " << z << "입니다. " << endl;
cout << x << ", " << y << "의 기하평균은 " << GMean(x, y) << "입니다. " << endl;
cout << "다른 두 수를 입력하세요(끝내려면 q): ";
}
catch (BadHMean& bg)
{
bg.Mesg();
cout << "두 수를 새로 입력하세요: ";
continue; // 계속할 수 있다.
}
catch (BadGMean& hg)
{
cout << hg.Mesg();
// public 멤버에 접근한다.
cout << "사용된 값: " << hg.v1 << ", " << hg.v2 << endl;
cout << "죄송합니다. 더 이상 진행할 수 없습니다. 프로그램을 종료합니다." << endl;
break; // 종료한다.
}
}
}
-
스택 풀기(unwinding the stack)
- 예외가 발생하면, 호출한 함수를 타고 계속 되돌아가서
try
와catch
가 있는 곳으로 되돌아간다. - 마치 함수 리턴과 마찬가지다.
- 예외가 발생하면, 호출한 함수를 타고 계속 되돌아가서
-
가장 중요한건, 스택에 올라와 있는 모든 자동 클래스 객체들에 대해 파괴자가 호출된다는 것이다.
return
구문은 해당 함수가 스택에 올려 놓은 객체들만 처리한다.- 하지만
throw
구문은try
구문과throw
구문 사이에 개입된 모든 함수들이 스택에 올려 놓은 객체를 모두 처리한다.
// 생략
class BadHMean {};
class BadGMean {};
double HMean(double x, double y) {}
double GMean(double x, double y) {}
// 객체의 파괴를 보기 위한 클래스
class Demo
{
private:
string word;
public:
Demo(const string& str)
{
word = str;
cout << "Demo가 생성되었다 : " << word << endl;
}
~Demo()
{
cout << "Demo가 파괴되었다 : " << word << endl;
}
void Show() const
{
cout << "Demo가 생존하고 있다 : " << word << endl;
}
};
double Means(double a, double b)
{
double am, hm, gm;
Demo d2("Means()"); // Demo 2
am = (a + b) / 2.0; // 산술 평균
try
{
hm = HMean(a, b);
gm = GMean(a, b);
}
catch (BadHMean& bg) // HMean만 catch한다.
{
cout << "Means()에서 잡힘" << endl;
bg.Mesg();
throw; // main()에 예외를 보낸다.
}
d2.Show();
return (am + hm + gm) / 3.0;
}
int main()
{
double x, y, z;
{
Demo d1("main()"); // Demo 1
cout << "두 수를 입력하세요: ";
while (cin >> x >> y)
{
try
{
z = Means(x, y); // Means() 호출
cout << x << ", " << y << "의 조화평균은 " << z << "입니다. " << endl << endl;
cout << "다른 두 수를 입력하세요(끝내려면 q): ";
}
catch (BadHMean& bg)
{
bg.Mesg();
cout << endl << "두 수를 새로 입력하세요: ";
continue;
}
catch (BadGMean& hg) // GMean은 여기서 catch한다.
{
cout << hg.Mesg();
cout << "사용된 값: " << hg.v1 << ", " << hg.v2 << endl;
cout << "죄송합니다. 더 이상 진행할 수 없습니다. 프로그램을 종료합니다." << endl;
break;
}
}
d1.Show();
}
}
Demo가 생성되었다 : main()
두 수를 입력하세요: 6 12
Demo가 생성되었다 : Means()
Demo가 생존하고 있다 : Means()
Demo가 파괴되었다 : Means() // Means()함수 종료로 파괴되었다.
6, 12의 조화평균은 8.49509입니다.
다른 두 수를 입력하세요(끝내려면 q): 6 -6
Demo가 생성되었다 : Means()
Means()에서 잡힘
HMean()에 x = -y 는 허용되지 않습니다.
Demo가 파괴되었다 : Means() // throw 구문으로 Means()함수가 종료되었다. 파괴자도 호출된다.
HMean()에 x = -y 는 허용되지 않습니다.
두 수를 새로 입력하세요: 6 -8
Demo가 생성되었다 : Means()
Demo가 파괴되었다 : Means() // throw 구문으로 Means()함수가 종료되었다. 파괴자도 호출된다.
GMean()에 음수는 허용되지 않습니다.
사용된 값: 6, -8
죄송합니다. 더 이상 진행할 수 없습니다. 프로그램을 종료합니다.
Demo가 생존하고 있다 : main()
Demo가 파괴되었다 : main() // 프로그램 종료로 파괴되었다.
catch
블록이 참조를 지정할지라도 컴파일러는 언제나 예외가 발생하면 임시 복사본을 만든다.- 왜냐하면, 예외가 발생한 함수 블록이 끝나면 객체는 존재하지 않기 때문이다.
- 추가적으로, 기초 클래스 참조로 파생 클래스를 참조할 수 있기 때문이겠다.
- 파생 순서의 역순으로
catch
블록들을 배치해야 한다.
- 파생 순서의 역순으로
class A {};
class B : public A {};
class C : public B {};
void Test()
{
//...
if (aa) throw A();
if (bb) throw B();
if (cc) throw C();
}
int main()
{
//...
try
{
Test();
}
catch(C& c) { } // C가 먼저 와야 한다.
catch(B& b) { }
catch(A& a) { } // A가 먼저오면 모든 예외를 A가 catch한다.
}
- 어떤 예외라도 포착하는 방법
catch(...) {}
exception 클래스 #
- 예외 클래스의 기초 클래스로
exception
클래스를 사용할 수 있다.exception
헤더파일에 포함되어 있다.what()
이라는 하나의 가상 멤버 함수가 주어진다. 이것을 재정의할 수 있다.
#include <exception>
class BadHMean : public exception
{
public:
// what() 가상함수를 재정의한다.
virtual const char * what()
{
return "HMean()에 x = -y 는 허용되지 않습니다. \n";
}
};
int main()
{
//...
try { }
catch (exception& e) // exception 형식으로 포착한다.
{
cout << e.what() << endl;
}
}
logic_error
클래스와runtime_error
클래스stdexcept
헤더파일에 포함되어 있다.exception
으로 부터public
으로 파생된다.- 다음과 같이,
string
매개변수를 받는 생성자를 가지고 있다.- 이것은
what()
함수의 리턴값으로 사용한다.
- 이것은
class logic_error : public exception
{
public:
explicit logic_error(const string& what_arg);
};
logic_error
클래스- 일반적인 논리 에러들을 서술한다.
domain_error
- 예를들어, 아크사인의 정의역은 -1에서 +1까지이다. 이것을 벗어날 때 이 예외를 발생시킬 수 있다.
- 정의역 (Domain): f: X → Y에서 X를 함수 f의 정의역이라고 한다.
- 치역 (Range): f(x)가 치역이다.
- 예를들어, 아크사인의 정의역은 -1에서 +1까지이다. 이것을 벗어날 때 이 예외를 발생시킬 수 있다.
invalid_argument
- 기대하지 않는 값이 함수에 전달되었을 때.
length_error
- 원하는 액션을 취할 만큼 충분한 공간을 사용할 수 없을 때.
out_of_range
- 인덱싱 에러.
runtime_error
클래스- 실행하는 동안에 나타날 수 있는 에러를 서술한다.
range_error
- 언더플로나 오버플로 없이 계산 결과가 함수의 절절한 치역을 벗어날 때.
overflow_error
- 정수형이나, 부동소수점형 계산에서 나타낼 수 있는 최대 크기보다 더 큰 값을 산출하는 계산을 할 때.
underflow_error
- 예를 들어, 부동소수점 계산에서 나타낼 수 있는 최소 크기보다 더 작은 값을 산출하는 계산을 할 때.
- C++이
new
를 사용할 때 일어나는 메모리 대입 문제를 해결하는 두 가지 방법- (1)
new
가 널 포인터를 리턴한다. - (2)
new
가bad_alloc
예외를 발생시킨다.
- (1)
struct Big
{
double stuff[20000];
};
int main()
{
Big* pb;
try
{
cout << "큰 메모리 블록 대입을 요청합니다. " << endl;
pb = new Big[10900]; // 큰 메모리를 대입한다.
cout << "요청을 통과하였습니다. " << endl;
}
catch (bad_alloc& ba) // new가 bad_alloc 예외를 발생시킨다. (2)
{
cout << "예외가 감지되었습니다!" << endl;
cout << ba.what() << endl;
exit(EXIT_FAILURE);
}
cout << "메모리 블록이 성공적으로 대입되었습니다. " << endl;
pb[0].stuff[0] = 4;
cout << pb[0].stuff[0] << endl;
delete[] pb;
}
큰 메모리 블록 대입을 요청합니다.
예외가 감지되었습니다!
bad allocation // ba.what()를 출력한 결과
int main()
{
Big* pb;
cout << "큰 메모리 블록 대입을 요청합니다. " << endl;
pb = new (nothrow) Big[10900]; // new가 널 포인터를 리턴한다. (1)
if (pb == nullptr)
{
cout << "메모리 블록 대입에 실패하였습니다. " << endl;
exit(EXIT_FAILURE);
}
cout << "메모리 블록이 성공적으로 대입되었습니다. " << endl;
pb[0].stuff[0] = 4;
cout << pb[0].stuff[0] << endl;
delete[] pb;
}
큰 메모리 블록 대입을 요청합니다.
메모리 블록 대입에 실패하였습니다.
예외, 클래스, 상속 #
- 예외, 클래스, 상속은 서로 상호작용한다.
// Sales.h
#include <stdexcept>
#include <string>
using namespace std;
class Sales
{
public:
enum { MONTHS = 12 };
// 클래스 정의 안에 내포된 예외 클래스.
class bad_index : public logic_error
{
private:
int bi;
public:
explicit bad_index(int ix, const string& s = "Sales 객체에서 인덱스 에러 발생! \n");
int bi_val() const { return bi; }
virtual ~bad_index() throw() {}
};
explicit Sales(int yy = 0);
Sales(int yy, const double* gr, int n);
virtual ~Sales() { }
int Year() const { return year; }
virtual double operator[](int i) const;
virtual double& operator[](int i);
private:
double gross[MONTHS];
int year;
};
class LabeledSales : public Sales
{
public:
class nbad_index : public Sales::bad_index // 예외 클래스를 파생시킨다.
{
private:
string lbl;
public:
nbad_index(const string& lb, int ix, const string& s = "LabeledSales 객체에서 인덱스 에러 발생! \n");
const string& label_val() const { return lbl; }
virtual ~nbad_index() throw() {}
};
explicit LabeledSales(const string& lb = "none", int yy = 0);
LabeledSales(const string& lb, int yy, const double* gr, int n);
virtual ~LabeledSales() { }
const string& Label() const { return label; }
virtual double operator[](int i) const;
virtual double& operator[](int i);
private:
string label;
};
// Sales.cpp
// ...
// 배열에서 벗어나는 인덱스에 접근하면 예외가 발생한다.
double Sales::operator[](int i) const
{
if (i < 0 || i >= MONTHS) throw bad_index(i);
return gross[i];
}
double& Sales::operator[](int i)
{
if (i < 0 || i >= MONTHS) throw bad_index(i);
return gross[i];
}
double LabeledSales::operator[](int i) const
{
if (i < 0 || i >= MONTHS) throw nbad_index(Label(), i);
return Sales::operator[](i);
}
double& LabeledSales::operator[](int i)
{
if (i < 0 || i >= MONTHS) throw nbad_index(Label(), i);
return Sales::operator[](i);
}
// ...
int main()
{
double vals1[12] =
{
1220, 1100, 1122, 2212, 1232, 2334,
2884, 2393, 3302, 2922, 3002, 3544
};
double vals2[12] =
{
12, 11, 22, 21, 32, 34,
28, 29, 33, 29, 32, 35
};
Sales sales1(2011, vals1, 12);
LabeledSales sales2("Blogstar", 2012, vals2, 12);
cout << "첫번째 try 블록\n";
try
{
cout << sales2[12] << endl; // 배열을 넘어가는 인덱스에 접근한다. (LabeledSales)
}
catch (LabeledSales::nbad_index& bad)
{
cout << bad.what();
cout << "Company: " << bad.label_val() << endl;
cout << "bad index: " << bad.bi_val() << endl;
}
catch (Sales::bad_index& bad)
{
cout << bad.what();
cout << "bad index: " << bad.bi_val() << endl;
}
cout << "\n두번째 try 블록\n";
try
{
sales1[20] = 23345; // 배열을 넘어가는 인덱스에 접근한다. (Sales)
}
catch (LabeledSales::nbad_index& bad)
{
cout << bad.what();
cout << "Company: " << bad.label_val() << endl;
cout << "bad index: " << bad.bi_val() << endl;
}
catch (Sales::bad_index& bad)
{
cout << bad.what();
cout << "bad index: " << bad.bi_val() << endl;
}
}
잘못된 예외 #
-
예외가 발생한 후에도 문제를 일으킬 수 있는 두 가지 가능성이 있다.
- 1. 기대하지 않는 예외(unexpected exception)
- 발생한 예외는 예외 지정자 리스트에 있는 데이터형들 중의 어느 하나와 일치해야한다.
- 만약 일치하지 않으면, 기본적으로 프로그램이 중지된다.
- 2. 포착되지 않는 예외(uncaught exception)
- 예외는 반드시 포착되어야 한다.
- 만약 포착되지 않으면, (
try
나catch
구문이 없을 때) 기본적으로 프로그램이 중지된다.
- 1. 기대하지 않는 예외(unexpected exception)
-
이 두 가지 예외가 발생했을 때, 프로그램이 중지가 아닌 다른 응답을 하도록 사용자가 바꿀 수 있다.
2. 포착되지 않는 예외
- 이 예외가 발생하면
terminate()
함수가 호출되고, 이 함수가abort()
를 호출한다. terminate()
함수가abort()
말고 다른 함수를 호출하도록 바꿀 수 있겠다.terminate()
함수의 행동을 바꾸는set_terminate()
함수를 사용하면 된다.terminate()
함수와set_terminate()
함수는exception
헤더 파일에 선언되어 있다.
#include <exception>
using namespace std;
// 포착되지 않는 예외 발생 시 이렇게 행동하라.
void myQuit() // 형식은 매개변수와 리턴형이 void여야한다.
{
cout << "포착되지 않는 예외가 발생하여 프로그램을 중지시킵니다. " << endl;
exit(5); // 종료 상태값 5를 가지고 exit() 함수를 호출한다.
}
int main()
{
set_terminate(myQuit); // 함수를 설정한다.
throw; // 포착되지 않는 예외.
}
1. 기대하지 않는 예외
-
이 예외가 발생하면
unexpected()
함수가 호출되고, 이 함수가terminate()
를 호출한다. 그리고terminate()
가abort()
를 호출한다. -
unexpected()
함수의 행동을 바꾸는set_unexpected()
함수를 사용하면 된다. -
이 함수들도
exception
헤더 파일에 선언되어 있다. -
set_unexpected()
함수의 선택지- (1)
terminate()
또는abort()
또는exit()
을 사용해서 프로그램을 종료시킬 수 있다. - (2) 새로운 예외를 발생시킬 수 있다.
- 예외가 이전과 일치하면, 새로 발생된 예외와 일치하면
catch
블록을 찾는다.
- 예외가 이전과 불일치하고, 이전 예외 지정에
bad_exception
형이 없으면terminate()
를 호출한다.
- 예외가 이전과 불일치하고, 이전 예외 지정에
bad_exception
형이 있으면bad_exception
형의 예외로 대체된다.
- 예외가 이전과 일치하면, 새로 발생된 예외와 일치하면
- (1)
// **** 예상대로 작동하지 않아서 확인이 필요합니다. ***
void myUnexpected()
{
throw; // 새로운 예외를 발생시킨다.
}
double Argh(double, double) throw(out_of_range, bad_exception) // out_of_range, bad_exception의 예외를 발생시킨다.
{
throw length_error("length_error");
}
int main()
{
set_unexpected(myUnexpected);
try
{
int x = Argh(1.0, 2.0);
}
catch (out_of_range& ex)
{
cout << "out_of_range" << endl;
}
catch (bad_exception& ex)
{
cout << "bad_exception" << endl; // 예외 지정에 bad_exception이 있으므로 이것이 실행된다.
}
}
예외 주의사항 #
- 예외를 사용하면 프로그램의 크기가 커지고, 실행 속도가 떨어진다.
- 예외 지정들은 템플릿과는 잘 어울리지 않는다.
- 왜냐하면, 템플릿은 특수화에 따라 서로 다른 종류의 예외를 발생시킬 수 있기 때문이다.
- 예외와 동적 메모리 대입도 항상 잘 어울리는 것은 아니다.
void Test()
{
int* ar = new int[10];
throw; // 예외 발생!
delete[] ar; // 실행되지 않는다!
return;
}
- 위와 같은 코드에서 메모리 누수를 다음 코드와 같이 해결할 수 있다.
- 하지만, 실수로 무언가를 빠트리거나 다른 에러를 저지를 가능성을 높인다.
- 또 다른 해결책은
auto_ptr
을 사용하는 것이다.
void Test()
{
int* ar = new int[10];
try
{
throw exception();
}
catch (exception& ex)
{
delete[] ar; // 메모리를 해제하고
throw; // 한번 더 예외를 발생시킨다.
}
delete[] ar;
return;
}
- 예외 지정(expection specification)
- 함수에서 발생 가능한 예외의 종류를 명확하게 지정할 수 있다.
- 하지만 예외지정이 의도대로 지정되지 않는 문제가 있어서 C++11표준은 예외 지정을 사용하지 말 것을 권고한다.
- 그러나 예외를 발생하지 않는 함수에 대해서 명시할 필요가 있다고 판단하였고,
noexcept
키워드를 추가하였다.
void Function1() throw(bad_dog) // bad_dog 타입 예외를 발생시킨다.
void Function2() throw() // 예외를 발생시키지 않는다.
void Function3() noexcept // 예외를 발생시키지 않는다.