[C++] Chapter 15 - 예외처리(Exception Handling)

Lee Jeong Min·2021년 1월 23일
0

Cpp

목록 보기
15/16
post-thumbnail

15-1 예외상황과 예외처리의 이해

예외상황을 처리하지 않았을 때의 결과

UnhandlingException.cpp

#include <iostream>
using namespace std;

int main(void)
{
	int num1, num2;
	cout << "두 개의 숫자 입력: ";
	cin >> num1 >> num2;

	cout << "나눗셈의 몫: " << num1 / num2 << endl;
	cout << "나눗셈의 나머지: " << num1 % num2 << endl;
	return 0;
}

예외처리를 하지 않았을 시 num2에 0이 들어오게 되면 프로그램이 강제종료가 됨. 보통 이러한 예외처리를 하기 위해 if문과 같은 조건문을 사용하지만 이러한 경우 예외의 발생위치, 발견위치, 처리위치가 달라지므로 예외처리를 위한 코드와 프로그램의 흐름을 구성하는 코드를 쉽게 구분하기 위해 C++의 예외처리 메커니즘인 try, catch 그리고 throw에 대해서 배움.


15-2 C++의 예외처리 메커니즘

C++의 예외처리 메커니즘의 이해: try와 catch 그리고 throw의 이해

  • try: 예외를 발견한다
  • catch: 예외를 잡는다
  • throw: 예외를 던진다
Try블록과 catch블록은 항상 하나의 문장처럼 같이 쓰임
try
{
    // 예외 발생 지역
    if (....)
       throw expn;
}
catch(처리할 예외의 종류 명시)
{
    // 예외처리 코드의 삽입
}

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

HandlingExceptionTryCatch.cpp

#include <iostream>
using namespace std;

int main(void)
{
	int num1, num2;
	cout << "두 개의 숫자 입력: ";
	cin >> num1 >> num2;

	try
	{
		if (num2 == 0)
			throw num2;

		cout << "나눗셈의 몫: " << num1 / num2 << endl;
		cout << "나눗셈의 나머지: " << num1 % num2 << endl;
	}
	catch(int expn)
	{
		cout << "제수는 0이 될 수 없습니다." << endl;
		cout << "프로그램을 다시 실행하세요." << endl;
	}
	cout << "end of main" << endl;
	return 0;
}

throw부분을 보면 예외가 발생한 지점 이후를 실행하는 것이 아닌, catch 블록의 이후가 실햄됨을 확인할 수 있다. 또한 catch블록의 매개변수 자료형은 throw절에 의해 던져진 예외 데이터의 자료형과 일치해야한다. --> 일치하지 않으면 던져진 예외 데이터는 catch블록으로 전달 X --> 함수를 호출한 곳으로 예외 데이터가 전달됨


15-3 Stack Unwinding(스택 풀기)

예외의 전달

PassException.cpp

#include <iostream>
using namespace std;

void Divide(int num1, int num2)
{
	if (num2 == 0)
		throw num2;
	cout << "나눗셈의 몫: " << num1 / num2 << endl;
	cout << "나눗셈의 나머지: " << num1 % num2 << endl;
}

int main(void)
{
	int num1, num2;
	cout << "두 개의 숫자 입력: ";
	cin >> num1 >> num2;

	try
	{
		Divide(num1, num2);

		cout << "나눗셈을 마쳤습니다." << endl;

	}
	catch (int expn)
	{
		cout << "제수는 0이 될 수 없습니다." << endl;
		cout << "프로그램을 다시 실행하세요." << endl;
	}

	return 0;
}

위 예제를 통해 예외처리에 대한 책임은 함수를 호출한 영역으로 넘어가게 된다는 것을 알 수 있다. 따라서 메인함수 내에서 예외에 대한 처리르 진행하게 된다. 대부분의 경우에 있어서 예외의 발생위치와 예외의 처리위치는 다르다.

스택 풀기(Stack Unwinding)

StackUnwinding.cpp

#include <iostream>
using namespace std;

void SimpleFuncOne(void);
void SimpleFuncTwo(void);
void SimpleFuncThree(void);

int main(void)
{
	try
	{
		SimpleFuncOne();
	}
	catch (int expn)
	{
		cout << "예외코드: " << expn << endl;
	}
	return 0;
}

void SimpleFuncOne(void)
{
	cout << "SimpleFuncOne(void)" << endl;
	SimpleFuncTwo();
}

void SimpleFuncTwo(void)
{
	cout << "SimpleFuncTwo(void)" << endl;
	SimpleFuncThree();
}

void SimpleFuncThree(void)
{
	cout << "SimpleFuncThree(void)" << endl;
	throw -1;
}

예외가 처리될 때까지 호출된 함수의 역순으로 데이터가 전달되며 이러한 예외 데이터의 전달을 가리켜 스택 풀기라고 한다.

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

CatchList.cpp

#include <iostream>
#include <cstring>
#include <cmath>
using namespace std;

int StoI(char* str)
{
	int len = strlen(str);
	int num = 0;

	if (len != 0 && str[0] == '0')
		throw 0;

	for (int i = 0; i < len; i++)
	{
		if (str[i] < '0' || str[i]>'9')
			throw str[i];
		num += (int)(pow((double)10, (len - 1) - i) * (str[i] + (7 - '7')));
	}
	return num;
}

int main(void)
{
	char str1[100];
	char str2[200];

	while (1)
	{
		cout << "두 개의 숫자 입력: ";
		cin >> str1 >> str2;

		try
		{
			cout << str1 << " + " << str2 << " = " << StoI(str1) + StoI(str2) << endl;
			break;
		}
		catch (char ch)
		{
			cout << "문자 " << ch << "가 입력되었습니다." << endl;
			cout << "재입력 진행합니다." << endl << endl;
		}
		catch (int expn)
		{
			if (expn == 0)
			{
				cout << "0으로 시작하는 숫자는 입력불가." << endl;
			}
			else
				cout << "비정상적 입력이 이루어졌습니다." << endl;

			cout << "재 입력 진행합니다." << endl << endl;
		}
	}
	cout << "프로그램을 종료합니다." << endl;
	return 0;
}

전달되는 인자의 자료형에 따라 자료형에 맞는 catch 블록이 수행이됨.

전달되는 예외의 명시

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

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

위와 같이 함수를 정의할 때에는 함수 내에서 발생 가능한 예외의 종류를 명시해주는 것이 좋다. 만약 명시되지 않은 다른 예외데이터가 전달되는 경우 terminate함수의 호출로 프로그램은 종료된다.


15-4 예외상황을 표현하는 예외 클래스의 설계

예외 클래스와 예외 객체

ATMSim.cpp

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

class DepositException
{
private:
	int reqDep; // 요청 입금액
public:
	DepositException(int money) : reqDep(money)
	{}
	void ShowExceptionReason()
	{
		cout << "[예외 메시지: " << reqDep << "는 입금불가]" << endl;
	}
};

class WithdrawException
{
private:
	int balance; // 잔고
public:
	WithdrawException(int money) : balance(money)
	{}
	void ShowExceptionReason()
	{
		cout << "[예외 메시지: 잔액 " << balance << ", 잔액부족]" << endl;
	}
};

class Account
{
private:
	char accNum[50]; // 계좌번호
	int balance;
public:
	Account(const char* acc, int money) : balance(money)
	{
		strcpy(accNum, acc);
	}
	void Deposit(int money) throw(DepositException)
	{
		if (money < 0)
		{
			DepositException expn(money);
			throw expn;
		}
		balance += money;
	}
	void Withdraw(int money) throw(WithdrawException)
	{
		if (money > balance)
			throw WithdrawException(balance);
		balance -= money;
	}
	void ShowMyMoney()
	{
		cout << "잔고: " << balance << endl << endl;
	}
};

int main(void)
{
	Account myAcc("5678-827120", 5000);

	try
	{
		myAcc.Deposit(2000);
		myAcc.Deposit(-300);
	}
	catch (DepositException& expn)
	{
		expn.ShowExceptionReason();
	}
	myAcc.ShowMyMoney();

	try
	{
		myAcc.Withdraw(3500);
		myAcc.Withdraw(4500);
	}
	catch (WithdrawException& expn)
	{
		expn.ShowExceptionReason();
	}
	myAcc.ShowMyMoney();
	return 0;
}

예외 클래스를 위의 예제에서 확인할 수 있다. 예외 객체를 전달하는 일반적인 방법은 Withdraw throw에서 볼수 있는 것처럼 임시객체의 형태로 생성하는 것이 일반적이라고 한다.

상속관계에 있는 예외클래스를 정의할 시, virtual을 통한 순수 가상함수를 정의해준다면 예외의 처리를 좀 더 단순화할 수 있다.
ATMSim2.cpp

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

class AccountException
{
public:
	virtual void ShowExceptionReason() = 0; // 순수 가상함수
};

class DepositException : public AccountException
{
private:
	int reqDep; // 요청 입금액
public:
	DepositException(int money) : reqDep(money)
	{}
	void ShowExceptionReason()
	{
		cout << "[예외 메시지: " << reqDep << "는 입금불가]" << endl;
	}
};

class WithdrawException : public AccountException
{
private:
	int balance; // 잔고
public:
	WithdrawException(int money) : balance(money)
	{}
	void ShowExceptionReason()
	{
		cout << "[예외 메시지: 잔액 " << balance << ", 잔액부족]" << endl;
	}
};

class Account
{
private:
	char accNum[50]; // 계좌번호
	int balance;
public:
	Account(const char* acc, int money) : balance(money)
	{
		strcpy(accNum, acc);
	}
	void Deposit(int money) throw(AccountException)
	{
		if (money < 0)
		{
			DepositException expn(money);
			throw expn;
		}
		balance += money;
	}
	void Withdraw(int money) throw(AccountException)
	{
		if (money > balance)
			throw WithdrawException(balance);
		balance -= money;
	}
	void ShowMyMoney()
	{
		cout << "잔고: " << balance << endl << endl;
	}
};

int main(void)
{
	Account myAcc("5678-827120", 5000);

	try
	{
		myAcc.Deposit(2000);
		myAcc.Deposit(-300);
	}
	catch (AccountException& expn)
	{
		expn.ShowExceptionReason();
	}
	myAcc.ShowMyMoney();

	try
	{
		myAcc.Withdraw(3500);
		myAcc.Withdraw(4500);
	}
	catch (AccountException& expn)
	{
		expn.ShowExceptionReason();
	}
	myAcc.ShowMyMoney();
	return 0;
}

예외의 전달방식에 따른 주의사항

CatchFlow.cpp

#include <iostream>
using namespace std;

class AAA
{
public:
	void ShowYou() { cout << "AAA exception!" << endl; }
};

class BBB : public AAA
{
public:
	void ShowYou() { cout << "BBB exception!" << endl; }
};

class CCC : public BBB
{
public:
	void ShowYou() { cout << "CCC exception!" << endl; }
};

void ExceptionGenerator(int expn)
{
	if (expn == 1)
		throw AAA();
	else if (expn == 2)
		throw BBB();
	else
		throw CCC();
}

int main(void)
{
	try
	{
		ExceptionGenerator(3);
		ExceptionGenerator(2);
		ExceptionGenerator(1);
	}
	catch (AAA& expn)
	{
		cout << "catch(AAA& expn)" << endl;
		expn.ShowYou();
	}
	catch (BBB& expn)
	{
		cout << "catch(BBB& expn)" << endl;
		expn.ShowYou();
	}
	catch (CCC& expn)
	{
		cout << "catch(CCC& expn)" << endl;
		expn.ShowYou();
	}
	return 0;
}

서로 상속된 관계에서 다음과 같이 프로그램을 짜게되면 모든 예외객체가 AAA클래스를 상속하기 때문에 처음 catch 블록이 실행된다. 따라서 이를 방지하기 위해 catch문 블록의 배치를 CCC, BBB, AAA순으로 다시 재배치해야한다.


15-5 예외처리와 관련된 또 다른 특성들

new 연산자에 의해서 발생하는 예외

BadAlloc.cpp

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

int main(void)
{
	int num = 0;

	try
	{
		while (1)
		{
			num++;
			cout << num << "번째 할당 시도" << endl;
			new int[10000][10000];
		}
	}
	catch (bad_alloc& bad)
	{
		cout << bad.what() << endl;
		cout << "더이상 할당 불가!" << endl;
	}
	return 0;
}

프로그래머가 정의하지 않아도 발생하는 예외가 있음. 위의 예외는 bad_cast예외이다.

만약 catch부분에 catch(...)와 같이 상요하면 전달되는 모든 예외를 다 받아주겠다는 선언이다. 하지만 이러한 경우 그 어떠한 정보도 전달받을 수 없으며, 전달된 예외의 종류도 구분이 불가능해진다.

예외 던지기

ReThrow.cpp

#include <iostream>
using namespace std;

void Divide(int num1, int num2)
{
	try
	{
		if (num2 == 0)
			throw 0;
		cout << "몫: " << num1 / num2 << endl;
		cout << "나머지: " << num1 % num2 << endl;
	}
	catch (int expn)
	{
		cout << "first catch" << endl;
		throw;
	}
}

int main(void)
{
	try
	{
		Divide(9, 2);
		Divide(4, 0);
	}
	catch (int expn)
	{
		cout << "second catch" << endl;
	}
	return 0;
}

함수안에서 catch블록으로 전달된 예외가 소멸되지 않고 다시 던져져 함수를 호출한 main함수까지 와서 catch 블록에 있는 코드들을 진행시킨다. 보통 필요한 상황이 아니면 예외를 다시 던지기 위해서 노력할 필요는 없다.

profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글