[C++] 예외 처리, try & catch

김형태·2021년 5월 11일
0

C++

목록 보기
11/13
post-custom-banner

1. 예외 상황이란?

C++에서 말하는 '예외(exception)'는 프로그램의 실행 도중에 발생하는 문제상황을 의미한다. ㅋ따라서 컴파일 시 발생하는 문법적인 에러는 예외의 범주에 포함되지 않는다. 예를 들어,

  • 나이를 입력하라고 했는데, 0보다 작은 값이 입력됨.
  • 나눗셈을 위한 두 개의 정수를 입력 받는데, 나누는 수로 0이 입력되었다.

이렇듯 '예외'라는 건 문법적인 오류가 아닌, 프로그램의 논리에 맞지 않는 상황을 의미한다.

2. if문을 이용한 예외 처리

우리가 알고 있는 예외의 처리는 if 문을 사용하는 것이다. if문을 통해 예외 상황의 발생유무를 확인한 후 그에 따른 처리를 진행하는 것이 우리가 아는 예외 처리 방식이다.

if문을 이용한 예외 처리에는 문제가 있다. 다음 코드를 살펴보자.

#include <iostream>

int main(void)
{
	int num1, num2;
    std::cout << "두 개의 숫자 입력" << std::endl;
    std::cin >> num1 >> num2; // 7 0 입력 시 예외 발생
    
    if (num2 == 0) // 예외 발견 위치
    {
    	std::cout << "나누는 수는 0이 될 수 없습니다" << std::endl; // 예외 처리 위치
        std::cout << "프로그램을 다시 실행하세요." << std::endl; // 예외 처리 위치
    }
    else
    {
    	std::cout << "나눗셈의 몫: " << num1/num2 << std::endl;
        std::cout << "나눗셈의 나머지: " << num1%num2 << std::endl;
    }
    return (0);
}

위 프로그램을 다음과 같이 실행할 수 있다.

두개의 숫자 입력: 7 0
나누는 수는 0이 될 수 없습니다.
프로그램을 다시 실행하세요.

위와 같이 '예외가 발견되는 위치'와 '예외가 발생되는 위치'가 다를 수 있다. 그리고 위와 같은 예외 처리 방식은 예외처리를 위한 코드와 프로그램의 흐름을 구성하는 코드를 쉽게 구분할 수 없다. if문이 예외 처리 뿐만 아니라 논리적인 기능의 완성을 위해서도 사용되기 때문이다. 그래서 C++에서는 다른 예외 처리 메커니즘을 사용한다.

3. try & catch

C++은 구조적으로 예외를 처리할 수 있는 메커니즘을 제공한다.

예외 처리 메커니즘과 관련하여 익숙해져야 할 세 가지 키워드는 다음과 같다.
try: 예외를 발견한다
catch: 예외를 잡는다
throw: 예외를 던진다

  • try 블록은 예외 발생에 대한 검사의 범위를 지정할 때 사용된다. try블록 내에서 예외가 발생하면, C++의 예외처리 메커니즘에 의해 처리가 된다.
try
{
	// 예외 발생 예상 지역
}
  • catch 블록은 try 블록에서 발생한 예외를 처리하는 코드가 담기는 영역으로, 반환형 없는 함수와 유사하다.
catch(처리할 예외 종류 명시)
{
	// 예외 처리 코드 삽입
}

사실 try와 catch는 하나의 문장으로, 항상 이어서 등장해야 한다. 중간에 다른 문장이 오면 안 된다!

try
{
	...
}
std::cout << "test" << std::endl; // 컴파일 에러!
catch(e)
{
	...
}
  • throw는 예외가 발생했음을 알리는 문장 구성에 사용된다.
	throw expn;

위 세 키워드를 묶으면 다음과 같은 문장으로 정리할 수 있다.

"throw에 의해 던져진 '예외 데이터'는, '예외 데이터'를 감싸는 try블록에 의해 감지가 되어 이어서 등장하는 catch 블록에 의해 처리된다."

3.1. try & catch 를 이용한 예외 처리 예시

#include <iostream>

int main(void)
{
	int num1, num2;
    std::cout << "두 개의 숫자 입력" << std::endl;
    std::cin >> num1 >> num2; // 7 0 입력 시 예외 발생
    
    try
    {
    	if (num2 == 0) // 예외 발견 위치
    		throw num2;
        std::cout << "나눗셈의 몫: " << num1/num2 << std::endl;
        std::cout << "나눗셈의 나머지: " << num1%num2 << std::endl;
    }
    catch (int expn)
    {
    	std::cout << "나누는 수는 0이 될 수 없습니다" << std::endl; // 예외 처리 위치
        std::cout << "프로그램을 다시 실행하세요." << std::endl; // 예외 처리 위치
    }
    return (0);
}

위 코드를 실행하면 다음과 같다.

두 개의 숫자 입력 7 0
나누는 수는 0이 될 수 없습니다.
프로그램을 다시 실행하세요.

try 블록에서 예외가 발생하면, 예외가 발생한 지점 이후를 실행하는 것이 아니라, catch 블록 이후가 실행된다.

그러므로 try 블록을 묶을 때에는, 예외가 발생할 만한 영역만 묶는 게 아니라 그와 관련된 모든 문장을 함께 묶어서 이를 하나의 '일(work)'의 단위로 구성해야 한다.

3.2. 스택 풀기(stack unwinding)

다음 코드를 보자. 다음 코드는 예외가 발생할 수 밖에 없는 코드이다.

#include <iostream>

void simpleFuncOne(void);
void simpleFuncTwo(void);
void simpleFuncThree(void);

int		main(void)
{
	try
    {
    	simpleFuncOne(void);
    }
    catch(int expn)
    {
    	std::cout << "error code: " << std::endl;
    }
    return (0);
}

void simpleFuncOne(void)
{
	std::cout << "simpleFunOne" << std::endl;
    simpleFuncTwo();
}

void simpleFuncTwo(void)
{
	std::cout << "simpleFunTwo" << std::endl;
    simpleFuncThree();
}

void simpleFuncThree(void)
{
	std::cout << "simpleFunThree" << std::endl;
    throw -1;
}

simpleFuncOne
simpleFuncTwo
simpleFuncThree
error code: -1

simpleFuncOne에서는 simpleFuncTwo를 호출하고, simpleFuncTwo에서는 simpleFuncThree를 호출한다. 그리고 simpleFuncThree에서 발생한 예외는 호출된 순서의 역순으로 전달된다. 그 예외의 처리는 main의 catch 블록에서 처리된다. 스택이 쌓였다가 풀리는 것과 같다. 그러므로 예외 데이터의 전달을 가리켜 '스택 풀기(스택의 반환)'라고 한다.

만약 main에서 예외를 처리하지 않으면(simpleFuncOne이 try & catch 블록으로 감싸져 있지 않다면) 어떻게 될까? 바로 프로그램을 종료시키는 함수(terminate 함수)가 호출되며 프로그램이 종료된다.

3.2.1. 자료형의 불일치

예외 데이터와 catch의 매개변수 형은 일치해야 한다. 다음과 같은 상황에선 어떤 일이 벌어질까?

int simpleFunc(void)
{
	...
    try
    {
    	if (...)
        	throw -1; // 예외 데이터는 int
    }
    catch(char expn) // catch의 매개변수형은 char
    {
    	...
	}
    ...
}

이 경우에는 자료형의 불일치로 catch블록으로 값이 전달되지 않고, simpleFunc를 호출한 영역으로 예외 데이터가 전달된다!

3.3. 하나의 try 블록과 다수의 catch 블록

전달되는 예외 데이터의 형식이 catch 블록의 매개변수형과 일치해야 하므로, 여러개의 catch 블록이 와도 매개변수를 통해 구분할 수 있으므로 하나의 try 블록애 매개변수형이 다른 여러개의 catch 블록이 붙을 수 있다!

3.4. 전달되는 예외의 명시

함수 내에서 발생할 수 있는 예외의 종류도 함수의 특징으로 간주된다. 따라서 함수를 정의할 때 다음과 같이 함수 내에서 발생 가능한 예외의 종류를 명시할 수 있다.

int throwFunc(int num) throw (int, char)
{
	...
}

위 함수 int와 char형의 예외 데이터를 던진다고 명시했으므로, 다음과 같이 호출할 수 있다.

try
{
	...
    throwFunc(20);
    ...
}
catch(int expn)
{
	...
}
catch(char expn)
{
	...
}

위와 같이 코드를 작성하면, 함수로부터 int형 예외 데이터와 char형 예외 데이터만이 전달되어야 하며, 다른 자료형의 예외 데이터가 전달되면 terminate 함수가 호출되어 프로그램이 종료된다!

만약 예외 데이터를 명시하는 부분이 비어있다면,

int throwFunc(int num) throw ()
{
	...
}

어떠한 예외도 전달하지 않음을 의미하며, 위 함수가 예외를 전달할 경우 프로그램이 종료된다.

함수에 선언된 것 이외의 예외 데이터가 전달되었다는 것은, 프로그래머의 실수가 있었거나 프로그래머도 예상치 못했던 예외가 발생해서 이에 대한 대비가 되지 않았음을 뜻한다. 그러므로 예외 데이터를 명시해주고 프로그램이 종료되는, 발생 가능한 예외를 정확히 점검하도록 하자.

profile
steady
post-custom-banner

0개의 댓글