Skip to main content

[C++ Primer Plus] Chapter 15. (2) 예외

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)

    • 예외가 발생하면, 호출한 함수를 타고 계속 되돌아가서 trycatch가 있는 곳으로 되돌아간다.
    • 마치 함수 리턴과 마찬가지다.
  • 가장 중요한건, 스택에 올라와 있는 모든 자동 클래스 객체들에 대해 파괴자가 호출된다는 것이다.

    • 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)가 치역이다.
    • invalid_argument
      • 기대하지 않는 값이 함수에 전달되었을 때.
    • length_error
      • 원하는 액션을 취할 만큼 충분한 공간을 사용할 수 없을 때.
    • out_of_range
      • 인덱싱 에러.

  • runtime_error 클래스
    • 실행하는 동안에 나타날 수 있는 에러를 서술한다.
    • range_error
      • 언더플로나 오버플로 없이 계산 결과가 함수의 절절한 치역을 벗어날 때.
    • overflow_error
      • 정수형이나, 부동소수점형 계산에서 나타낼 수 있는 최대 크기보다 더 큰 값을 산출하는 계산을 할 때.
    • underflow_error
      • 예를 들어, 부동소수점 계산에서 나타낼 수 있는 최소 크기보다 더 작은 값을 산출하는 계산을 할 때.

  • C++이 new를 사용할 때 일어나는 메모리 대입 문제를 해결하는 두 가지 방법
    • (1) new가 널 포인터를 리턴한다.
    • (2) newbad_alloc 예외를 발생시킨다.
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)
      • 예외는 반드시 포착되어야 한다.
      • 만약 포착되지 않으면, (trycatch 구문이 없을 때) 기본적으로 프로그램이 중지된다.
  • 이 두 가지 예외가 발생했을 때, 프로그램이 중지가 아닌 다른 응답을 하도록 사용자가 바꿀 수 있다.


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형의 예외로 대체된다.
// **** 예상대로 작동하지 않아서 확인이 필요합니다. ***

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   // 예외를 발생시키지 않는다.