Programming Language/C++

[C++] 예외 처리(Exception handling)

lumana 2024. 6. 28. 20:58

예외 처리(Exception Handling)

  • 예외 Case

    • ex) 파일이 제대로 열리지 않는 경우

    • ex) 데이터 포맷이 맞지 않아 데이터를 받을 수 없는 경우

Factorial을 구하는 함수

#include <iostream>
using namespace std;

int fact(int n) {
    if (n == 0) return 1;
    return n * fact(n - 1);
}

int main() {
    int n;
    cin >> n;
    if (n < 0) {
        cout << n << ": 음수입니다." << endl;
    }
    else {
        cout << n << "!= " << fact(n) << endl;
    }
}
  • 이렇게 사용자가 실수로 음수를 입력할 수 있다.

  • n이 음수인 경우 fact함수가 실행되지 않도록 처리를 해줘야 함

#include <iostream>
using namespace std;

int fact(int n) {
    if (n == 0) return 1;
    return n * fact(n - 1);
}

int main() {
    int n;
    cin >> n;
    try {
        if (n < 0) {
            throw n; // n은 예외 객체
        }
        cout << n << "! = " << fact(n) << endl; 
    }
    catch (int e) {
        cout << e << ": 음수입니다. " << endl;
    }
        // ...
}
  • 예외를 발생할 수 있는 부분을 try 블럭에 넣자

  • 예외가 발생하지 않는다면 cout << n << "! = " << fact(n) << endl; 을 출력하고 catch문 아래로 간다

  • 예외가 발생하면 throw 후 catch문으로 이동하고, throw한 예외 객체를 catch문의 매개변수로 전달하여 catch문 내의 코드를 실행한다

  • 예외에 해당하는 메시지를 만들어서 던져보자

int main() {
    int n;
    cin >> n;
    try {
        if (n < 0) {
            throw to_string(n) + ": 음수입니다."; // n은 예외 객체
        }
        cout << n << "! = " << fact(n) << endl; 
    }
    catch (string e) {
        cout << e << endl;
    }
    // ...
}
  • 여기서 효율을 생각한다면 예외 객체가 이동 생성이 되도록 해줄 수 있다.
int main() {
    /* 생략 */
    catch (const string &e) {
        cout << e << endl;
    }
    // ...
}
  • if문을 사용하지 않고 try-catch를 사용해야 하는 이유?
#include <iostream>
#include <string>
using namespace std;

int fact(int n) {
    if (n == 0) return 1;
    return n * fact(n - 1);
}

int main() {
    int n, r;
    cin >> n >> r;
    try {
        if (n < 0) {
            throw to_string(n) + ": 음수입니다."; // n은 예외 객체
        }
        int a = fact(n);

        if (r < 0) {
            throw to_string(r) + ": 음수입니다."; // n은 예외 객체
        }
        int b = fact(r);

        if (n - r < 0) {
            throw to_string(n - r) + ": 음수입니다."; // n은 예외 객체
        }
        int c = fact(n - r);

        cout << a / b / c << endl; 
    }
    catch (const string &e) {
        cout << e << endl;
    }
    // ...
}
  • 이렇게 factorial을 호출해줄 때마다 throw를 해줘야 한다면 그냥 if문을 사용하는 거에 비해 장점이 하나도 없다

  • 객체를 던지고 받는 과정은 함수 내에서만 일어나는게 아니라, 함수 사이에서도 일어날 수 있다.

#include <iostream>
#include <string>
using namespace std;

int fact(int n) {
    if (n < 0) throw to_string(n) + ": 음수입니다.";
    if (n == 0) return 1;
    return n * fact(n - 1);
}

int main() {
    int n, r;
    cin >> n >> r;
    try {
        int a = fact(n);
        int b = fact(r);
        int c = fact(n - r);

        cout << a / b / c << endl; // 조합(Combination)
    }
    catch (const string &e) {
        cout << e << endl;
    }
    // ...
}
  • fact 함수에서는 예외 객체를 던져주기만 하고, 예외 처리를 의무를 메인 함수에 넘긴다

  • 이렇게 처리 해주면 코드가 되게 간결해진다

  • 조합을 구하는 과정을 comb 함수로 모듈화해보자

#include <iostream>
#include <string>
using namespace std;

int fact(int n) {
    if (n < 0) throw to_string(n) + ": 음수입니다.";
    if (n == 0) return 1;
    return n * fact(n - 1);
}

int comb(int n, int r) {
    int a = fact(n);
    int b = fact(r);
    int c = fact(n - r);
    return a / b / c;
}

int main() {
    int n, r;
    cin >> n >> r;
    try {
        cout << comb(n, r) << endl; 
    }
    catch (const string &e) {
        cout << e << endl;
    }
    // ...
}
  • 조합을 구하는 과정에서 fact()에서 예외가 발생하면 fact() try-catch가 없으므로 comb()로 나오고, comb()에도 try-catch가 없으므로 main으로 나오게 된다. main에 try-catch가 있으므로 main에서 예외처리를 해준다

  • 조합을 여러번 사용자가 구할 수 있도록 while문에 넣어보자

    • while문 안에 try-catch를 넣는 방법과

    • try-catch 안에 while문을 넣는 방법이 있다

  • 두 방법의 차이점에 대해서 확인해보자

int main() {
    int n, r;
    try {
        while (true) {
                cin >> n >> r;
                cout << comb(n, r) << endl; 
        }
    }
    catch (const string &e) {
        cout << e << endl;
    }
    // ...
}
  • try 안에 while문이 있는 경우 while문을 돌 다 예외가 발생하면 while문 밖으로 바로 나가게 된다.

    • 예외가 한 번이라도 발생하면 루프를 빠져나온다
int main() {
    int n, r;
    while (true) {
        try {
            cin >> n >> r;
            cout << comb(n, r) << endl; 
        }
        catch (const string &e) {
            cout << e << endl;
        }
    }
    // ...
}
  • while문 안에 try-catch가 존재하는 경우 예외가 발생해도 while문이 끝나지 않고 계속 루프를 돈다

    • 예외가 발생하더라도 루프를 빠져나오지 않는다
  • catch를 여러 개를 만들 수 있다.

    • 예외를 다루는 클래스를 여러 개를 만들고 예외를 계층적으로 다룰 때, 보통 catch를 여러 개를 만들어 사용한다
int main() {
    int n, r;
    while (true) {
        try {
            throw 123;
            cin >> n >> r;
            cout << comb(n, r) << endl; 
        }
        catch (const string &e) {
            cout << e << endl;
        }
        catch (int e) {
            cout << e << endl;
        }
    }
    // ...
}
  • 프로그램을 실행하면 throw 123을 먼저 만나서 catch (int e)로 진입하게 된다

  • catch에서 잡을 수 없는 에러가 발생한다면?

    • 위 예시에서 double을 throw한다면?
int main() {
    int n, r;
    while (true) {
        try {
            throw 12.3;
            cin >> n >> r;
            cout << comb(n, r) << endl; 
        }
        catch (const string &e) {
            cout << e << endl;
        }
        catch (int e) {
            cout << e << endl;
        }
    }
    // ...
}
  • catch 안으로 들어가지 않아 런타임 에러가 발생한다

  • 다음과 같이 처리할 수도 있다.

int main() {
    int n, r;
    while (true) {
        try {
            throw 12.3;
            cin >> n >> r;
            cout << comb(n, r) << endl; 
        }
        catch (const string &e) {
            cout << e << endl;
        }
        catch (int e) {
            cout << e << endl;
        }
        catch (...) {
            cout << "알 수 없는 예외 발생" << endl;
        }
    }
    // ...
}
  • catch하지 못한다면 swtich문의 default 처럼 catch(...) 으로 진입한다

참조) 두들낙서 C/C++ 강좌