C++ 공부 - 모두의 코드(3)

자훈·2023년 11월 21일
0

C++ / C study

목록 보기
4/8
post-thumbnail

🌟 가상함수 복습

#include <iostream>

class Base {
public:
    int baseValue;

    Base(int value) : baseValue(value) {}

    virtual void print() const {
        std::cout << "Base value: " << baseValue << std::endl;
    }
};

class Derived : public Base {
public:
    int derivedValue;

    Derived(int base, int derived) : Base(base), derivedValue(derived) {}

    void print() const override {
        std::cout << "Derived value: " << derivedValue << std::endl;
    }
};

int main() {
    Derived derivedObj(10, 20);
    
    // Rule 1: p_ancestor = p_descendant
    Base* p_ancestor = &derivedObj;

    // Rule 2: No data members will be lost
    p_ancestor->print();  // Calls the overridden print function in Derived class
    
    return 0;
}

main 함수에서 파생 클래스 객체를 생성하고, 기본 클래스 포인터로 가리키는 것을 확인할 수 있습니다. 이렇게 하면 데이터 멤버가 손실되지 않고, 가상 함수를 통해 파생 클래스의 버전이 호출됩니다.

1. p_ancestor가 p_descendant를 가리키는 것은 허용됩니다.

p_ancestor가 기본 클래스(Base class)의 포인터이고, p_descendant가 이를 상속한 파생 클래스(Derived class)의 포인터일 경우, p_ancestor에 p_descendant를 할당할 수 있습니다. 이를 통해 기본 클래스 포인터로 파생 클래스 객체를 가리킬 수 있습니다.

2. 데이터 멤버는 손실되지 않습니다.

기본 클래스 포인터 p_ancestor로 파생 클래스 객체를 가리키는 경우, 실제로 파생 클래스 객체의 모든 데이터 멤버가 그대로 유지됩니다. 데이터 멤버에 대한 정보 손실이 발생하지 않습니다.

📌 Error handling

에러 핸들링은 말 그대로 예외처리를 하는 방법에 관한 것이다. if - else 문이나 swith문에서도 이와 같은 방식으로 처리가 가능하지만, 가독성의 측면과 기능을 구분하는 차이에서 error handling과 관련된 exception의 기능을 직접 사용하는 것이 훨씬 더 좋을 것이다.
기본적으로 throwcatch를 이용해서 에러를 처리한다.

아래는 예제 코드이다.

#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();
  }
  return 0;
}

리소스 해제 3
실행!
리소스 해제 2
실행!
리소스 해제 1

예외가 발생하지 않을 경우에는, 해당 코드를 실행했을 때의 결과값처럼 함수안에 있는 실행을 출력하는 문구가 잘 작동한다. 하지만 에러가 발생하게 될 경우, 객체에 대해서만 소멸자만 진행되고, 출력과 관련된 내용은 실행시키지 않는다.

#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!

보이는 것과 같이, 리소스 해제인 소멸자 함수에 대한 작동은 제대로 이루어지지만 그 이외의 기능은 실행하지 않는다. 예외가 발생하지 않았을 경우에는 정상적으로 작동이 되므로 이 차이를 알고 있어야 한다.
그리고 이러한 과정을 스택 풀기라고 한다.

🌟 클래스 객체 예외처리

기본적으로 catch는 어떤 종류의 throw든 잘 받을 수 있습니다. 물론 번거롭게 다 설정을 해야하겠지만요. 그렇기에 이 부분에 대해서는 문제가 없지만, 클래스 객체를 리턴하게 될 때 특이한 현상이 발생합니다.

#include <exception>
#include <iostream>

class Parent : public std::exception {
 public:
  virtual const char* what() const noexcept override { return "Parent!\n"; }
};

class Child : public Parent {
 public:
  const char* what() const noexcept override { return "Child!\n"; }
};

int func(int c) {
  if (c == 1) {
    throw Parent();
  } else if (c == 2) {
    throw Child();
  }
  return 0;
}

int main() {
  int c;
  std::cin >> c;

  try {
    func(c);
  } catch (Parent& p) {
    std::cout << "Parent Catch!" << std::endl;
    std::cout << p.what();
  } catch (Child& c) {
    std::cout << "Child Catch!" << std::endl;
    std::cout << c.what();
  }
}

1
Parent Catch!
Parent!
2
Parent Catch!
Child!

위와 같이 출력값이 나오게 된다. 2번을 보자.
int func에서 throw를 통해 Child클래스 객체를 던졌고, 받아서 Child Catch!와 함께 c.what()를 실행해야하지만, Parent Catch가 나온다. 왜 그럴까??

심플하게 생각하면 된다. 자식 클래스의 잘못은 부모 클래스가 처리한다.
catch 문의 경우 가장 먼저 대입될 수 있는 객체를 받는다.

Parent& p = Child();

는 가능하다. 상속과 포인터의 개념을 잘 따라왔다면 이것의 역은 성립하지 않는다는 것을 알 수 있을 것이다.

🌟 예외 명세(specific exception)

void someFunction() throw(); // 빈 예외 명세; 모든 예외를 처리하지 않으면 프로그램이 종료됩니다.

void someFunction() throw(DivideByZero, OtherException);
// DivideByZero 및 OtherException을 제외한 다른 모든 예외는 프로그램을 종료시킵니다.

void someFunction();
// 모든 종류의 예외를 정상적으로 처리하며, catch 블록에 잡히지 않으면 프로그램이 종료됩니다.

예외 명세란, 해당 함수에서 발생하는 예외에 대해서 명시해놓은 곳으로, 함수 뒤에 작성한다.

void someFunction() throw()는 이 함수내에서는 예외가 발생하지 않는다는 것을 말하고, 만약 발생한다면 프로그램이 종료된다.
void someFunction() throw(DivideByZero, OhterException)는 발생할 수 있는 예외를 명시하여, 이 함수 내에서는 해당 예외가 발생할 수 있으며, 이외에 예외에 대해서는 프로그램을 종료시킵니다.
void someFunction() 는 함수는 어떤 예외도 던질 수 있으며, 예외가 발생하면 해당 예외가 catch 블록에 잡히지 않으면 std::terminate()가 호출되어 프로그램이 종료된다.

Specific exception

  • 일반적으로 예외처리 블록은 가장 구체적인 것 부터 작성한다. 이러한 예외 처리 구조를 통해 예외가 어떤 종류인지에 따라 적절한 조치를 취할 수 있습니다. 또한, 가장 구체적인 예외부터 처리하므로 예외에 대한 정확하고 명확한 대응이 가능하다.

  • 상위 클래스보다 하위 클래스의 예외를 먼저 처리하는 것이 일반적이다.

  • catch(...) 로 Default Catch가 가능하다. 모든 예외를 받는 것이 가능하다는 말이다..

  • noexcept를 통해 예외가 발생하지 않는다는 것을 명시할 수도 있다

    #include
    int foo() noexcept {}
    int bar(int x) noexcept { throw 1; }
    int main() { foo(); }

noexcept를 붙였다고 해서, throw로 객체를 던질 수 없는 것은 아니다. 오류는 발생하나 컴파일은 된다. noexcept가 붙은 함수가 예외를 발생시키지 않는구나~~~ 하고 컴파일을 실행하게 되는 것입니다.

본 내용은 씹어먹는c++을 통해 알게된 내용을 개인적인 공부를 위해 정리한 포스팅입니다. 저작권을 해칠 의도가 없으며, 모든 것은 해당 블로그 저자의 지식재산입니다.
https://modoocode.com/210

0개의 댓글