예외 처리의 기본 개념
예외 처리의 필요성
프로그램 실행 중에는 다양한 오류 상황이 발생할 수 있습니다.
이러한 오류를 적절히 처리하지 않으면 프로그램이 비정상적으로 종료되거나 예기치 않은 동작을 할 수 있습니다.
예외 처리는 이러한 오류 상황을 체계적으로 관리하고 대응할 수 있게 해주는 메커니즘입니다.
예외 처리의 주요 장점
- 오류 처리 코드와 정상 실행 코드의 분리
- 오류의 효과적인 전파 및 집중화된 처리
- 오류 상황에 대한 체계적이고 일관된 대응 가능
C++의 예외 처리 기본 구문
C++에서는 try
, catch
, throw
키워드를 사용하여 예외를 처리합니다.
try {
// 예외가 발생할 수 있는 코드
if (someErrorCondition) {
throw SomeException("Error message");
}
} catch (ExceptionType1& e1) {
// ExceptionType1 타입의 예외 처리
} catch (ExceptionType2& e2) {
// ExceptionType2 타입의 예외 처리
} catch (...) {
// 모든 타입의 예외 처리
}
예외 발생시키기 (throw)
throw
키워드를 사용하여 예외를 발생시킬 수 있습니다.
어떤 타입의 객체도 예외로 던질 수 있지만, 일반적으로 예외 클래스의 객체를 사용합니다.
void validateAge(int age) {
if (age < 0) {
throw std::invalid_argument("Age cannot be negative");
}
if (age > 150) {
throw std::out_of_range("Age is unrealistically high");
}
}
표준 예외 클래스
C++ 표준 라이브러리는 <stdexcept> 헤더에 여러 가지 기본 예외 클래스를 제공합니다.
주요 표준 예외 클래스
std::exception
: 모든 표준 예외의 기본 클래스std::runtime_error
: 실행 시간에 감지될 수 있는 오류std::logic_error
: 프로그램의 내부 논리적 오류std::out_of_range
: 범위를 벗어난 접근 시도std::invalid_argument
: 잘못된 인자 전달std::bad_alloc
: 메모리 할당 실패
#include <iostream>
#include <stdexcept>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3};
try {
std::cout << vec.at(5) << std::endl; // 범위를 벗어난 접근
} catch (const std::out_of_range& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}
사용자 정의 예외 클래스
특정 상황에 맞는 예외를 정의하기 위해 사용자 정의 예외 클래스를 만들 수 있습니다.
일반적으로 std::exception
을 상속받아 구현합니다.
class NetworkException : public std::exception {
private:
std::string message;
public:
NetworkException(const std::string& msg) : message(msg) {}
const char* what() const noexcept override {
return message.c_str();
}
};
void connectToServer() {
// 서버 연결 시도
throw NetworkException("Failed to connect to server");
}
예외 재전파
catch
블록에서 예외를 부분적으로 처리한 후, 다시 상위 호출자에게 전달해야 할 때 예외를 재전파할 수 있습니다.
void function1() {
try {
// 일부 코드
throw std::runtime_error("Error in function1");
} catch (const std::exception& e) {
std::cerr << "Caught in function1: " << e.what() << std::endl;
throw; // 예외 재전파
}
}
void function2() {
try {
function1();
} catch (const std::exception& e) {
std::cerr << "Caught in function2: " << e.what() << std::endl;
}
}
예외와 소멸자
소멸자에서 예외가 발생하면 프로그램이 즉시 종료됩니다.
따라서 소멸자는 예외를 던지지 않아야 합니다.
class Resource {
public:
~Resource() noexcept {
try {
// 리소스 정리 작업
} catch (...) {
// 예외 처리 (로깅 등)
}
}
};
함수 try 블록
생성자의 초기화 리스트에서 발생하는 예외를 잡기 위해 함수 try 블록을 사용할 수 있습니다.
class MyClass {
int* ptr;
public:
MyClass(int size) try : ptr(new int[size]) {
// 생성자 본문
} catch (const std::bad_alloc& e) {
std::cerr << "Memory allocation failed: " << e.what() << std::endl;
throw; // 예외 재전파
}
};
noexcept 지정자
함수가 예외를 던지지 않음을 명시하기 위해 noexcept
지정자를 사용할 수 있습니다.
void safeFunction() noexcept {
// 이 함수는 예외를 던지지 않음을 보장
}
void potentiallyUnsafeFunction() noexcept(false) {
// 이 함수는 예외를 던질 수 있음
}
예외 처리의 성능 고려사항
예외 처리 메커니즘은 오류가 발생하지 않을 때는 거의 성능 저하를 일으키지 않습니다.
그러나 예외가 실제로 발생하면 스택 풀기(stack unwinding) 과정 때문에 상당한 오버헤드가 발생할 수 있습니다.
실습 : 간단한 계산기 프로그램
다음 요구사항을 만족하는 간단한 계산기 프로그램을 구현해보세요.
DivisionByZeroException
과InvalidInputException
예외 클래스 정의- 사칙연산 함수 구현 (덧셈, 뺄셈, 곱셈, 나눗셈)
- 0으로 나누기 시도 시
DivisionByZeroException
발생 - 잘못된 입력 시
InvalidInputException
발생
#include <iostream>
#include <stdexcept>
#include <string>
class DivisionByZeroException : public std::runtime_error {
public:
DivisionByZeroException() : std::runtime_error("Division by zero") {}
};
class InvalidInputException : public std::runtime_error {
public:
InvalidInputException(const std::string& msg) : std::runtime_error(msg) {}
};
class Calculator {
public:
static double add(double a, double b) { return a + b; }
static double subtract(double a, double b) { return a - b; }
static double multiply(double a, double b) { return a * b; }
static double divide(double a, double b) {
if (b == 0) {
throw DivisionByZeroException();
}
return a / b;
}
};
int main() {
double a, b;
char operation;
try {
std::cout << "Enter first number: ";
if (!(std::cin >> a)) {
throw InvalidInputException("Invalid first number");
}
std::cout << "Enter operation (+, -, *, /): ";
if (!(std::cin >> operation)) {
throw InvalidInputException("Invalid operation");
}
std::cout << "Enter second number: ";
if (!(std::cin >> b)) {
throw InvalidInputException("Invalid second number");
}
double result;
switch (operation) {
case '+': result = Calculator::add(a, b); break;
case '-': result = Calculator::subtract(a, b); break;
case '*': result = Calculator::multiply(a, b); break;
case '/': result = Calculator::divide(a, b); break;
default: throw InvalidInputException("Unknown operation");
}
std::cout << "Result: " << result << std::endl;
} catch (const DivisionByZeroException& e) {
std::cerr << "Error: " << e.what() << std::endl;
} catch (const InvalidInputException& e) {
std::cerr << "Error: " << e.what() << std::endl;
} catch (const std::exception& e) {
std::cerr << "Unexpected error: " << e.what() << std::endl;
}
return 0;
}
연습 문제
std::vector
를 사용하여 간단한 스택 클래스를 구현하세요. 스택이 비어있을 때 pop 연산을 시도하면 사용자 정의 예외를 던지도록 하세요.- 파일 입출력을 수행하는 함수를 작성하고, 파일을 열 수 없거나 읽기 / 쓰기 오류가 발생할 때 적절한 예외를 던지도록 구현하세요.
참고자료
- C++ 표준 문서의 예외 처리 섹션 : Exception handling
- 표준 예외 클래스 : Standard exception classes
- C++ Core Guidelines의 예외 처리 관련 항목 : E : Error handling
- throw 표현식 : throw expression
- try-catch 블록 : try-catch