C++(기법) - 5.예외처리

Ui Jin·2021년 10월 22일
0

C++ Grammar

목록 보기
13/13

예외처리란?

프로그램을 아무리 잘 작성했다고 하더라도 프로그램의 외적인 요인에 의해서 오류는 필연적으로 발생하게 됩니다.

(예를들면 컴퓨터에서 사용가능한 메모리가 부족해진다고 하거나, 사용자가 범위 밖의 값을 입력하는 경우가 있겠죠)

이러한 경우 프로그램이 오작동하거나 갑자기 죽어버리는 경우가 발생합니다. 이 경우 작성 내용이 사라지거나 프로그램 전체에 영향을 끼칠 수 있겠죠.

즉, 이를 막고 견고한 프로그램을 만들기 위해서는 예외처리가 필수적이라고 볼 수 있습니다.


반환값을 사용한 예외처리

C++이 아닐경우 주로 사용하는 방법입니다. 이 경우 해당 함수가 bool type을 반환하도록해서 예외처리를 하게됩니다.

이렇게 이야기하면 잘 이해가 안되실것이 당연하므로 다음의 예시를 참고해봅시다.

다음은 myArray클래스에서 멤버 변수에 값을 넣는 Setx()함수를 구현한 것입니다.

bool myArray::Setx(int index, int value) {
    if(index < 0 || index >= GetSize() )
        return false;
    else {
        arr[index] = value;
        return true;
    }
}

이 함수는 멤버 변수에 값을 넣고 bool type을 반환합니다.

즉, 이런 방법으로 예외처리를 하게될 경우 반환값은 별도로 사용이 불가하고, 항상 반환값을 확인해가며 프로그래밍을 진행해야 된다는 단점이 발생합니다.


구조적 예외처리

SEH(Structured Exception Handling)

위와 같은 문제를 해결하기 위해서 C++에서는 별도의 키워드를 제공하여 예외처리를 도와줍니다.

이 키워드는 throw, try, catch 입니다. 뭐... 대충 어떤것인지 예상이되지 않나요? 아닌가요...ㅎㅎ

네, 뭔가 try문 안에서 함수를 호출할 때, throw는 예외를 던지고, catch는 그것을 받는 역할을 할 것 같지 않나요..?

위의 개념을 생각을 해보면서 이제 이 키워드들을 살펴봅시다.

trycatch

try는 예외를 던지는 범위를 지정하는 역할을 하게됩니다.

catch는 항상 try와 같이 쓰이면서 try에서 던저진 예외를 받는 역할을 하게 됩니다.

try{
    ~
} catch() {
    ~
}

즉, 위와같은 코드는 try안에서 발생한 예외에 대해서만 처리하게 되고 이 예외는 catch로 보내지게 됩니다.

이때 이때 catch()의 매개변수에는 반드시 한가지 타입의 매개변수만 적어 주어야 한다는것을 꼭 기억하고 주의해줍시다.

try{
    ~
} catch(cosnt char* example) {
    ~
}

위의 코드는 cosnt char* type의 예외만 catch하게됩니다.

throw

throwtry문 안에서 catch문으로 실행의 흐름을 바꾸어 주는 역할을 합니다.

try{
    ~
    throw 3;
    ~
}
catch(int i) {
    cout << i << endl;
}

즉 위와 같은 코드의 경우 try문을 실행하다가 throw를 만나면 try를 탈출하고 3catch의 Parameter에 전달하여 catch 안의 문장을 실행하게 됩니다.

throw는 꼭 try catch문 안에 존재할 필요는 없습니다.

만약 어떤 함수안에 throw키워드를 쓰고, 그 함수를 try문 안에서 실행할 경우 그 함수를 실행하다 throw를 만나게 되면 catch로 다음 실행을 옮겨주게 됩니다.


구조적 예외처리 - 심화

1) catch활용

  • 여러개의 catch

    여러개의 하나의 try문에 여러개의 catch를 붙이는 것이 가능합니다. 단, 이때는 각 catch의 Parameter에 서로 다른 type을 넣어주어야 합니다.

    try{
        ~
    }
    catch(int x) {
        ~
    }
    catch(const char* ex) {
        ~
    }

    이때 예외가 발생할 경우 위의catch에서부터 해당 예외를 받을 수 있는 Parameter가 있는지 차근차근 비교하며 내려오게 됩니다. 이를 유의하면서 작성 합시다.

  • 모든 예외를 받는 catch

    어떤 예외가 던져질 지 모를 때는 다음과 같은 방법으로 모든 예외를 받아줄 수 있습니다.

    try{
        ~
    }
    catch(int x) {
        ~
    }
    catch(...) {	//  모든 예외를 받는 구문
        ~
    }

    (참고)
    만약 모든 예외를 받는 catch를 맨 처음에 적을 경우뒤의 catch는 평생 실행될 일이 없게 되겠죠?

2) 객체 던지기

위의 예시에서는 기본 타입을 통해 예외를 처리했습니다. 하지만 실제 현장에서는 대부분 기본타입의 값 대신에 객체를 던져서 예외를 처리하게 됩니다.

이렇게 객체를 사용하면 어떠한 장점이 있을까요?

1. 다양한 정보 전달

우선 오류의 내용을 받기 위해서 던질 예외 객체를 만들어 봅시다.

class MyException
{
public:
    const void* sender;      // 누가 예외를 일으켰는지 알기 위해서,
    			     // 그 주소를 저장하기 위한 포인터입니다.
    const char* description; // 예외에 대한 설명을 저장하기위한 곳입니다.
    
    MyException(const void* sender, const char* des) {
      this -> sender = sender;
      this -> descrition = des;
    }
};

이렇게 예외를 던질 때 필요한 정보들을 넣어줄 수 있는 객체를 만들어 주었습니다.

try
{
    throw MyException(this, "Out of Range");
}
catch(MyException& ex)
{
    ~;
}

그 후에 위처럼 객체를 catch에 전달하면 오류에 관한 내용을 훨씬 수월하고 많이 전달할 수 있게 되겠죠?

(참고)
객체는 레퍼런스형식으로 전달하는것을 추천드립니다.

만약 객체를 전달하면 불필요한 복사가 일어날 것이고 포인터로 전달하게 되면 메모리 할당과 해제를 신경써야 하기 때문입니다.

2. 다형성 활용 가능

위에서 catch를 사용할 때 주의해야 할 점이 어떤것이었는지 기억 나시나요?

바로 하나의 catch는 1개의 Parameter만 사용할 수 있다는 것입니다.

이때, 객체의 다형성을 활용하면 이 1개의 catch에서 1개의 파라미터 (부모 포인터 혹은 레퍼런스)로도 여러개의 예외를 다룰 수 있게 된다는 것입니다!!

여러모로 객체가 프로그래밍에 있어 큰 도움을 준다고 느껴지지 않나요? 감탄이 나오네요...

class MyException
{
public:
    const void* sender;      // 누가 예외를 일으켰는지 알기 위해서,
    			     // 그 주소를 저장하기 위한 포인터입니다.
    const char* description; // 예외에 대한 설명을 저장하기위한 곳입니다.
    
    MyException(const void* sender, const char* des) {
      this -> sender = sender;
      this -> descrition = des;
    }
};

즉, 위의 객체를 상속받아 여러개의 객체를 만들어 봅시다.

class RangeException: public MyException
{
public:
    RangeException(const void* sender)
        :MyException(sender, "Out of Range")
    {}
};

class MemoryException: public MyException
{
public:
    MemoryException(const void* sender)
        :MyException(sender, "Out of Memory")
    {}
};

이렇게 오류처리를 위한 객체를 하나의 객체를 상속받아 여러개를 만들어 보았습니다.

try{
    ~
}
catch(MyException& error){
    ~
}

이렇게 되면 위와같이 이 부모객체인 MyException의 레퍼런스를 통해서 자식객체들을 받아서 처리할 수 있겠죠?

3) 예외를 다시 던지기

음.. 글로 설명하기는 다소 힘들것 같아서 다음과 같은 코드를 생각해 봅시다.

try
{
    try
    {
        ~
    } catch(int a){
        ~
    }
    cout << "hello" << endl;
    
} catch(int a)
{
    ~
}

이 경우 hello는 출력이 될까요?

  • try문 안쪽의 try문에서 오류가 발생하지 않을 경우

    네, 당연히 출력 됩니다

  • try문 안쪽의 try문에서 오류가 발생할 경우

    이 경우 안쪽 try문에 해당하는 catch가 실행되게 되고, 그 후에 이 catch문을 빠져나와 실행되게 됩니다.

즉, 이때는 외부의 catch에서는 안쪽의 예외를 받을 수 없다는 문제가 생기는데요 이때 안쪽에서 바깥으로 다시 예외를 던져주는 방법이 있습니다.

try
{
    try
    {
        ~
    } catch(int a){
        ~
        throw;
    }
    cout << "hello" << endl;
    
} catch(int a)
{
    ~
}

바로 위와같이 안쪽의 catch문 안에서 throw를 통해 바깥으로 던져주는 것입니다. 이때는 따로 전달할 것을 안적어 주셔도 됩니다.

주의점

1) auto_ptr

구조적 예외처리는 이처럼 매우 편리하지만 반드시 고려해야될 주의점이 존재합니다.

힌트를 드리자면 throw를 만나게 되면 우리가 원하는 코드를 실행하지 않는다는 것입니다.

네, 바로 동적으로 생성해준 메모리를 해제하기 전에 throw를 만나게 되면 Memory leak이 발생하게 되겠죠

즉, 예외를 던지기 전에 관리해주는 작업이 필요합니다. 하지만 이런과정들은 복잡하고 항상 신경쓰고 있어야 하므로 프로그래밍이 힘들어지겠죠?

이때 우리가 생각할 수 있는것은 객체의 소멸자입니다. 소멸자는 항상 호출이 된다는 것을 이용해서 다음과 같이 활용해 봅시다.

class SmartPointer 
{
public:
    SmartPointer(char* p)
        :ptr(p)
    { }
    ~SmartPointer() {
        delete[] ptr;
    }
public:
    char* const ptr;
};

try
{
    char* ptr = new char[];
    SmartPointer Sp(p)

    throw "Error";
}
catch(const char* err){
    cout << err << endl;
}

위와 같이 작성할 경우 try문을 벗어남과 동시에 SmartPointer의 소멸자는 반드시 호출되게 될 것이므로 우리는 메모리 해제를 신경쓰지 않아도 되게 됩니다!

자, 이 주의점의 제목이 auto_ptr인 이유는 C++에서는 자체적으로 이러한 스마트 포인터 클래스를 제공하고 있기 때문입니다.

이 포인터를 사용하기 위해서는 다음과 같은 전처리가 필요합니다.
#include <memory>

그 다음 auto_ptr< a > b(new c) 와 같은 형식으로 포인터를 생성해주면
(a는 포인터가 가리킬 변수type, )
(b는 포인터 이름, )
(c는 동적 생성할 변수 type)

이 포인터는 사실 객체임에도 *b와 같이 포인터처럼 사용가능합니다.

(참고)
이러한 방법으로 동적 할당을 해줄 경우 따로 해제해주지 않아도 되서 매우 편리하지만 몇가지 단점들이 존재합니다.

2) 생성자와 소멸자의 예외처리

일반적으로 객체는 오류가 발생하던 하지않던 소멸자가 호출되기 때문에 비교적 오류에서 안전하다고 여겨집니다. 하지만 생성자나 소멸자에서 오류가 발생할 경우에는 문제가 발생합니다.

  • 생성자의 예외처리

    생성자에서 오류가 발생할 경우 해당 객체는 생성된것이 아니기 때문에 소멸자가 호출되지 않습니다. 따라서 다음과 같이 생성자 안에서 try문을 사용하고 오류를 한번더 던져줍시다.

    Myarray::Myarray()
    {
        try
        {    
             arr = new int(3)
             throw ~
        }
        catch(...)
        {
            delete arr
            throw
        }
    }
  • 소멸자의 예외처리

    위와 같은 이유로 try문을 사용해 줍시다.

    단, 소멸자에서 잡은 예외는 절대로 다시 밖으로 던져서는 안됩니다.

    Myarray::~Myarray()
    {
       try
       {    
            delete[] arr;
            arr = 0;
       }
       catch(...)
       { }
    }
profile
github로 이전 중... (https://uijinee.github.io/)

0개의 댓글