Programming/C ,C++,C#

[C++] 예외처리 try catch, 사용자 예외 클래스

Makuri 2019. 12. 22. 03:04
728x90

-예외처리

프로그래밍을 처음에 배울 때 들었던 말이 있다.

'프로그래머는 모든 경우를 생각해서 처리해야 한다'

우리가 만든 프로그램이 항상 원하는 루틴으로 흘러가 주지도 않고,

사용자가 생각지도 못한 방식으로 사용할 수 도 있다.

이러한 경우들 까지도 생각해서 처리 할 수 있어야 훌륭한 프로그래머라고 들은 거 같다.

 

근데 프로그래머도 사람인지라 세상 만사을 다 아는 것도 아니다.

그리고 생각보다 인간의 행동발상이란 매우 다채롭다(...)

 

아무래도 프로그래밍 언어 개발자들도 그런 생각을 했나보다.

그래서 예외 처리 클래스를 만들어 놓은 거 같다.

 

생각해보면 프로그램이 안터지도록 방지할 수 있는 매우 중요한 내용인데,

왜 학교랑 학원에서 다들 예외 처리는 넘겨버리는 걸까.

 

예를 들어 다음과 같은 경우를 보자.

 

#include <iostream>
using namespace std;

int main()
{
	int input = 0;
	cin >> input;
	cout << 10 / input << endl;
	return 0;
}

입력을 받아서 10을 나누는 피연산자로 사용하는 모습이다. 

2를 넣으면 5가 나올거고, 3을 넣으면 int형이니 그냥 3이 나온다.

 

어쨌든 '나눌 수'있는 수를 넣으면 괜찮다. 하지만 0은 어떨까?

VS2019는 디버그 모드로 하면 그냥 중단점을 찍어주네...안 터지고

예외가 발생한다. 당연히 10/0은 성립되지 않으니깐.

예전에 고등학교때 수학 시간에도 꼭 a/b (단, b은 0이 아니다) 라고 했던거 같다.(왜 !=의 그 수학 기호가 안써지냐)

 

이걸 본 우리는 그냥 이렇게 생각한다. 0이면 안 나누면 되겠지?

그냥 생각해보면 나누기 전에 이렇게 처리해주면 된다.

cin >> input;
if (input == 0)
{
	cout << "0은 안됨" << endl;
	return -1;
}
cout << 10 / input << endl;

일단 0은 막았다.

참고로 소수가 들어가면 어쩌냐 할수도 있는데

어차피 int형이라 정수부만 남기 때문에 0.2든 0.6이든 그냥 input은 0이다.

참고로2 하자면 문자가 들어가면 cin에서 애초에 타입이 달라서 안 받아들여지기 때문에 

input의 초기값인 0이 그대로 있어서 '0은 안됨'이 출력된다.

 

이렇게 프로그래머가 생각지 못한 에러를 사전에 방지하고 그에 따른 처리를 해주는 것을 예외처리 라고 한다.

 

위의 코드처럼 단순하게 에러를 막을 수 있으면 다행이다.

하지만 세상은 그렇게 만만하지가 않다.

 

- try~catch

사실 처음에는 몰랐다. 

new 키워드가 실패할 수도 있다는 사실을.

 

근데 생각해보면 당연하다. 어쨌든 메모리도 물리적으로 한정된 공간이니 할당 할 수 없을 수도 있단거다.

아, 그래도 그건 들어봤다. 할당을 실패할 수도 있으니 꼭 확인하라고.

#include <iostream>
using namespace std;

int main()
{
	int* pNum = new int[100000000];
	if (pNum == nullptr)
	{
		cout << "할당 안됨" << endl;
		return -1;
	}

	cout << "할당 됨" << endl;
	return 0;
}

0을 채울수 있을 만큼 넣어봤다. 음...되긴 된다 할당이.

(이 이상은 '배열의 전체 크기는 0x7fffffff바이트를 초과할 수 없습니다.'라고 컴파일 에러가 뜬다.)

 

그치만 이러한 할당이 너무 많이 일어나면 할당이 안되는데, 이때 new는 NULL을 반환한다. 

클래스의 경우 생성자를 실패한 경우에도 할당한 메모리를 해제하고 NULL을 반환한다고 한다.

 

그래서 이렇게 처리를 하는 경우도 있다고 한다.

 

하지만 C++에는 이러한 예외를 처리하는 방법을 제공해 주는데 그게 바로 try catch 문이다.

위 코드에 try catch를 쓰면 다음과 같다.

#include <iostream>
using namespace std;

int main()
{
	int* pNum = nullptr;
	try
	{
		for (int i = 0; i < 100; ++i)
		{
			pNum = new int[10000000];
			cout << "할당 됨" << endl;
		}
	}
	catch (const std::exception& e)
	{
		cout << "할당 안됨" << endl;
		return -1;
	}

	return 0;
}

할당이 안되게 해보려고 for문 100번을 돌려봤다. int 100만개를 100번 할당하니....4억 바이트인가? 4기가? 정도 할당해 봤는데 중간에 '할당 안됨'을 호출하고 종료한다.

참고로 내 컴퓨터의 메모리는 16기가다....물론 빌드를 x86으로 돌렸으니 그거랑은 별개 얘기다. 그 외에도 뭐 여러모로...

new키워드에서 할당을 실패하면 catch문으로 넘어가는 것을 확인 할 수 있었다.

사실 new 키워드 안에는 예외를 호출하는게 있다고 한다.

 

(아래는 그렇다는 내용의 스택오버플로우...)

https://stackoverflow.com/questions/2497151/can-the-c-new-operator-ever-throw-an-exception-in-real-life

 

Can the C++ `new` operator ever throw an exception in real life?

Can the new operator throw an exception in real life? And if so, do I have any options for handling such an exception apart from killing my application? Update: Do any real-world, new-heavy

stackoverflow.com

할당을 실패하면 bad_alloc이라는 예외를 호출하는데, 이 bad_alloc 또한 C++의 std 네임스페이스에 정의된 클래스이다.

	catch (const std::exception& e)
	{
		cout << "할당 안됨" << endl;
		return -1;
	}

catch문을 보면 exception 이라는 자료형을 인자로 받고 있는 것을 볼 수 있는데, 이는 예외처리 클래스로, bad_alloc의 부모 클래스이다.

인테리젼스도 클래스라고 표시해준다.보니까 그 외에도 이것저것 있나보다. 

F12누르고 따라가보면 다음과 같다.

class bad_alloc
    : public exception
{
public:

    bad_alloc() noexcept
        : exception("bad allocation", 1)
    {
    }

private:

    friend class bad_array_new_length;

    bad_alloc(char const* const _Message) noexcept
        : exception(_Message, 1)
    {
    }
};

noexcept에 대해선 이야기 할 거리가 많아 보이니 나중에 이야기하고,

 

일단 보면 exception을 상속받는 클래스인걸 확인 할 수 있다.

new키워드가 예외를 발생하면 catch문으로 넘어가면서 bad_alloc을 인자로 전달해준다.

 

수많은 exception의 자식중에 bad_alloc인지 확인하고 싶으면 exception의 멤버함수인 what()을 불러주면 된다.

catch (const std::exception& e)
	{
		cout << "할당 안됨 " <<e.what() << endl;
		return -1;
	}

bad_alloc클래스의 생성자에 적힌 bad allocation이 출력된다.

이 외에도 bad_cast, out_of_range, overflow_error 등등 다양한 예외 처리가 있다.

정확히 예측 하기 힘든 예외들에 대해 확인하고 처리 할 수 있기 때문에 예외 처리 구문을 사용하기도 한다.

cppreference : https://en.cppreference.com/w/cpp/error/exception

 

std::exception - cppreference.com

Provides consistent interface to handle errors through the throw expression. All exceptions generated by the standard library inherit from std::exception [edit] Member functions constructs the exception object (public member function) destroys the exceptio

en.cppreference.com

 

- 사용자 예외 클래스

bad_alloc이 그렇듯이 exception클래스를 상속한 클래스를 정의하면 임의로 원하는 예외를 던질 수 있다.

맨 처음의 0 나누기에서 원하는 예외를 던져보기로 하자.

 

#include <iostream>
using namespace std;

class MyException : public exception
{
public:
	MyException()
		:exception("예외!",1)
	{}
};

int main()
{
	int input = 0;
	cin >> input;
	try
	{
		if (input == 0)
			throw MyException();

		cout << 10 / input << endl;
	}
	catch (const std::exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

MyException을 만들어 봤다. 생성자서 '예외!'라고 하면서 1을 함께 넣어봤는데,

실제로 exception클래스를 따라가봤을 때는 아무런 의미가 없었다. 그 얘기 할라고 넣었다. 1은 안써도 된다.

(여기에는 exception클래스가 변하면서 구 버전들과의 호환을 위해 int를 추가한 생성자를 남겨 둔거 같다고 추측한다. noexption키워드가 생기면서 throw() 가 없어졌다고 하는데 이거랑 관련 있는 거 같다.)

 

이제 실행해서 0을 넣으면,

예외! 라고 뜨는 걸 볼 수 있다.

 

-마무리

1. 프로그램이 사용자 입력이나 할당 실패와 같은 예기치 않은 예외가 발생할 경우를 대비해서 예외 처리를 해주어야 한다.

2. C++에는 예외 처리를 위해 try~catch문과 exception클래스와 파생 클래스들이 존재한다.

3.exception 클래스를 상속받으면 사용자 예외 클래스를 만들 수 있다.

 

사실 예외는 발생하지 않는게 가장 좋으며, 프로그래머가 발생할 모든 가능성에 대해서 처리해주는 것이 좋다.

오히려 이때문에 try~catch는 잘쓰지 않는다고도 한다. 차라리 터져서 알 수 있는게 낫다는 결론인가 보다.

 

 

* 이 글은 '열혈강의 C++언어 본색'의 14장을 보고 작성하였습니다.

728x90