모두의 코드: 씹어먹는 C ++ - <11. C++ 에서 예외 처리>

YP J·2022년 6월 25일
0

모두의코드 C++

목록 보기
9/11
  • C++ 에서의 예외 처리

  • throw, try, catch

예외란?

기존의 예외 처리 방식

  • C언어에서는 언어 차원에서 제공하는 예외 처리 방식이 딱히 없었다.
  • 따라서 어떤 작업을 실행한 뒤에 그 결과 값을 확인하는 방식으로 처리.
  • 아래 예제 malloc으로 메모리 동적 할당 살펴보자
char *c = (char *)malloc(1000000000);
if (c == NULL)
{
	printf(" malloc error ");
    return ;
}
  • malloc 의 경우 메모리 할당 실패시에 NULL을 리턴 하므로 ,

  • 위와같이 c가 null인지 확인하는걸로 예외처리를 한다.

  • 하지만 위와 같은 방법은 함수가 깊어질수록 귀찮아진다

bool func1(int *addr) {
  if (func2(addr)) {
    // Do something
  }
  return false;
}
bool func2(int *addr) {
  if (func3(addr)) {
    // Do something
  }
  return false;
}
bool func3(int *addr) {
  addr = (int *)malloc(100000000);
  if (addr == NULL) return false;
  return true;
}
int main() {
  int *addr;
  if (func1(addr)) {
    // 잘 처리됨
  } else {
    // 오류 발생
  }
}
  • 위 경우 func3에서 예외가 발생할수 있는 작업 수행

  • 만약 예외 발생하면 false, 안 하면 true; 리턴

  • 문제는 이 func3가 func2에서 호출되고 다시 func2 는 func1에 호출 되고

  • func1은 main에서 호출된다.

  • main 입장에서 fun3에서 문제가 발생했을때 이를 캐치 하기 위해서는

  • 각각의 함수들에서 처리 결과를 모두 리턴해야합니다.

  • 즉, 위 코드가 만약에 func2도 어떤 다른 작업을 해서 예외를 발생시킬수 있다면 어떻게 해야 할까요?

예외 발생시키기 -throw

  • C++에서는 예외가 발생하였다는 사실을 명시적으로 나타낼수 있다.
  • 바로 throw 문 !
 const T& at(size_t index) const {
    if (index >= size_) {
      throw out_of_range("vector 의 index 가 범위를 초과하였습니다.");
    }
    return data_[index];
  }
throw std::out_of_range("vector 의 index가 범위를 초과했습니다.");
  • C++ 에는 예외를 던지고 싶다면
  • throw 로 예외를 전달하고 싶은 객체를 써주면 됩니다.
  • C++ 표준 라이브러리에는 이미 여러 종류들의 예외들이 정의 되어있다.
  • out_of_range 객체를 throw 합니다. C++ 표준에는 out_of_range 외에도 overflow_error, length_error, runtime_error 등등 여러가지

**

  • 이렇게 예외를 throw 하게 되면
  • throw 한 위치에서 즉시 함수가 종료되고
  • 예외 처리하는 부분 까지 첨프 하게 된다.
  • 따라서 throw 밑에 있는 모든 문장은 실행 되지 않는다.
  • 이렇게 함수를 빠져 나가면
  • stack 에 생성되었던 객체들을 빠짐없이 소멸시켜준다.
  • 즉, 예외가 발생하여도 사용하고 있는 자원들을 제대로 소멸시킬 수 있다! ( 소멸자만 제대로 작성했다면 )

예외 처리하기 - try & catch

#include <iostream>
#include <stdexcept>

template <typename T>
class Vector
{
 public:
  Vector(size_t size) : size_(size)
  {
    data_ = new T[size_];
    for (int i = 0; i < size_; i++)
	{
      data_[i] = 3;
    }
  }
  const T& at(size_t index) const
  {
    if (index >= size_)
	{
      throw std::out_of_range("vector 의 index 가 범위를 초과하였습니다.");
    }
    return data_[index];
  }
  ~Vector() { delete[] data_; }

 private:
  T* data_;
  size_t size_;
};
int main()
{
  Vector<int> vec(3);

  int index, data = 0;
  std::cin >> index;

  try
  {
    data = vec.at(index);
  } catch (std::out_of_range& e)
  {
    std::cout << "예외 발생 ! " << e.what() << std::endl;
  }
  // 예외가 발생하지 않았다면 3을 이 출력되고, 예외가 발생하였다면 원래 data 에
  // 들어가 있던 0 이 출력된다.
  std::cout << "읽은 데이터 : " << data << std::endl;
}
try {
  data = vec.at(index);
}
  • try안에서 무언가 예외가 발생할 만한 코드가 실행된다

  • data는 vec의 index 번째 값이 들어가고

  • catch 문은 무시된다.

  • 반면 예외가 발생할경우

  • 그 즉시 stack에 생성된 모든 객체들의 소멸자들이 호출되고

  • 가장 가까운 catch문으로 점프한다.

  • 위의 경우

if (index >= size_) {
  throw std::out_of_range("vector 의 index 가 범위를 초과하였습니다.");
}
  • 의 throw 다음으로 실행되는 문장이 바로
catch (std::out_of_range& e) {
  std::cout << "예외 발생 ! " << e.what() << std::endl;
}
  • 이 catch 부분이다

  • 여기서 catch 문은 throw 된 예외를 받는 부분,

  • 어떤 예외를 받냐면

  • catch 문 안에 정의된 예외의 꼴에 맞는 객체를 받게 된다.

  • out_of_range 클래스는 아주 간단한데, 그냥 내부에 발생엔 예외에 관한 내용을 저장하는 문자열 필드가 달랑 하나 있고 이 역시 what() 함수로 그 값을 들여다 볼 수 있습니다.

  • 위 경우 우리가 전달한 문장인 'vector의 index가 범위를 초과하였습니다' 가 나오게 됩니다.

스택 풀기 (stack unwinding)

  • 앞서 throw 를 하게 된다면, 가장 가까운 catch 로 점프한다고 하였습니다. 이 말의 뜻이 무엇인지 아래 예제로 살펴봅시다.
#include <iostream>
#include <stdexcept>

class Resource {
 public:
  Resource(int id) : id_(id) {}
  ~Resource() { std::cout << "리소스 해제 : " << id_ << std::endl; }

 private:
  int id_;
};

int func3() {
  Resource r(3);
  throw std::runtime_error("Exception from 3!\n");
}
int func2() {
  Resource r(2);
  func3();
  std::cout << "실행 안됨!" << std::endl;
  return 0;
}
int func1() {
  Resource r(1);
  func2();
  std::cout << "실행 안됨!" << std::endl;
  return 0;
}

int main() {
  try {
    func1();
  } catch (std::exception& e) {
    std::cout << "Exception : " << e.what();
  }
}
>>
리소스 해제 : 3
리소스 해제 : 2
리소스 해제 : 1
Exception : Exception from 3!
  • func3 은 func2 가 호출하고 func2 는 func1 이 호출 하고 func1은 main()에서 호출한다.

  • 예외가 발생하게 되면 가장 가까운 catch 에서 예외를 받는다.

  • 그런데 func1, 2 모두 catch가 없으므로

  • 가장 가까운 catch문은 main함수에 있는 catch 구문이 된다.

  • 실제로도 예외가 main함수 까지 잘 전달되어서 출력 되었다.

  • 또 한가지 중요한 점은 예외가 전파되면서 각 함수들에 정의 되어있던 객체들이 잘 소멸 되었다는 점이다.

  • 만약 func3에서 throw하지 않는다면

#include <iostream>
#include <stdexcept>

class Resource {
 public:
  Resource(int id) : id_(id) {}
  ~Resource() { std::cout << "리소스 해제 : " << id_ << std::endl; }

 private:
  int id_;
};

int func3() {
  Resource r(3);
  return 0;
}
int func2() {
  Resource r(2);
  func3();
  std::cout << "실행!" << std::endl;
  return 0;
}
int func1() {
  Resource r(1);
  func2();
  std::cout << "실행!" << std::endl;
  return 0;
}

int main() {
  try {
    func1();
  } catch (std::exception& e) {
    std::cout << "Exception : " << e.what();
  }
}
>>
리소스 해제 : 3
실행!
리소스 해제 : 2
실행!
리소스 해제 : 1
  • 정상적일 경우 실행! 이 출력되고 리소스 해제 되는데

  • 그 전 예외 발생 코드에서는 예외 발생시 바로 catch문으로 점프 하면서 각 함수들에 있던 객체들만 해제 하기 때문에

  • 리소스는 해제는 정상적으로 되고 출력문도 잘 나오지만

  • 그사이에 있는 "실행 안됩!" 부분은 건너 뛴다.

  • 이와 같이 catch 로 점프 하면서 스택 상에서 정의된 객체들을 소멸시키는 과정을 스택 풀기(stack unwinding) 이라고 부릅니다.

-예외를 생성자에서 던질 때 주의해야 할 점이 하나 있습니다.

  • 바로 생성자에서 예외가 발생 시에 소멸자가 호출되지 않는다 라는 점입니다.
  • 따라서, 만일 예외를 던지기 이전에 획득한 자원이 있다면 catch 에서 잘 해제시켜 줘야만 합니다.

여러종류의 예외 받기

pass~

profile
be pro

0개의 댓글