C++ 예외처리(메모용, 윤성우 열혈 C++ 프로그래밍 정리 CH 15)

RisingJade의 개발기록·2022년 3월 1일
0

윤성우 열혈C++ 정리

목록 보기
12/13

Chapter 15. 예외 처리


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

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

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

  • 나이를 입력하라고 했는데 음수값이 입력됨
  • 나눗셈을 위한 두 개의 정수를 입력받는데, 제수(나누는 수)로 0이 입력되었다.
  • 13자리 입력에 14자리를 입력하였다.
    이렇듯 예외는 문법적인 오류가 아닌 프로그램의 논리에 맞지 않는 상황을 의미한다.

if문을 이용한 예외의 처리

if문을 이용하면 예외가 발견되는 위치는 예외가 발생하는 위치와 다를 수 있다.
따라서, 이러한 예외 처리는 예외처리를 위한 코드와 프로그램의 흐름을 구성하는 코드를 쉽게 구분하지 못한다.

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

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

  • try 블록
    try블록은 예외발생에 대한 검사의 범위를 지정할 때 사용된다. 즉, try 블록 내에서 예외가 발생하면, 이는 C++의 예외처리 메커니즘에 의해서 처리가 된다.
try{
	...// 예외 발생 지역!
}
  • catch 블록
    catch 블록은 try블록에서 발생한 예외를 처리하는 코드가 담기는 영역으로써, 그 형태가 마치 반환형없는 함수와 유사하다.
catch{
	...// 예외처리 코드의 삽입
}
  • try블록과 catch블록
    catch블록은 try블록 뒤에 이어서 등장하며, try 블록에서 발생한 예외는 이곳 catch블록에서 처리된다.
try{
	...// 예외 발생 지역!
}
catch(/*처리할 예외의 종류 명시*/){
	...// 예외처리 코드의 삽입
}
  • throw
    키워드 throw는 예외가 발생했음을 알리는 문장의 구성에 사용된다.
    throw expn; 여기서 expn은 변수, 상수 그리고 객체 등 표현 가능한 모든 데이터가 될 수 있으나, 예외상황에 대한 정보를 담은, 의미 있는 데이터이어야 한다.
    • expn같은 데이터를 가리켜 그냥 '예외'라고 표현하는 것이 일반적이나 혼란을 줄 수 있어 여기서는 '예외 데이터' 또는 '예외 객체'라고 구분지어 표현하겠다.

예외처리 메커님즘의 적용

try, catch 그리고 throw의 매커니즘을 정리하고 가자면

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

try{
	if (/*예외가 발생하는 조건*/)
    	throw expn;
}
catch (type expn){ //throw로 던진 expn을 받는다.
	...// 예외처리 실행
}
  • 또한, 위에서 예외가 발생되어 throw가 실행되서 catch블록로 넘어가면, 예외가 발생한 지점 이후를 실행하는 것이 아니라, catch블록의 이후가 실행된다. 예외가 발생되면 발생된 지점 이후의 나머지 try영역은 그냥 건너뛴다.
  • throw절에 의해 던져진 예외 데이터의 자료형과 catch 블록의 매개변수 자료형은 일치해야된다. 그렇지 않으면 예외 데이터가 catch블록으로 전달되지 않는다.

try 블록을 묶는 기준

try블록은 단순히 예외가 발생할만한 영역만을 묶는 것이 아니다!그와 관련된 모든 문장을 함께 묶어서 이를 하나의 '일(work)' 단위로 구성하는 것이다.

try{
	if (num2==0)
    	throw expn;
}
catch (int expn){ //throw로 던진 expn을 받는다.
	...// 예외처리 실행
}
cout << "나눗셈의 몫: " << num1/num2 << endl;// divide by 0 ERROR 발생!
cout << "나눗셈의 나머지: " << num1%num2 << endl;

위와 같은 코드를 보면 왜 예외발생된 곳만 try를 묶는게 아니라 작업단위로 try를 묶는지 알 수 있다.

15-3.Stack Unwinding(스택 풀기)

throw절에 의해서 예외가 발생은 했는데, 이를 처리하지 않으면 어떻게 될지 살펴보자

예외의 전달

어떤 함수애서 throw절이 실행되어 예외가 발생했지만, 이 함수 내에서 예외처리를 위한 try~catch문이 존재하지 않으면, 예외처리에 대한 책임은 이 함수를 호출한 영역으로 넘어가게 된다.

void Divide(int num1, int num2){
	if(numw = 0){
    	throw num2;
    }
    cout << "나눗셈의 몫: " << num1/num2 << endl;// divide by 0 ERROR 발생!
	cout << "나눗셈의 나머지: " << num1%num2 << endl;
}
int main(void){
	int num1, num2;
    cout << "두개의 숫자 입력: ";
    cin >> num1 >> num2;
    try{
		Divide(num1, num2);
        cout << "나눗셈을 마쳤습니다. " << endl;
	}
	catch (int expn){ //throw로 던진 expn을 받는다.
		cout << "제수는 " << expn << "이 될 수 없습니다" <<  endl;// 예외처리 실행
	}
    return 0;
}

위와 같이 Divide()함수에서 해결하지 못한 예외는 main함수의 try~catch에서 해결하게 된다.
한번 더 정리해서 말하자면

얘외가 처리되지 않으면, 예외가 발생한 함수를 호출한 영역으로 예외 데이터가(더불어 예외처리에 대한 책임까지) 전달된다.

예외상황이 발생한 위치와 예외상황을 처리해야하는 위치가 다른 경우

예시를 하나 보자

int StoI(char * str){
	int len = strlen(str);
    int num=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');
    }
}
int main(void)
{
	char str1[100];
    char str2[100];
    
    while(1){
    	cout << "두 개의 숫자 입력: ";
        cin >> str1 >> str2;
        
        try{
        	cout << str1 << " + " << str2 << " = " << StoI(str1)+StoI(str2) << endl;
            break;//정상적으로 StoI 함수가 작동할 때만 break가 실행되어 무사히 while문을 빠져나온다.
        }
        catch(char ch){// 만약 에러가 발생하면 에러이유를 설명하고 다시 while 문에 의해 처음부터 진행된다.
        	cout << "문자 " << ch << "가 입력되었습니다." << endl;
            cout << "재입력 진행합니다." << endl;
        }
    }
    cout << "프로그램 종료" << endl;
    return 0;
}

함수 내에서 함수를 호출한 영역으로 예외 데이터를 전달하면, 해당 함수는 더 이상 실행되지 않고 종료가 된다.

스택 풀기(Stack Unwinding)

예외 처리가 되지 않아서 함수를 호출한 영역으로 예외 데이터가 전달되는 현상을 가리켜 '스택 풀기'라고 한다.

  • 함수가 예외를 발생시키면 그 예외가 처리될 때까지, 호출된 함수의 역순으로 예외 데이터가 전달된다.
  • 이때, 예외 데이터를 전달한 함수는 종료되기 때문에, 예외 데이터를 전달한 함수의 스택이 반환되는 것은 당연하다. 그래서 예외 데이터의 전달을 가리켜, '스택 풀기(스택의 반환)'라고 하는 것이다.

    참고로, 예외 데이터가 main함수까지 왔는데도 main함수에서조차 예외를 처리하지 않으면, terminate 함수(프로그램 종료시키는 함수)가 호출되면서 프로그램이 종료되어 버린다.

따라서 시스템 오류로 인해서 발생한 예외상황이 아니라면, 더 이상 프로그램의 실행이 불가능한 예외상황이 아니라면, 반드시 프로그래머가 예외상황을 처리해야 한다.

자료형이 일치하지 않아도 예외 데이터는 전달된다.

앞서 예외 데이터의 자료형과 catch의 매개변수 형이 일치해야 한다고 설명했다.
만약 catch의 매개변수 형이 다르다면, 자료형의 불일치로 인해서 예외는 처리되지 않는다.(catch 블록으로 값이 전달되지 않는다.) 따라서 해당 함수를 호출한 영역으로 예외 데이터가 전달된다.

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

하나의 try블록 내에서 유형이 다른 둘 이상의 예외상황이 발생할 수 있고, 이러한 경우 각각의 예외를 표현하기 위해 사용되는 예외 데이터의 자료형이 다를 수 있기 때문에, try 블록에 이어서 등장하는 catch블록은 둘 이상이 될 수 있다.
위의 예시의 일부분을 따와 catch구문을 추가해보았다.

try{
  	cout << str1 << " + " << str2 << " = " << StoI(str1)+StoI(str2) << endl;
    break;//정상적으로 StoI 함수가 작동할 때만 break가 실행되어 무사히 while문을 빠져나온다.
}
catch(char ch){// 만약 에러가 발생하면 에러이유를 설명하고 다시 while 문에 의해 처음부터 진행된다.
  	cout << "문자 " << ch << "가 입력되었습니다." << endl;
    cout << "재입력 진행합니다." << endl;
}
catch(int expn){// 만약 에러가 발생하면 에러이유를 설명하고 다시 while 문에 의해 처음부터 진행된다.
  	cout << "0으로 시작하는 숫자가 입력되었습니다." << endl;
    cout << "재입력 진행합니다." << endl;
}
        

이런식으로 쓸 수 있다.

전달되는 예외의 명시

함수 내에서 발생할 수 있는 예외의 종류도 함수의 특징으로 간주된다. 따라서 이미 정의된 특정 함수의 호출을 위해서는 함수의 이름, 매개변수 선언, 반환형 정보에 더해서, 함수 내에서 전달될 수 있는 예외의 종류(예외 데이터의 자료형)과 그 상황도 알아야한다.
그래야 해당 함수의 호출문장을 감싸는 적절한 try~catch 블록을 구성할 수 있기 때문이다.

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

int main(void){
	...
    try{
    	Throwfunc(20);
    }
    catch(int expn){...}
    catch(char expn){...}
}

위와 같이 catch에 어떤 매개변수를 만들어야하는지 알 수 있다.

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

지금까지 기본 자료형 데이터만을 예외 데이터로 사용했는데, 클래스의 객체도 예외 데이터가 될 수 있으며, 이것이 좀더 일반적인 방법이다.

예외 클래스와 예외객체

예외발생을 알리는데 사용되는 객체를 가리켜 '예외객체'라 하며, 예외객체의 생성을 위해 정의된 클래스를 '예외 클래스'라고 한다.
예외객체를 이용해서 예외상황을 알리면, 예외가 발생한 원인에 대한 정보를 보다 자세히 담을 수 있다.

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

class Account{
private:
	accNum[50];//계좌 번호
    int balance; //잔고
public:
	Account(char *acc, int money) : balance(money)
    {
    	strcpy(accNum, acc);
    }
    void Deposit(int money) throw (DepositException)//이런 식으로 class를 throw타입으로 명시!
    {
    	if(money < 0){
        	DepositException expn(money);
            throw expn;
        }
        balance+=money;
    }
}

int main(void)
{
	Account myAcc("12345-12345", 5000);
    
    try{
    	myAcc.Deposit(200);
        myAcc.Deposit(-300);
    }
    catch(DepositException &expn){//예외 클래스를 매개변수로 받는다!
    	expn.ShowExceptionReason();// 예외 클래스의 예외이유 함수를 호출하여 예외를 보다 잘 알 수 있다!
    }
}

위 예제에서 보이듯이 예외 클래스라해서 딱히 특별한건 없지만, 해당 예외상황을 잘 표현할 수 있도록 정의하자. 그리고 너무 복작하게 정의하지 않는 것이 좋고, 말 그대로 예외의 표현을 위해 최소한의 기능만 담자.

상속관계에 있는 예외 클래스

예외 클래스도 클래스인 만큼 상속의 관계를 구성할 수 있다.
쓰는 방법도 전 챕터에서 말했던 상속 특성을 이용해서 사용하면 된다.

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

try블록의 뒤를 이어 등장하는 catch블록이 둘 이상인 경우, 적절한 catch블록을 찾는 과정은 위에서 아래로 하나씩 예외데이터를 받을 수 있는 매개변수를 가진catch블록을 찾아나간다. 그리고 적절한 catch블록을 찾으면 해당 catch블록이 실행되면서 예외의 처리는 완료가 된다.
따라서 상속받은 예외를 쓸때에는 순서에 주의하자 (최상위 부모 예외클래스를 catch블록 제일 위에 적으면 그 아래있는 모든 자식 예외 클래스는 작동이 안된다...)
최하위 자식 클래스부터 적으면 해결되긴한다.(근데 그럴땐 그냥 상속안하고 만드는게 맘 편하다..)

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

예외처리의 나머지 특성들을 설명한다.

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

new 연산에 의한 메모리 공간의 할당이 실패하면 bad_alloc이라는 예회가 발생하고, 이 bad_alloc은 헤더파일 <new>에 선언된 예외 클래스로써 메모리 공간의 할당이 실패했음을 알리는 의도로 정의되었다.

try{new int[111111111][11111111111][111111111]//강제로 HEAP 할당 공간을 터트리기}
catch(bad_alloc &bad){
	cout << bad.what() << endl;
    cout << "더이상 할당 불가!" << endl;
}

위와 같은 식으로 bad_alloc예외를 catch 블록의 매개변수로 받아 사용하면된다.

모든 예외를 처리하는 catch 블록

try{....}
catch(...){// catch구문 안에 ...은 try 블록 내에서 전달되는 모든 예외가 자료형에 상관없이 걸려든다.

}

위와 같이 catch의 매개변수로 ...를 적어주면 try 블록 내에서 전달되는 모든 예외가 자료형에 상관없이 걸려든다. 그러나 발생한 예외와 관련해서 그 어떠한 정보도 전달받을 수 없으며, 전달된 예외의 종류도 구분이 불가능하다.

예외 던지기

catch블록에 전달된 예외는 다시 throw를 통해 던져질 수 있다. 그리고 이로 인해서 하나의 예외가 둘 이상의 catch블록에 의해서 처리되게 할 수 있다.

  • 하지만 예외처리는 가급적 간결한 구조를 띠는게 좋다. 따라서 정말로 필요한 상황이 아니라면, 굳이 예외를 다시 던지려고 하지 말자.
profile
언제나 감사하며 살자!

0개의 댓글