상세 컨텐츠

본문 제목

C++ 예외 처리

C++/C++98

by deulee 2023. 8. 8. 16:43

본문

1. 예외 처리

C++은 언어 차원에서 예외 처리 문법을 제공한다.

  • try
    • 예외가 발생할만한 코드 블록을 지정함. "try{}" 괄호안에 예외 처리 대상 코드를 작성한다. 이 블록 안에서 예외가 발생했을 때 "throw" 명령으로 예외를 던진다.
  • throw
    • 프로그램이 정상적으로 실행될 수 없는 상황일 때 이 명령으로 예외를 던진다. "throw" 다음에 던지고자 하는 예외를 적는다. 이를 통해 "catch"문으로 점프하게 되며 "throw" 명령 아래쪽의 코드들은 모두 무시되며 곧바로 예외 처리 구문으로 이동한다.
  • catch
    • "try" 블록 다음에 이어지며 던져진 예외를 받아서 처리한다. 그래서 "catch" 블록을 예외 핸들러라고 한다. "catch" 다음에는 받고자 하는 예외의 타입을 적는데 이 객체는 "throw"에 의해 던져진다. "catch" 블록에는 예외를 처리하는 코드가 작성된다.

예제 코드

#include <iostream>

int main(void)
{
	int x;
	try {
		std::cin >> x;
		if (x < 0)
			throw x;
		else if (x == 0)
			throw "0은 안됩니다.";
	}
	catch(int a) {
		std::cerr << a << "는 음수이다" << std::endl;
		exit(-1);
	}
    //int y; // 에러
	catch(const char* message)
	{
		std::cerr << message << std::endl;
		exit(-1);
	}
	std::cout << x << "의 제곱은 " << x * x << "입니다." << std::endl;
	return 0;
}

하나의 "try" 블록에서 위와 같이 타입이 다른 여러 개의 예외를 발생시킬 수도 있는데 이때는 예외의 타입수만큼의 "catch"를 "try" 블록 다음에 나열하면 된다. 각 "catch" 문들은 모두 "try"와 한 덩어리므로 중각에 다른 문장이 끼어들어선 안된다.

 

즉, catch는 throw에 의해 호출되는 함수에 비유될 수 있으며 함수가 오버로딩될 수 있듯이 catch도 여러 가지 예외 타입에 따라 오버로딩될 수 있다.

 

그렇다고 catch가 함수는 아니다. throw는 호출이 아니라 분기문인 goto와 더 가깝다.


2. 함수와 예외 처리

보통 예외를 던지는 throw는 보통 try 블록 내부에 있어야 한다. 하지만 함수 안에서는 throw 만 있을 수도 있다.

 

이때는 함수를 호출하는 호출원이 try 블록을 가져야 하는다. 다음 예제를 보도록 하자.

#include <iostream>

void divide(int a, int d)
{
	if (d == 0)
		throw "0으로는 나눌 수 없다";
	std::cout << "result : " << a / d << std::endl;
}

int main(void)
{
	try {
		divide(10, 0);
	}
	catch(const char* message) {
		std::cerr << message << std::endl;
	}

	divide(10, 2);
	// divide(10, 0); // 디폴트 프로그램 강제 종료
	/*
	try {
		divide(20, 0);
	}
	catch(int a) {
		std::cerr << a << "번 실패" << std::endl;
	}
	*/ // 프로그램 강제 종료
	return 0;

	return 0;
}
  1. 첫 번째 divide(10, 0);
    • catch문을 찾아 잘 점프됨
  2. 두 번째 divide(10, 2);
    1. 잘 실행됨
  3. 세 번째 divde(10, 0);
    1. 예외는 던져졌으나 try 블록에 있지 않아 예외를 처리할 수 없어 디폴트 처리되어 프로그램이 강제 종료됨
  4. 네 번재 divde(20, 0);
    1. try 블록 안에 있고 catch도 있지만 divide가 던지는 char * 타입의 catch가 없으므로 프로그램 강제 종료됨.

 

이렇게 위의 예시를 보았지만 여기에는 굉장히 중요한 원리가 숨겨져 있다.

 

중요

함수가 호출될 때는 스택에 각 함수의 스택 프레임이 생성되며 스택 프레임에는 함수 실행에 필요한 여러 가지 정보들이 저장된다. 그리고, 함수가 리턴할 때 스택 프레임은 정확하게 호출 전의 주소(상태)로 돌아가도록 되어 있다.

 

하지만 예외가 발생했을 때 호출원의 catch로 곧바로 점프해 버리면 스택이 항상성을 읽어 버리므로 이후 프로그램이 제대로 실행될 수 없다.

 

그래서 "throw"는 호출원으로 돌아가기 전에 "자신"과 자신을 호출한 "함수의 스택"을 모두 정리하고 돌아가는데 이를 "스택 되감기(Stack Unwinding)"이라고 한다.

 

그래서 첫 번째 divide가 호출되고 나서 다음의 구문들이 정상적으로 처리될 수 있던 이유도 스택 되감기가 실행되었기 때문이다.

 

그리고 이때 스택의 원리에 맞게 후입선출의 원리로 위쪽 함수 스택 프레임을 찾아 올라가며 호출 스택을 차례대로 정리한다. 이때 지역적으로 선언한 객체들도 정상적으로 파괴된다.

 

즉, 파괴자(소멸자)도 호출되는 것이다.

 

#include <iostream>

class C
{
public:
	C()
	{
		std::cout << "constructor called" << std::endl;
	}
	~C()
	{
		std::cout << "destructor called" << std::endl;
	}
};

void divide(int a, int d)
{
	if (d == 0)
		throw "0으로는 나눌 수 없다";
	std::cout << "result : " << a / d << std::endl;
}

void cal()
{
	C c;
	divide(10, 0);
}


int main(void)
{
	try {
		cal();
	}
	catch(const char* message) {
		std::cerr << message << std::endl;
	}
	return 0;
}

출력 결과

호출 순서는 main() -> cal() -> divide()이고 스택 되감기는 divide() -> cal() -> main() 순서가 된다.

 

그리고 위의 출력결과를 보면 catch 블록으로 점프되기 전에 파괴자가 정상적으로 출력된 것을 볼 수 있다.

 

파괴자는 이때 단순히 메모리만 정리하는게 아니라 때로는 DB 연결 해제, 프로그램 상태 변경 등의 중요한 일을 할 수 있으므로 반드시 호출되어야 한다.

 

즉, 스택 되감기는 중요하다.


3. 중첩 예외 처리

예외 처리 구문은 중첩도 된다.

#include <iostream>
#include <cstring>

int main(void)
{
	int num;
	char str[25];
	int wow;

	try {
		std::cin >> num;
		if (num < 0)
			throw num;
		try {
			std::cin >> str;
			if (strlen(str) < 4)
				throw "너무 짧음";
			std::cin >> wow;
			if (wow < 0)
				throw wow;
		}
		catch(const char* str) {
			std::cerr << str << std::endl;
		}
		catch(int) {
			throw;
		}
	}
	catch(int n) {
		std::cerr << n << "은 음수다" << std::endl;
	}
	return 0;
}

중첩하였을 경우 문제 없이 작동이 되며 그리고 내부에 int 타입을 받았을 때 외부로 던지는 것이 보이는가?

 

이때 안쪽에서 정수형 예외를 처리하기에 부적당하다거나 아니면 이미 바깥쪽에서 같은 종류의 예외를 처리하고 있다면 안쪽의 catch에서는 이 예외를 직접 처리하지 않고 바깥쪽의 예외 처리기에게 넘기는 것이 가능하다.

 

그리고 다시 던질 때는 예외 객체를 지정할 필요없이 "throw;" 명령만 단독으로 사용한다.

 

그런데 이때, 바깥쪽에 적절한 catch가 없으면 디폴트 처리되어 프로그램이 강제로 종료된다.

 

'C++ > C++98' 카테고리의 다른 글

C++ try 블록 함수  (0) 2023.08.08
C++ 예외 객체  (0) 2023.08.08
C++ 클래스 템플릿  (0) 2023.08.08
C++ 함수 템플릿  (0) 2023.08.08
C++ 순수 가상 함수  (0) 2023.08.07

관련글 더보기