18장 예외 처리

2023. 12. 3. 21:29프로그래밍 공부/OOP

프로그램을 작성하는 한 가지 방법은 먼저 특이하거나 잘못된 일이 일어나지 않을 것이라고 가정하는 것이다.

예를 들어, 프로그램이 리스트에서 항목을 제거하면 리스트가 비어 있지 않다고 가정할 수 있다.

프로그램이 항상 계획대로 진행되는 핵심 상황을 위해 작동하게 되면 코드를 추가하여 예외적인 경우를 처리할 수 있다.


C++는 이 접근 방식을 코드에 반영하는 방법을 가지고 있다.

기본적으로 아주 특이한 일이 일어나지 않는 것처럼 코드를 작성한다.

그런 다음 C++ 예외 처리 기능을 사용하여 그러한 특이한 경우에 대한 코드를 추가한다.


예외 처리는 일반적으로 오류 상황을 처리하는 데 사용되지만, 

어쩌면 예외를 보는 더 나은 방법은 예외 상황을 처리하는 방법일 수도 있다.

결국, 만약 당신의 코드가 "오류"를 올바르게 처리한다면, 그것은 더 이상 오류가 아니다.


아마도 예외의 가장 중요한 용도는

함수가 사용되는 방식에 따라 다르게 처리되는 특수한 경우가 있는 함수를 처리하는 것일 것이다.
아마도 함수는 많은 프로그램에서 사용될 것인데, 

어떤 프로그램은 한 가지 방법으로 특수한 경우를 처리하고, 

다른 프로그램은 다른 방법으로 처리할 것이다.

예를 들어, 함수에 0으로 나눗셈이 있을 경우,

함수 호출의 일부에서는 프로그램이 종료되어야 하지만

다른 호출에서는 다른 일이 발생해야 함이 밝혀질 수 있다.

당신은 그러한 함수가 특수한 경우가 발생하면

예외를 던지도록 정의될 수 있다는 것을 알게 될 것이고,

그 예외는 함수 외부에서 특수한 경우를 처리하는 것을 허용할 것이다.

따라서 특수한 경우는 함수의 다른 호출에 대해 다르게 처리될 수 있다.


C++에서 예외 처리는 다음과 같이 진행된다.

1. 예외 던지기(throwing an exception): 어떤 라이브러리 소프트웨어나 코드는 비정상적인 일이 발생했을 때 신호를 보내는 메커니즘을 제공한다.

2. 예외 처리(handling the exception): 예외적인 경우를 처리하는 코드를 프로그램의 다른 위치에 배치한다.

이 프로그래밍 방법은 코드를 더 깨끗하게 만든다.

물론 C++에서 어떻게 하는지에 대한 자세한 설명이 필요하다.

 

 

18.1 예외 처리 기본 사항

예외 처리는 단순한 소개 예에 포함시키는 것이 합리적인 것보다 더 관련된 상황에서 특별히 사용되어야 한다.

그래서 우리는 C++의 예외 처리 세부 사항을 보통 예외 처리를 사용하지 않는 간단한 예를 통해 알려드리겠다.

이것은 C++의 예외 처리 세부 사항에 대해 배우는 데 많은 의미가 있지만,

이 첫 번째 예는 토이 예제라는 것을 잊지 말자.

실제로 그렇게 간단한 것에는 예외 처리를 사용하지 않을 것이다.

 

예외 처리의 토이 예제

예를 들어, 우유가 우리 문화에서 매우 중요한 음식이기 때문에 우유가 거의 떨어지지 않지만, 

여전히 우리는 이러한 일이 일어날 가능성이 거의 없는 상황을 수용하기를 바란다.

우유가 떨어지지 않는다고 가정하는 기본 코드는 다음과 같을 수 있다:

cout << "Enter number of donuts:\n";
cin >> donuts;
cout << "Enter number of glasses of milk:\n";
cin >> milk;
dpg = donuts / static_cast<double>(milk);
cout << donuts << " donuts.\n"
         << milk << " glasses of milk.\n"
         << "You have " << dpg
         << " donuts for each glass of milk.\n";

 

만약 우유가 없다면, 이 코드는 0으로 나눗셈을 포함할 것이고, 이것은 오류이다.

우유가 부족한 이 특별한 상황을 처리하기 위해, 우리는 테스트를 추가할 수 있다.

이 특별한 상황에 대한 테스트가 추가된 전체 프로그램은 디스플레이 18.1에 나와 있다.


디스플레이 18.1의 프로그램은 예외 처리를 사용하지 않는다.

이제 C++ 예외 처리 기능을 사용하여 이 프로그램을 어떻게 다시 작성할 수 있는지 알아보겠다.


디스플레이 18.2에서 예외를 사용하여 디스플레이 18.1의 프로그램을 다시 작성했다.

이것은 단지 토이 예제일 뿐이며, 이 경우에는 예외를 사용하지 않을 것이다.

그러나 이것은 우리에게 작업하기 위한 간단한 예제를 제공한다.

전체적으로 프로그램이 단순하지는 않지만,

적어도 try와 catch 단어 사이의 부분은 더 깨끗해서,

예외를 사용하는 것의 이점을 암시한다.

try와 catch 단어 사이의 코드를 보자.

기본적으로 디스플레이 18.1의 코드와 같지만,

큰 if-else 문(디스플레이 18.1에서 강조 표시됨)이 아닌

다음과 같은 작은 if 문(그리고 일부 단순한 비분기(nonbranching)문)이 있다:
if (milk <= 0)
    throw donuts;

 

이 if문은 만약 우유가 없다면, 특별한 행동을 하라는 것이다.

예외적인 것은 단어 catch 뒤에 주어진다.

아이디어는 정상적인 상황은 단어 try 뒤에 오는 코드에 의해 처리되고,

예외적인 상황은 단어 catch 뒤에 오는 코드에 의해 처리된다는 것이다.

따라서, 우리는 정상적인 경우와 예외적인 경우를 구분했다.

이 장난감 예시에서, 그것은 우리에게 많은 것을 사주지 않지만,

다른 상황에서는 매우 도움이 될 것이다.

자세한 내용을 살펴보자.

 

디스플레이 18.1 예외 처리 없이 특별한 경우의 처리

#include <iostream>
using std::cin;
using std::cout;

int main( )
{
	int donuts, milk;
	double dpg;
	cout << "Enter number of donuts:\n";
	cin >> donuts;
	cout << "Enter number of glasses of milk:\n";
	cin >> milk;
    
	if (milk <= 0)
	{
		cout << donuts << " donuts, and No Milk!\n"
		     << "Go buy some milk.\n";
	}
	else
	{
		dpg = donuts / static_cast<double>(milk);
		cout << donuts << " donuts.\n"
		     << milk << " glasses of milk.\n"
		     << "You have " << dpg
		     << " donuts for each glass of milk.\n";
	}
    
	cout << "End of program.\n";
	return 0;
}

 

샘플 대화 상자

Enter number of donuts:
12
Enter number of glasses of milk:
0
12 donuts, and No Milk!
Go buy some milk.
End of program.

 

디스플레이 18.2 예외 처리를 사용하는 동일한 경우

#include <iostream>
using std::cin;
using std::cout;

int main( )
{
	int donuts, milk;
	double dpg;
    
	try
	{
		cout << "Enter number of donuts:\n";
		cin >> donuts;
		cout << "Enter number of glasses of milk:\n";
		cin >> milk;

		if (milk <= 0)
			throw donuts;
            
		dpg = donuts / static_cast<double>(milk);
		cout << donuts << " donuts.\n"
			<< milk << " glasses of milk.\n"
			<< "You have " << dpg
			<< " donuts for each glass of milk.\n";
	}
	catch(int e)
	{
		cout << e << " donuts, and No Milk!\n"
		    << "Go buy some milk.\n";
	}
    
	cout << "End of program.\n";
	return 0;
}

이것은 C++ 구문을 배우기 위한 토이 예제일 뿐이다.

예외 처리의 전형적인 좋은 사용 예로 받아들이지 말기!

 

샘플 대화 상자 1

Enter number of donuts:
12
Enter number of glasses of milk:
6
12 donuts. 6 glasses of milk.
You have 2 donuts for each glass of milk.
End of program.

 

샘플 대화 상자 2

Enter number of donuts:
12
Enter number of glasses of milk:
0
12 donuts, and No Milk!
Go buy some milk.
End of program.

 

try 블록

C++에서 예외를 처리하는 기본적인 방법은 try-throw-catch 3단 구성이다. 

try 블록은 다음과 같은 구문을 갖는다:
try
{
    Some_Code
}
try 블록(try block): 모든 것이 순조롭게 진행될 때 무엇을 해야 할지 알려주는 기본 알고리즘의 코드를 담고 있다.

모든 것이 차질 없이 진행될 것이라고 100% 확신할 수는 없지만 '해보고 싶다'는 이유로 try 블록이라고 부른다

특이한 일이 발생하면 이를 나타내는 방법은 예외를 적용하는 것이다.
그래서 우리가 throw를 추가할 때 기본적인 개요는 다음과 같다:
try
{
    Code_To_Try
    Possibly_Throw_An_Exception
    More_Code
}


다음은 throw문이 포함된 try 블록의 예이다(디스플레이 18.2에서 복사됨):
try
{
    cout << "Enter number of donuts:\n";
    cin >> donuts;
    cout << "Enter number of glasses of milk:\n";
    cin >> milk;
if (milk <= 0)
    throw donuts;

dpg = donuts / static_cast<double>(milk);
cout << donuts << " donuts.\n"
        << milk << " glasses of milk.\n"
        << "You have " << dpg
        << " donuts for each glass of milk.\n";
}

 

throw문
다음 문장은 int값 donuts를 던진다(throw):
throw donuts;

 

예외

예외(exception): 던져진 값(value thrown)(이 경우 donuts)

 

예외 던지기

예외 던지기(throwing an exception): throw문의 실행

어떤 종류의 값이든 던질 수 있다. 이 경우 int 값을 던진다.

 

throw문

SYNTAX
throw expression_for_value_to_Be_Throw;

throw문이 실행되면, 에워싸는 try 블록의 실행이 중지된다.

try 블록 뒤에 적절한 catch 블록이 뒤따르면,

제어 흐름은 catch 블록으로 전달된다.

throw문은 거의 항상 if문과 같은 분기문에 포함된다.

throw된 값은 임의의 형식이 될 수 있다.
EXAMPLE
if (milk <= 0)
    throw donuts;

 

catch 블록

어떤 것이 "던져지면", 어떤 것이 한 장소에서 다른 장소로 이동한다.

C++에서, 한 장소에서 다른 장소로 이동하는 것은 (던져진 값뿐만 아니라) 제어의 흐름이다.

예외가 던져지면, 주변의 try 블록의 코드는 실행을 멈추고,

catch 블록(catch block)이라고 알려진 코드의 다른 부분이 실행을 시작한다.

catch 블록을 실행하는 것

= 예외를 잡는 것(catching the exception)

= 예외를 처리하는 것(handling the exception)
예외가 던져지면 궁극적으로 어떤 catch 블록에 의해 처리되어야 한다.

디스플레이 18.2에서 적절한 catch 블록은 try 블록 바로 뒤에 있다.

우리는 다음과 같은 방법으로 catch 블록을 반복한다:
catch ( int e)
{
cout << e << " donuts, and No Milk!\n"
        << "Go buy some milk.\n";
}


이 catch 블록은 int형의 매개변수를 가지는 함수 정의와 매우 유사하다.

함수 정의는 아니지만 어떤 면에서 catch 블록은 함수와 같다.

프로그램이 (이전 try 블록 내에서) 다음을 만나면 실행되는 별개의 코드이다:
throw Some_int;

 

그래서 이 throw문은 함수 호출과 비슷하지만 

함수를 호출하는 대신 catch 블록을 호출하여 catch 블록에서 코드를 실행하라고 말한다.

catch 블록은 종종 예외 처리기(exception handler)로 불리는데, 

이는 catch 블록이 함수와 유사한 특성을 가지고 있음을 시사하는 용어이다.

 

catch-블록 매개변수(catch-block parameter)
catch 블록에서 다음 줄에 있는 식별자 e는 무엇일까?
catch(int e)
그 식별자 e는 매개변수처럼 보이고 매개변수와 매우 유사하게 작용한다.

catch-블록 매개변수: catch-블록 표제어에서 식별자. 예) e


각각의 catch 블록은 최대 하나의 catch-블록 매개변수를 가질 수 있다.

catch-블록 매개변수는 다음과 같은 두 가지 일을 한다:
1. catch-블록 매개변수 앞에는

catch 블록이 어떤 종류의 throw된 값을 잡을 수 있는지를 지정하는 형식 이름이 붙는다.
2. catch-블록 매개 변수는 입력된 값의 이름을 제공하므로

해당 값으로 작업을 수행하는 코드를 catch 블록에 기록할 수 있다.

 

catch 블록 매개변수의 이 두 가지 기능을 역순으로 논의할 것이다.
이 절에서는 던져진 값과 잡아진 값의 이름으로

catch-블록 매개변수를 사용하는 방법에 대해 설명한다.

이 장의 뒷부분에 있는 "다중 Throw와 Catch"라는 제목의 절에서는

던져진 값을 처리할 catch 블록(예외 처리기)에 대해 설명한다.

현재 예제에는 하나의 catch 블록만 있다.
catch-블록 매개 변수의 일반적인 이름은 e이지만

e 대신 모든 법적 식별자를 사용할 수 있다.

 

디스플레이 18.2의 catch 블록이 어떻게 작동하는지 알아보겠다.

값을 던지면 try 블록에서 코드의 실행이 종료되고

제어는 try 블록 바로 뒤에 배치된 catch 블록(또는 블록들)으로 전달된다.

 

디스플레이 18.2의 catch 블록은 여기서 재생된다:
catch(int e)
{
    cout << e << " donuts, and No Milk!\n"
             << "Go buy some milk.\n";
}
값을 던지면 이 특정한 catch 블록을 적용하려면 던져진 값이 int 형식이어야 한다.

18.2 그림에서 던져진 값은 변수 donuts에 의해 주어진다.

donuts는 int 타입이기 때문에 이 catch 블록은 던져진 값을 잡을 수 있다.


그림 18.2의 두 번째 표본 대화에서와 같이

donuts = 12이고 milk = 0이라고 가정한다.

milk의 값이 양수가 아니므로 if문 내의 throw문이 실행된다.

이 경우 변수 donuts의 값이 던져진다.

디스플레이 18.2의 catch 블록이 donuts의 값을 잡으면

donuts의 값이 catch 블록 매개변수 e에 연결되고

catch 블록의 코드가 실행되어 다음 출력이 생성된다:

12 donuts, and No Milk!
Go buy some milk.

 

donut의 값이 양수이면 throw문은 실행되지 않는다.

이 경우, 전체 try 블록이 실행된다.

try 블록의 마지막 문장이 실행된 후,

catch 블록 이후의 문장이 실행된다.

예외가 throw되지 않으면, catch 블록은 무시된다.


이 논의는 try-throw-catch 설정이 if-else 문과 동등한 것처럼 들린다.

그것은 던져진 값을 제외하고 거의 동등하다.

try-throw-catch 설정은 분기 중 하나에 메시지를 보내는 기능이 추가된 if-else 문과 같다.

이것은 if-else 문과 크게 다르지 않게 들리지만

실제로는 큰 차이가 있는 것으로 드러났다.

 

좀 더 공식적인 어조로 요약하자면, 

try 블록은 우리가 가정하고 있는 어떤 코드가 throw문을 포함하고 있다.

throw문은 보통 예외적인 상황에서만 실행되지만, 실행되면 어떤 종류의 값을 던진다.

예외(디스플레이 18.2의 donuts 등의 값)가 던져지면 try블록은 종료된다.

try 블록의 나머지 코드는 모두 무시되고 제어는 적절한 catch 블록으로 전달된다.

catch 블록은 바로 앞에 있는 try 블록에만 적용된다.

예외가 던져지면

해당 예외 객체가 catch 블록 매개변수에 연결되고

catch 블록의 문장이 실행된다.

예를 들어 디스플레이 18.2의 대화상자를 보면

사용자가 양수가 아닌 숫자를 입력하는 순간

try 블록이 중지되고 catch 블록이 실행된다.

지금은 모든 try 블록 뒤에 적절한 catch 블록이 이어진다고 가정할 것이다.

적절한 catch 블록이 없을 때 어떤 일이 일어나는지에 대해서는 나중에 논의하겠다.

 

catch-블록 매개변수
catch-블록 매개변수: 던져질 수 있는 예외(값)에 대한 자리 표시자 역할을 하는 캐치 블록의 머리글에 있는 식별자이다. 

이전 try 블록에 적합한 값이 입력되면 해당 값이 catch-블록 매개변수에 연결된다.
(catch 블록을 실행하려면 catch-블록 매개변수에 대해 지정된 형식의 값이어야 한다.) 

catch-블록 매개변수에 대해 합법적인 (예약되지 않은 단어) 식별자를 사용할 수 있다.
EXAMPLE
catch(int e)
{
    cout << e << " donuts, and No Milk!\n"
             << "Go buy some milk.\n";
}
e는 catch-블록 매개변수이다.

 

만약 예외(값 없음)가 try 블록에 던져지지 않으면, 

try 블록이 완료된 후에 catch 블록 뒤의 코드로 프로그램 실행이 계속된다.

즉, 예외가 던져지지 않으면 catch 블록은 무시된다.

프로그램이 실행되는 대부분의 경우,

throw문은 실행되지 않으므로

대부분의 경우 try 블록의 코드는 완료로 실행되고

catch 블록의 코드는 완전히 무시된다.

 

try-throw-catch
예외를 던지고 잡기 위한 기본 메커니즘은 try-throw-catch 순서이다.

throw 문: 예외(값)를 던진다.

catch 블록: 예외(값)를 잡는다.

예외가 던져지면 try 블록이 종료된 다음 catch 블록의 코드가 실행된다.

catch 블록이 완료된 후에는 catch 블록 또는 블록들이 실행된 후 코드가 실행된다

(catch 블록이 프로그램을 종료하거나 다른 특수 작업을 수행하지 않은 경우).

(던져진 예외의 형식은 catch-블록 매개변수에 대해 나열된 형식과 일치해야 한다.

그렇지 않으면 예외가 해당 catch 블록에 의해 catch되지 않다.

이 점은 "다중 Throw와 Catch" 항목에서 더 자세히 설명한다.)
try 블록에 예외가 던져지지 않으면, try 블록이 완료된 후, 

catch 블록 또는 블록들 다음에 코드로 프로그램 실행이 계속된다.

(즉, 예외가 던져지지 않으면, catch블록 또는 블록은 무시된다.)

 

SYNTAX
try
{
    Some_Statements
    < throw문이 있는 어떤 코드나
            예외를 발생시킬 수 있는 함수 호출>
    Some_More_Statements
}
catch(Type e)
{
    < catch 블록 매개변수 형식 값이

        try 블록에 들어갈 경우 수행되는 코드>
}
EXAMPLE
디스플레이 18.2를 예시로 보자.

 

사용자의 예외 클래스 정의

throw문은 어떤 종류의 값이든 던질 수 있다.

객체가 원하는 정확한 종류의 정보를

catch 블록에 전달할 수 있는 클래스를 정의하는 것은 일반적이다.
전문 예외 클래스를 정의하는 훨씬 더 중요한 이유는

가능한 각 종류의 예외 상황을 식별할 수 있는 다른 형식을 가질 수 있기 때문이다.


예외 클래스는 단지 클래스일 뿐이다.

그것을 예외 클래스로 만드는 것은 그것이 사용되는 방법이다.
그래도 예외 클래스의 이름과 기타 세부 사항을 선택하는 데 약간의 주의를 기울이는 것이 좋다.
디스플레이 18.3은 프로그래머 정의 예외 클래스를 가진 프로그램의 예제를 포함하고 있다.

이것은 단지 예외 처리에 관한 C++ 세부사항을 보여주기 위한 토이 프로그램이다.

이것은 그렇게 단순한 작업에 너무 많은 장치를 사용하지만,

그렇지 않으면 일부 C++ 세부사항의 정확하지 않은 예이다.
다음과 같은 내용으로 재현된 throw문에 주목한다:
throw NoMilk(donuts);
NoMilk(donuts) 부분은 NoMilk 클래스에 대한 생성자의 호출이다.

생성자는 하나의 int 인수(이 경우 donuts)를 가져가서 클래스 NoMilk의 객체를 생성한다.

그런 다음 해당 객체를 던진다.

 

다중 throw 및 catch

하나의 throw 블록은 잠재적으로 임의의 수의 예외 값을 던질 수 있으며,

이는 다른 종류의 예외 값일 수 있다.

try 블록의 실행 중 어느 하나에서는 많아야 하나의 예외 값이 던져진다

(throw문으로 try 블록의 실행이 종료되므로).

각 catch 블록은 한 종류의 예외 값만 잡을 수 있지만

try블록 뒤에 하나 이상의 catch 블록을 배치하여 다른 종류의 예외 값을 잡을 수 있다.

예를 들어, 18.4의 프로그램은 try블록 뒤에 두 개의 catch 블록을 가지고 있다.

 

디스플레이 18.3 자신의 예외 클래스 정의

#include <iostream>
using std::cin;
using std::cout;

class NoMilk
{
public:
	NoMilk( ) {}
	NoMilk( int howMany) : count(howMany) {}
	int getCount( ) const { return count; }
private:
	int count;
};

int main( )
{
	int donuts, milk;
	double dpg;
	try
	{
		cout << "Enter number of donuts:\n";
		cin >> donuts;
		cout << "Enter number of glasses of milk:\n";
		cin >> milk;
        
		if (milk <= 0)
			throw NoMilk(donuts);
            
		dpg = donuts / static_cast<double>(milk);
		cout << donuts << " donuts.\n"
		     << milk << " glasses of milk.\n"
		     << "You have " << dpg
		     << " donuts for each glass of milk.\n";
	}
	catch(NoMilk e)
	{
		cout << e.getCount( ) << " donuts, and No Milk!\n"
		     << "Go buy some milk.\n";
	}
	cout << "End of program.\n";
	return 0;
}

이것은 C++ 구문을 배우기 위한 장난감 예제일 뿐이다.

예외 처리의 전형적인 좋은 사용 예제로 받아들이지 말자.

샘플 대화는 디스플레이 18.2와 동일하다.

 

디스플레이 18.4 다중 예외 catch하기

#include <iostream>
#include <string>
using std::cin;
using std::cout;
using std::endl;
using std::string;

class NegativeNumber
{
public:
	NegativeNumber( ){}
	NegativeNumber(string theMessage): message(theMessage) {}
	string getMessage( ) const { return message; }
private:
	string message;
};

class DivideByZero
{};

int main( )
{
	int pencils, erasers;
	double ppe; //pencils per eraser
	try
	{
		cout << "How many pencils do you have?\n";
		cin >> pencils;
		if (pencils < 0)
			throw NegativeNumber("pencils");
		cout << "How many erasers do you have?\n";
		cin >> erasers;
		if ( erasers < 0)
			throw NegativeNumber( "erasers");
		if ( erasers != 0)
			ppe = pencils / static_cast<double>(erasers);
		else
			throw DivideByZero( );
		cout << "Each eraser must last through "
		     << ppe << " pencils.\n";
	}
	catch(NegativeNumber e)
	{
		cout << "Cannot have a negative number of "
		     << e.getMessage( ) << endl;
	}
	catch(DivideByZero)
	{
		cout << "Do not make any mistakes.\n";
	}
	cout << "End of program.\n";
	return 0;
}

 

샘플 대화 상자 1

How many pencils do you have?
5
How many erasers do you have?
2
Each eraser must last through 2.5 pencils
End of program.

 

샘플 대화 상자 2

How many pencils do you have?
-2
Cannot have a negative number of pencils
End of program.

샘플 대화 상자 3

How many pencils do you have?
5
How many erasers do you have?
0
Do not make any mistakes.
End of program.

 

DivideByZero의 캐치 블록에는 매개변수가 없다.

매개변수가 필요 없는 경우에는 매개변수가 없는 형식만 나열하면 된다.

이에 대해서는 프로그래밍 팁 섹션 "예외 클래스는 사소할 수 있다"에서 조금 더 자세히 설명한다

 

함정: 좀 더 구체적인 예외를 먼저 파악

여러 예외를 잡을 때는 catch 블록의 순서가 중요할 수 있다.

try 블록에 예외 값을 던지면 그 뒤에 오는 catch 블록을 순서대로 시도하고,

던지는 예외의 형식에 가장 먼저 맞는 것이 실행되는 것이다.
예를 들어, 다음은 모든 유형의 던져진 값을 캐치하는 특수한 유형의 catch블록이다:

catch (...)
{
    <Place whatever you want in here.>
}

 

세 개의 점은 생략된 어떤 것을 나타내는 것이 아니다.

여러분은 실제로 프로그램에 세 개의 점을 입력한다.

이렇게 하면 다른 모든 catch 블록 뒤에 배치할 수 있는 기본 catch 블록이 된다.

예를 들어, 다음과 같이 디스플레이 18.4의 캐치 블록에 추가할 수 있다:

catch(NegativeNumber e)
{
    cout << "Cannot have a negative number of "
             << e.getMessage( ) << endl;
}
catch(DivideByZero)
{
    cout << "Do not make any mistakes.\n";
}
catch (...)
{
    cout << "Unexplained exception.\n";
}

 

그러나 이 기본 catch 블록을 catch 블록 목록의 끝에 배치하는 것은 말이 된다.

예를 들어, 우리가 대신 사용했다고 가정해 보겠다

catch(NegativeNumber e)
{
    cout << "Cannot have a negative number of "
             << e.getMessage( ) << endl;
}
catch (...)
{
    cout << "Unexplained exception.\n";
}
catch(DivideByZero)
{
    cout << "Do not make any mistakes.\n";
}

 

이 두 번째 순서로 NegativeNumber 유형의 예외(던져진 값)가

NegativeNumber catch 블록에 그대로 잡힌다.

그러나 DivideByZero 형식의 값이 던져지면

catch(...)를 시작하는 블록에 잡히므로 DivideByZero catch블록에 도달할 수 없다.
다행히도, 이런 종류의 실수를 하면 대부분의 컴파일러가 알려줄 것이다.

 

팁: 예외 클래스는 사소한 것일 수 있음

아래에서는 예외 클래스 DivideByZero의 정의를 재현했다
디스플레이 18.4에서:
class DivideByZero
{};
이 예외 클래스에는 멤버 변수가 없고 기본 생성자 이외의 멤버 함수가 없다.

이름만 있으면 충분하다.
DivideByZero 클래스의 객체를 던지면

디스플레이 18.4에서와 같이 해당 catch 블록을 활성화할 수 있다.


사소한 예외 클래스를 사용할 때 

일반적으로 컨트롤이 catch 블록에 도달하면

예외(던져진 값)에 대해 수행할 수 있는 작업은 없다.

예외는 catch 블록으로 이동하는 데만 사용된다.

따라서 catch 블록 매개변수를 생략할 수 있다.

사실, 예외 형식이 사소한 것이든 아니든 필요하지 않을 때

catch 블록 매개변수를 생략할 수 있다.

 

함수에서 예외 던지기

때때로 예외 처리를 지연시키는 것이 합리적이다.

예를 들어, 0으로 나누려는 시도가 있을 경우

예외를 던지는 코드를 가진 함수가 있을 수 있지만,

그 함수에서 예외를 포착하고 싶지 않을 수도 있다.

아마도 그 함수를 사용하는 일부 프로그램은 예외가 던져지면 그냥 끝나야 할 것이고,

그 함수를 사용하는 다른 프로그램은 다른 일을 해야 할 것이다.

따라서 예외를 함수 내부에 포착했다면 어떻게 해야 할지 알 수 없을 것이다.

이런 경우에는

함수 정의에서 예외를 포착하지 않고

함수를 사용하여 함수 호출을 try 블록에 넣고

그 try 블록 뒤에 오는 catch 블록에 예외를 포착하기 위해

함수를 사용하는 프로그램(또는 다른 코드)을 갖는 것이 합리적이다.

 

디스플레이 18.5에 있는 프로그램을 보자.

try 블록이 있지만 try 블록에 보이는 throw문은 없다.

그 프로그램에서 throw를 하는 문장은

if (bottom == 0)
    Throw DivideByZero( );

이 문은 try 블록에는 보이지 않다.

그러나 프로그램 실행 측면에서는 try 블록에 있다.

왜냐하면 함수 safeDivide의 정의에 있고

try 블록에 safeDivide의 호출이 있기 때문이다.
safeDivide 선언에서 throw (DivideByZero)의 의미는 다음 하위 섹션에서 설명한다.

 

디스플레이 18.5 함수 내부에 예외 던지기

#include <iostream>
#include <cstdlib>
using std::cin;
using std::cout;
using std::endl;

class DivideByZero
{};

double safeDivide( int top, int bottom) throw (DivideByZero);

int main( )
{
	int numerator;
	int denominator;
	double quotient;
	cout << "Enter numerator:\n";
	cin >> numerator;
	cout << "Enter denominator:\n";
	cin >> denominator;
    
	try
	{
		quotient = safeDivide(numerator, denominator);
	}
	catch(DivideByZero)
	{
		cout << "Error: Division by zero!\n"
		     << "Program aborting.\n";
		exit(0);
	}
    
	cout << numerator << "/" << denominator
	     << " = " << quotient << endl;
         
	cout << "End of program.\n";    
	return 0;
}

double safeDivide( int top, int bottom) throw (DivideByZero)
{
	if (bottom == 0)
		throw DivideByZero( );
        
	return top / static_cast<double>(bottom);
}

 

샘플 대화 상자 1

Enter numerator:
5
Enter denominator:
10
5/10 = 0.5
End of Program.

 

샘플 대화 상자 2

Enter numerator:
5
Enter denominator:
0
Error: Division by zero!
Program aborting.

 

예외 규격

예외 규격(exception specification)

어떤 함수가 예외를 포착하지 못하면 적어도 함수의 호출이

예외를 발생시킬 수 있음을 프로그래머들에게 알려야 한다.

만약 함수 정의에 포함되지 않고 버려질 수 있는 예외가 있다면,

그러한 예외 유형은 예외 사양에 나열되어야 하며,

이는 디스플레이 18.5의 다음 함수 선언으로 표시된다:
double safeDivide( int top, int bottom) throw (DivideByZero);

 

throw 리스트

예외 규격은 18.5 그림과 같이 함수 선언과 함수 정의에 모두 나타나야 한다.

함수에 둘 이상의 함수 선언이 있을 경우

모든 함수 선언은 동일한 예외 규격을 가져야 한다.

throw 리스트(throw list): 함수에 대한 예외 규격


함수 정의에 둘 이상의 예외를 적용할 수 있는 경우

다음과 같이 예외 유형이 쉼표로 구분되어 나열된다:
void someFunction( ) throw (DivideByZero, SomeOtherException);

 

예외 규격에 나열된 모든 예외 유형은 정상적으로 처리된다.

예외가 정상적으로 처리된다고 말할 때는

이 하위 섹션 앞에서 설명한 대로 처리된다는 뜻이다.

특히 함수 호출을 try 블록 다음에 catch 블록을 배치하여

해당 유형의 예외를 잡을 수 있고,

함수가 예외를 던지고

(그리고 함수 내부에서 해당 예외를 잡지 않으면)

try 블록 다음에 오는 catch 블록이 예외를 잡을 것이다.


예외 규격(삭제 목록 없음)이 전혀 없는 경우(빈 것도 아님) 

코드는 가능한 모든 예외 유형이

예외 규격에 나열된 것과 동일하게 동작한다.

즉, 삭제된 모든 예외는 정상적으로 처리된다.


예외가 함수에 포함되어 있지 않고

예외 규격에 포함되지 않은 경우에는 어떻게 될까?

이것은 컴파일 시간 오류도 아니고 런타임 오류도 아니다.

그런 경우에는 함수 unexpected( )를 호출한다.

 

함수의 동작을 unexpected로 변경할 수 있지만 

기본 동작은 함수를 terminate( )로 부르는 것으로 프로그램이 종료된다.

특히, 예외가 함수에 포함되어 있지만 예외 규격에 나열되지 않은 경우

(그리고 함수 내부에 포함되지 않은 경우),

해당 예외는 프로그램의 catch 블록에 걸리지 않고

대신 프로그램을 종료하는 기본 동작인 unexpected( )가 호출된다.


예외 규격은 함수를 "바꾸는" 예외에 대한 것임을 염두에 두어야 한다.

함수를 벗어나지 못하면 예외 규격에 속하지 않다.

함수를 벗어나는 경우에는 원래 위치에 상관없이 예외 규격에 속한다.

함수 정의 안에 있는 try 블록에 예외가 포함되어 있고

함수 정의 안에 있는 catch 블록에 포함되어 있으면

예외 규격에 그 유형을 나열할 필요가 없다.

함수 정의에 다른 함수의 호출이 포함되어 있고 

다른 함수가 탐지되지 않은 예외를 던질 수 있는 경우 

예외 유형을 예외 규격에 넣어야 한다.


만약 예외가 발견되지 않고

throw 리스트에 없는 것을 던질 가능성을 컴파일러가 확인하고

컴파일러 오류를 발생시켜야 한다고 생각할 수 있다.

그러나 C++에 있는 예외의 세부 사항 때문에 컴파일러가 확인을 수행할 수는 없다.

검사는 런타임에 수행해야 한다.
함수 내부에 포함되지 않은 예외를 던져서는 안 된다고 하려면

다음과 같은 빈 예외 규격을 사용한다:

void someFunction( ) throw ( );

 

요약하자면

void someFunction( ) throw (DivideByZero, SomeOtherException);
//Exceptions of type DivideByZero or SomeOtherException are
//treated normally. All other exceptions invoke unexpected( ).

void someFunction( ) throw ( );
//Empty exception list, so all exceptions invoke unexpected( ).
void someFunction( );
//All exceptions of all types are treated normally.

 

unexpected( )의 기본 동작은 프로그램을 종료하는 것이다.

특별한 include 또는 using 지시어를 사용하여 

unexpected( )의 기본 동작을 얻을 필요는 없습니다.


일반적으로 unexpected( )의 동작을 재정의할 필요가 없다.

그러나 unexpected( )의 동작은 set_unexpected 함수로 변경할 수 있다.

set_unexpected를 사용해야 한다면 자세한 내용은 더 고급 책을 참조해야 한다.


파생 클래스의 객체도 기본 클래스의 객체임을 염두에 두자.

따라서 D가 클래스 B의 파생 클래스이고 B가 예외 규격에 있으면

클래스 B의 객체이므로 클래스 D의 던져진 객체는 정상적으로 처리된다.

그러나 자동 형식 변환은 수행되지 않다.

만약 이중이 예외 규격에 있다면 이것은 int 값을 던지는 것을 설명하지 않다.

예외 규격에 int와 double을 모두 포함해야 한다.

 

경고
마지막 경고 하나: 모든 컴파일러가 예외 규격을 원래대로 취급하는 것은 아니다.

일부 컴파일러는 예외 규격을 기본적으로 주석으로 취급하지만,

해당 컴파일러에서는 예외 규격이 코드에 아무런 영향을 미치지 않는다.

이것이 함수에 의해 던져질 수 있는 모든 예외를 throw 규격에 배치하는 또 다른 이유이다.

이렇게 하면 모든 컴파일러가 예외를 동일한 방식으로 처리할 수 있다.

물론 throw 규격을 전혀 갖지 않음으로써

컴파일러 일관성을 동일하게 얻을 수 있지만,

프로그램이 문서화되지 않고

throw 규격을 사용하는 컴파일러가 제공하는 추가 오류 검사도 얻을 수 없다.

throw 규격을 처리하는 컴파일러를 사용하면

프로그램이 예상하지 못한 예외를 던지자마자 종료된다.
(이것은 런타임 동작이지만 어떤 런타임 동작이 발생하는지는 컴파일러에 따라 다르다.)

 

함정 : 파생 클래스의 예외 규격

파생 클래스에서 함수 정의를 재정의하거나 재정의할 때 

기본 클래스의 예외 규격과 동일한 예외 규격을 가져야 하거나 

예외가 기본 클래스 예외 규격의 하위 집합인 예외 규격을 가져야 한다.

즉, 함수 정의를 재정의하거나 재정의할 때 예외 규격에 예외를 추가할 수 없다

(단, 원하는 경우 일부 예외를 삭제할 수도 있다).

이는 파생 클래스의 객체는 아무 곳에서나 사용할 수 있으므로

기본 클래스의 객체를 사용할 수 있으므로

재정의되거나 덮어쓰기된 함수는

기본 클래스의 객체에 대해 쓰여진 모든 코드에 적합해야 한다.

 

18.2 예외 처리를 위한 프로그래밍 기법

지금까지 C++에서 예외 처리가 어떻게 작동하는지를 설명하는 코드를 많이 보여주었지만, 

예외 처리를 잘 현실적으로 활용하는 프로그램의 예는 보여주지 못했다.

하지만 이제 예외 처리의 메커니즘을 알게 되었으므로,

이 절에서는 예외 처리 기술에 대해 설명할 수 있다.

 

예외를 던질 때

우리는 예외 처리의 기본 개념을 설명하기 위해 몇 가지 매우 간단한 코드를 제공했다.

그러나 우리의 예제는 비현실적으로 간단했다.

더 복잡하지만 더 나은 지침은

예외를 던지는 것과 예외를 잡는 것을 각각의 함수로 분리하는 것이다.

대부분의 경우, 당신은 함수 정의 내에 임의의 throw문을 포함시키고,

그 함수의 예외 규격에 예외를 나열하고, 다른 함수에 catch절을 배치해야 한다.

따라서, 선호되는 함수의 사용
try-throw-catch 삼단 구성은 다음 그림과 같다:

void functionA( ) throw (MyException)
{
    .
    .
    .
    throw MyException( <Maybe an argument>);
    .
    .
    .
}

다음과 같은 다른 함수(아마도 다른 파일에 있는 다른 함수)에서는 다음과 같은 것들이 있다:

void functionB( )
{
    .
    .
    .
    try
    {
        .
        .
        .
        functionA( );
        .
        .
        .
    }
    catch(MyException e)
    {

        <Handle exception>

    }
    .
    .
    .
}

 

심지어 이런 종류의 throw문 사용은 피할 수 없는 경우에 대비해야 한다.

만약 당신이 다른 방법으로 쉽게 문제를 처리할 수 있다면, 예외를 던지지 마라.

예외 조건을 처리하는 방법은 함수를 어떻게 어디서 사용하는지에 달려 있다.

예외 조건을 처리하는 방법이 함수를 호출하는 방법과 장소에 달려 있다면,

가장 좋은 방법은 함수를 호출한 프로그래머가 예외를 처리하도록 하는 것이다.

다른 모든 상황에서는 예외를 던지는 것을 피하는 것이 좋다.

이런 종류의 상황에 대한 예시적인 시나리오를 설명해 보자.


병원의 환자 모니터링 시스템을 처리하기 위한 함수 라이브러리를 작성하고 있다고 가정해 보자. 

한 함수는 일부 파일에서 환자의 기록에 접근하여 온도의 합을 촬영한 횟수로 나눔으로써

환자의 일평균 온도를 계산할 수 있다.

이제 이러한 함수가 다른 상황에서 사용될 다른 시스템을 만드는 데 사용된다고 가정해 보자.

환자의 체온을 측정한 적이 없어서 

평균을 0으로 나누는 경우에는 어떻게 해야 할까?

중환자실에서는 환자를 잃는 것과 같이 매우 잘못된 것이 있음을 나타낼 수 있다.

(그런 일이 발생하는 것으로 알려져 있다.)

따라서 시스템의 경우 0으로 분할할 가능성이 발생하면

응급 메시지가 전송되어야 한다.

그러나 외래 진료나 심지어 중요하지 않은 병동과 같이

덜 긴급한 환경에서 시스템을 사용하기 위해서는

의미가 없을 수 있으므로

환자 기록에 있는 간단한 메모만으로도 충분하다.

이 시나리오에서 온도의 평균화를 위한 함수는

0으로 분할할 때 예외를 던지고,

예외 규격에 예외를 나열하여

각 시스템이 해당 시스템에 적합한 방식으로 예외 사례를 처리하도록 해야 한다.

 

예외를 적용할 때
대개의 경우 함수 내에서 throw 문을 사용하고 함수의 예외 규격에 나열해야 한다. 

또한 예외 조건을 다루는 방식이 함수를 사용하는 방법과 위치에 따라 달라지는 상황에 대비해야 한다. 

예외 조건을 다루는 방식이 함수를 호출하는 방법과 위치에 따라 다르다면 

가장 좋은 방법은 함수를 호출한 프로그래머가 예외를 처리하도록 하는 것이다.

다른 상황에서는 거의 항상 예외를 던지는 것을 피하는 것이 좋다.

 

함정: 포착되지 않은 예외

코드가 던지는 모든 예외는 코드의 어딘가에 있어야 한다.

예외가 던져졌지만 어디에도 걸리지 않으면 프로그램은 종료된다.

 

terminate()
엄밀히 말하면 예외가 던져졌지만 잡히지 않으면 함수 terminate()를 호출한다.

terminate()의 기본 의미는 프로그램을 종료하는 것이다.

 

함수에 포함되지만 함수 내부 또는 외부에 포함되지 않는 예외는 두 가지 가능한 경우가 있다.

예외가 예외 규격에 나열되지 않으면 함수 unexpected( )가 호출된다.

예외가 예외 규격에 나열되지 않으면 함수 terminate( )가 호출된다.

그러나 unexpected( )의 기본 동작을 변경하지 않는 한

unexpected( ) 호출은 terminate( )를 호출한다.

따라서 결과는 두 경우 모두 동일하다.

예외가 함수에 포함되지만

함수 내부 또는 외부에 포함되지 않으면 프로그램이 종료된다.

 

함정: 중첩 try-catch 블록

더 큰 try 블록 안에 try 블록과 후속 try 블록을 배치하거나 더 큰 try 블록 안에 배치할 수 있다.

드문 경우이지만, 이 방법이 유용할 수도 있지만,

이렇게 하고 싶다면 프로그램을 구성하는 더 좋은 방법이 있다고 의심해야 한다.

거의 항상 함수 정의 안에 내부 try 블록을 배치하고

외부 try 또는 catch 블록에 함수의 호출을 배치하는 것이 더 좋다

(또는 하나 이상의 try 블록을 완전히 제거할 수도 있다).

 

더 큰 try 블록 안에 try 블록을 넣고 catch 블록을 따르는 경우 

예외가 내부 try 블록에 포함되지만 내부 catch 블록에는 포함되지 않는 경우 

예외가 처리를 위해 외부 try 블록에 포함되고 외부 try 블록 다음에 catch 블록에 포함될 수 있다.

 

함정: 예외의 남용

예외적으로 제어 흐름이 너무 관여되어

프로그램을 이해하기가 거의 불가능한 프로그램을 작성할 수 있다.


예외를 던지는 것은 

프로그램의 어느 곳에서든 

프로그램의 거의 다른 곳으로 

전송할 수 있게 해준다. 

 

프로그래밍 초기에는

이러한 종류의 제한 없는 제어 흐름이

goto라고 알려진 작도를 통해 허용되었다. 

프로그래밍 전문가들은 

이제 이러한 제한 없는 제어 흐름이 매우 빈약한 프로그래밍 스타일이라는 데 동의한다. 

예외는 당신이 제한 없는 제어 흐름의 좋지 않은 옛날로 되돌아갈 수 있게 해준다. 

예외는 특별히 그리고 특정한 방식으로만 사용되어야 한다. 

좋은 규칙은 다음과 같다. 

만일 당신이 throw문을 포함하고 싶다면, 

이 throw문 없이 당신의 프로그램 또는 클래스 정의를 어떻게 쓸지 생각해보라. 

합리적인 코드를 생성하는 대안을 생각해 낼 수 있다면,

아마도 throw문을 포함하고 싶지 않을 것이다.

 

예외 클래스 계층 구조

예외 클래스의 계층 구조를 정의하는 데 매우 유용할 수 있다.

예시)

ArmeticError 예외 클래스를 가진 다음

ArmeticError의 파생 클래스인 예외 클래스 DivideByZeroError를 정의할 수 있다.

DivideByZeroError는 ArmeticError이므로

ArmeticError의 모든 catch 블록은 DivideByZeroError를 catch한다.

예외 규격에 ArmeticError를 나열하면

실제로 DivideByZeroError를 예외 규격에 추가한 것이다.

 

사용 가능한 메모리 테스트

17장에서는 다음과 같은 코드로 새로운 동적 변수를 만들었다:

struct Node
{
    int data;
    Node *link;
};
typedef Node* NodePtr;
...
NodePtr pointer = new Node;

새 노드를 생성하는 데 사용할 수 있는 메모리가 충분하면 작동한다.
하지만 그렇지 않으면 어떻게 될까?

노드를 생성하기에 메모리가 부족하면 bad_alloc 예외가 발생한다.

 

new는 노드를 생성할 메모리가 부족할 때 

bad_alloc 예외를 발생시키므로 

다음과 같이 메모리가 부족한지 확인할 수 있다:

try
{
    NodePtr pointer = new Node;
}
catch (bad_alloc)
{
    cout << "Ran out of memory!";
}

물론 단순히 경고 메시지를 보내는 것 외에도 다른 작업을 수행할 수 있지만 

수행하는 세부 작업은 특정 프로그래밍 작업에 따라 달라진다.


bad_alloc의 정의는 헤더 파일 <new>와 함께 라이브러리에 있으며 std 네임스페이스에 있다.

따라서 bad_alloc을 사용할 때 프로그램에는

다음과 같은 내용(또는 유사한 내용)이 포함되어야 한다:

#include <new>
using std::bad_alloc;

 

예외 다시 던지기

예외를 catch 블록 내에 던지는 것은 합법적이다.

드문 경우이지만

예외를 잡고 세부 정보에 따라 예외 처리 블록의 연쇄에서 더 멀리 처리하기 위해

동일하거나 다른 예외를 적용하기로 결정할 수도 있다.

'프로그래밍 공부 > OOP' 카테고리의 다른 글

19장 템플릿 표준 라이브러리  (2) 2023.12.04
17장 연계된 자료 구조(3)  (0) 2023.12.01
17장 연계된 자료 구조(2)  (2) 2023.12.01
17 연계된 자료 구조(1)  (1) 2023.12.01
16장 탬플릿  (1) 2023.11.30